三、Redis基本操作——List
小喵的唠叨话:前面我们介绍了Redis的string的数据结构的原理和操作。当时我们提到Redis的键值对不仅仅是字符串。而这次我们就要介绍Redis的第二个数据结构了,List(链表)。由于List在原理上的实现并不是特别的复杂,我们在这里将原理和具体的命令都放在一起介绍。
小喵的个人博客地址: http://www.miaoerduo.com/ ,欢迎随时骚扰~
该博客原地址: http://www.miaoerduo.com/redis/三、redis基本操作-list.html ,排版应该更精美一点。
Redis基本操作——List(原理篇)
学习过数据结构的同学,一定对链表(Linked List)十分的熟悉。相信我们自己也曾经使用过这种数据结构。
链表分为很多种:单向链表,双向链表,循环链表,块状链表[1]等等。
链表的作用也有很多。首先,链表可以存放数据。其次链表可以模拟队列、堆栈等其他的数据结构。
链表的实现也有多种,以C语言为例,最常见的是构造节点node,node中又有指针,用于指向下一个node,这样就构成了单向链表,如果node中有两个指针,分别指向前一个和后一个node,则构造了双向链表,再如果链表是首尾相连的,那么就是循环链表。链表的具体算法,是数据结构里面的内容,我们这里就不专门介绍啦。
那么,Redis内部的List是什么样子的链表呢?又是怎么来维护的呢?
我们慢慢的剖析。
1 typedef struct listNode { 2 3 // 前置节点 4 struct listNode *prev; 5 6 // 后置节点 7 struct listNode *next; 8 9 // 节点的值 10 void *value; 11 } listNode;
上述就是Redis的节点的定义。
可以看出,这是一个双向的链表。比较有意思的是,节点内存的值,是一个void *的指针,这就说明节点内部可以存放任何的内容。
反派汪:我不能理解为什么value是void *的指针就能存任何内容?我甚至不知道为什么还有要void *这么奇怪的数据类型。
喵太:其实很多人也有这个疑问。不过先问问你自己,指针到底是什么?int *,float *和char *的这几种指针有什么区别?
反派汪:指针里面存的就是地址,通过地址就能访问到指定的内存。以int *指针为例,int *说明,指针指向的是一个由int型的数据组成的内存。通过指针运算符*,我们就能取到对应位置的内存中的值。通过对指针的+和-,我们就能移动到相邻的位置。
喵太:嗯,说的不错,但是你只说对了一半。指针里面存放的是地址,无论任何类型的指针,里面存放的都只是一个32位的无符号整数。指针的类型,其实只是决定了对内存进行的操作。int *的指针,每次加1的时候,其实移动了4个字节,char *则移动1个。使用指针运算符*其实也是一样。int *会得到连续4个字节的内容,并且存进一个int变量里面,而char *则会得到一个字节的内容,并存进一个char里面。因此,任何类型的指针其实本质上是一样的。
反派汪:那你的意思是,所有类型的指针都是一样的了。那么我还是不能理解void *的指针究竟能做什么。
喵太:void *的指针是C语言中最简单的一种指针,它存放的是一个地址,并且没有给出任何操作上的提示。但是任何类型的指针都能赋给void *的指针。void *的指针也能强制转换成任何类型的指针。这里使用void *的指针,只是在说明,value对应的是一块内存。开发者,可以自行转换指针的类型,来做相应的操作。如果你熟悉C语言的话,malloc函数的返回值也是一个void *的指针,而我们通常会把它强制转换成我们需要的任何数据类型。
反派汪:这样啊,那原则上这里的指针用什么类型的都是可以的了?反正就是一个地址而已。只是void *更纯粹一点,也最符合要求,毕竟它只是指向数据的地址,没有做任何的限制。
喵太:孺狗可教也。
现在,我们有了listNode这个节点的数据结构,就可以构造我们的链表了。
图一 链表的结构
依照上图的示意图,我们就可以使用listNode构造一个双向链表了。
Redis中,为了更好地管理链表,定义了一个list的数据结构,作为链表的封装。
1 typedef struct list { 2 3 // 头节点 4 listNode *head; 5 6 // 尾节点 7 listNode *tail; 8 9 // 链表中的节点数 10 unsigned int len; 11 12 // 节点值复制函数 13 void *(*dup) (void *ptr); 14 15 // 节点值释放函数 16 void (*free) (void *ptr); 17 18 // 节点值对比函数 19 int (*match) (void *ptr, void *key); 20 } list;
list结构记录了链表的头指针,尾指针,链表的节点数。dup,free和match三个成员则表示对节点的值进行复制,释放和比较的函数。由于这里的节点的value的内容是任意的,复制和释放并不一定能用类似于memcpy和free的函数来处理(不太理解的话,可以google一下浅拷贝和深拷贝)。
因此,我们得到的最终的链表结构是这个样子的:
图二 链表的结构
Redis的链表的实现的主要特性如下:
- 双端:链表节点都有prev和next指针,这样获取一个节点的前置节点和后置节点的算法复杂度都为O(1)。
- 无环:list的第一个节点(头节点)的prev和最后一个节点(尾节点)的next都指向NULL。
- 带表头指针和表尾指针:通过list的head和tail两个指针,可以随意的从链表的头和尾进行操作。
- 带链表长度计数器:可以通过len成员来获取链表的节点的个数,复杂度O(1)。
- 多态:链表使用void *指针来保存value,并且可以通过dup,free,match来操控节点的value值,因此,该链表可以保存任意类型的值。
小喵看到这个多态的实现也是惊呆了,原来C语言也能实现多态,也第一次知道了函数指针居然可以这么用。
由此想到之前看过的一句话:高手用树叶也能杀人。希望每个人都能成为传说中的高手~
关于链表的操作,是数据结构课程中的基础,无外乎是增、删、改、查这四个操作,这里不再介绍。聪明的你,bing一下就OK了!
Redis基本操作——List(实战篇)
现在,我们已经基本了解了Redis的List结构的底层实现。那么,Redis提供了哪些可以供我们调用的接口呢?请听小喵慢慢道来。
以下指令根据类别和使用的频率进行排序。
指令清单:
- BLPOP
- BRPOP
- BRPOPLPUSH
- LINDEX
- LINSERT
- LLEN
- LPOP
- LPUSH
- LPUSHX
- LRANGE
- LREM
- LSET
- LTRIM
- RPOP
- RPOPLPUSH
- RPUSH
- RPUSHX
一、PUSH操作
1,RPUSH key value [value ...]http://redis.cn/commands/blpop.html。
4,BRPOP key [key ...] timeout2]:
模式:安全的队列
Redis通常都被用做一个处理各种后台工作或消息任务的消息服务器。 一个简单的队列模式就是:生产者把消息放入一个列表中,等待消息的消费者用 RPOP 命令(用轮询方式), 或者用 BRPOP 命令(如果客户端使用阻塞操作会更好)来得到这个消息。
然而,因为消息有可能会丢失,所以这种队列并是不安全的。例如,当接收到消息后,出现了网络问题或者消费者端崩溃了, 那么这个消息就丢失了。
RPOPLPUSH (或者其阻塞版本的 BRPOPLPUSH) 提供了一种方法来避免这个问题:消费者端取到消息的同时把该消息放入一个正在处理中的列表。 当消息被处理了之后,该命令会使用 LREM 命令来移除正在处理中列表中的对应消息。
另外,可以添加一个客户端来监控这个正在处理中列表,如果有某些消息已经在这个列表中存在很长时间了(即超过一定的处理时限), 那么这个客户端会把这些超时消息重新加入到队列中。
模式:循环列表
RPOPLPUSH 命令的 source 和 destination 是相同的话, 那么客户端在访问一个拥有n个元素的列表时,可以在 O(N) 时间里一个接一个获取列表元素, 而不用像 LRANGE 那样需要把整个列表从服务器端传送到客户端。
上面这种模式即使在以下两种情况下照样能很好地工作: * 有多个客户端同时对同一个列表进行旋转(rotating):它们会取得不同的元素,直到列表里所有元素都被访问过,又从头开始这个操作。 * 有其他客户端在往列表末端加入新的元素。
这个模式让我们可以很容易地实现这样一个系统:有 N 个客户端,需要连续不断地对一批元素进行处理,而且处理的过程必须尽可能地快。 一个典型的例子就是服务器上的监控程序:它们需要在尽可能短的时间内,并行地检查一批网站,确保它们的可访问性。
值得注意的是,使用这个模式的客户端是易于扩展(scalable)且安全的(reliable),因为即使客户端把接收到的消息丢失了, 这个消息依然存在于队列中,等下次迭代到它的时候,由其他客户端进行处理。
2,BRPOPLPUSH source destination timeout
弹出一个列表的值,将它推到另一个列表,并返回它;或阻塞,直到有一个可用
RPOPLPUSH的阻塞版本。timeout的单位是秒,当timeout为0的时候,表示无限期阻塞。
具体使用参考RPOPLPUSH。
四、其他
1,LLEN key
获得队列(List)的长度
和名字一样易懂,这里不再过多解释。
2,LRANGE key start stop
从列表中获取指定返回的元素
我们在前面用到了很多次。LRANGE可以获取list的指定范围的值。范围用start和stop表示。负数表示从右向左数。
需要注意的是,超出范围的下标不会产生错误:如果start>end,会得到空列表,如果end超过队尾,则Redis会将其当做列表的最后一个元素。
3,LINDEX key index
获取一个元素,通过其索引列表
我们之前介绍的操作都是对list的两端进行的,所以算法复杂度都只有O(1)。而这个操作是指定位置来进行的,每次操作,list都得找到对应的位置,因此算法复杂度为O(N)。list的下表是从0开始的,index为负的时候是从右向左数。-1表示最后一个元素。当下标超出的时候,会返回nul。所以不用像操作数组一样担心范围越界的情况。
1 redis> rpush q1 a b c d f e g 2 (integer) 7 3 redis> lrange q1 0 -1 4 1) "a" 5 2) "b" 6 3) "c" 7 4) "d" 8 5) "f" 9 6) "e" 10 7) "g" 11 redis> lindex q1 0 12 "a" 13 redis> lindex q1 3 14 "d" 15 redis> lindex q1 4 16 "f" 17 redis> lindex q1 -1 18 "g" 19 redis> lindex q1 100 20 (nil)
4,LSET key index value
设置队列里面一个元素的值
这个操作和LINDEX类似,只不过LINDEX是读,而LSET是写。下标的用法和LINDX相同。不过当index越界的时候,这里会报异常。
1 redis> rpush q1 a b c d f e g 2 (integer) 7 3 redis> lrange q1 0 -1 4 1) "a" 5 2) "b" 6 3) "c" 7 4) "d" 8 5) "f" 9 6) "e" 10 7) "g" 11 redis> lset q1 0 1 12 OK 13 redis> lset q1 3 3 14 OK 15 redis> lset q1 -1 0 16 OK 17 redis> lset q1 100 0 18 (error) ERR index out of range 19 redis> lrange q1 0 -1 20 1) "1" 21 2) "b" 22 3) "c" 23 4) "3" 24 5) "f" 25 6) "e" 26 7) "0"
5,LREM key count value
从列表中删除元素
该命令用于从key对应的list中,移除前count次出现 的值为value的元素。count参数有三种情况:
count > 0: 表示从头向尾(左到右)移除值为value的元素。
count < 0: 表示从尾向头(右向左)移除值为value的元素。
count = 0: 表示移除所有值为value的元素。
1 redis> rpush q 1 0 1 1 2 1 0 1 1 2 (integer) 9 3 redis> lrange q 0 -1 4 1) "1" 5 2) "0" 6 3) "1" 7 4) "1" 8 5) "2" 9 6) "1" 10 7) "0" 11 8) "1" 12 9) "1" 13 redis> lrem q 2 1 14 (integer) 2 15 redis> lrange q 0 -1 16 1) "0" 17 2) "1" 18 3) "2" 19 4) "1" 20 5) "0" 21 6) "1" 22 7) "1" 23 redis> lrem q -2 1 24 (integer) 2 25 redis> lrange q 0 -1 26 1) "0" 27 2) "1" 28 3) "2" 29 4) "1" 30 5) "0" 31 redis> lrem q 0 1 32 (integer) 2 33 redis> lrange q 0 -1 34 1) "0" 35 2) "2" 36 3) "0"
6,LTRIM key start stop
修剪到指定范围内的清单
这个命令和LRANGE十分相似,LRANGE会将指定范围的元素返回给客户端,而LTRIM会对list进行修剪,使其只包含指定范围的元素。start和stop表示范围。
超出范围的下标不会产生错误:如果start>end,会得到空列表,如果end超过队尾,则Redis会将其当做列表的最后一个元素。
1 redis> rpush q a b c d e f g 2 (integer) 7 3 redis> lrange q 0 -1 4 1) "a" 5 2) "b" 6 3) "c" 7 4) "d" 8 5) "e" 9 6) "f" 10 7) "g" 11 redis> ltrim q 1 4 12 OK 13 redis> lrange q 0 -1 14 1) "b" 15 2) "c" 16 3) "d" 17 4) "e"
7,LINSERT key BEFORE|AFTER pivot value
在列表中的另一个元素之前或之后插入一个元素
该命令将value插入值key对应的列表的基准值pivot的前面或是后面。
当key不存在时,这个list被视为空列表,任何操作都不会发生。
当key存在,但保存的不是list,则会报error。
该命令会返回修改之后的list的长度,如果找不到pivot,则会返回-1。
1 redis> rpush q a b c d e 2 (integer) 5 3 redis> lrange q 0 -1 4 1) "a" 5 2) "b" 6 3) "c" 7 4) "d" 8 5) "e" 9 redis> linsert q before c 0 10 (integer) 6 11 redis> linsert q after c 0 12 (integer) 7 13 redis> linsert q before 0 1 14 (integer) 8 15 redis> linsert q after 0 1 16 (integer) 9 17 redis> lrange q 0 -1 18 1) "a" 19 2) "b" 20 3) "1" 21 4) "0" 22 5) "1" 23 6) "c" 24 7) "0" 25 8) "d" 26 9) "e" 27 redis> linsert q after w 1 28 (integer) -1
这里,我们构造了一个list{a, b, c, d, e},先在c的前后插入了0,再在0前后插入了1。可以看出,linsert只会对第一个匹配pivot的前后进行操作。
最后,我们在w前面插入1,但是list中并没有w,所以返回了-1。
转载请注明出处~~