列表
列表(list)类型是用来存储多个有序的字符串中,如下图,a、b、c、
d、e五个元素从左到右组成一个有序的列表,列表中的每个字符串称为
(element),一个列表最多可以存储2^32 - 1个元素。在Redis中,可以对
列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获
取指定索引下标的元素等。列表是一个钟比较灵活的数据结构,他可以充当栈
和队列的角色,在实际开发商有很多应用场景。
列表两端插入和弹出操作:
子列表获取、删除:
列表类型有两个特点:第一、列表中的元素是有序的,这就意味着可以通过索
引下标获取某个元素或者某个范围内的元素列表,例如要获取上图列表中第5个
元素,可以执行lindex user:1:message(索引从0算起)就可以得到元素e。
第二、列表中的元素可以是重复的。这个特点在后面介绍集合和有序集合后,
会显得更加突出,因此在考虑是否使用该数据结构前,首先需要弄清楚列表数
据结构的特点。
-
命令
操作类型 操作 添加 rpush lpush linsert 查 lrange lindex len 删除 lpop rpop lrem ltrim 修改 lset 阻塞操作 blpop brpop -
添加操作
(1)添加操作
rpush key value [value ...]
下面代码从右向左插入元素c、b、a:
127.0.0.1:6379> rpush listkey c b a (integer) 3
lrange 0 -1命令可以从左到右获取列表的所有元素:
127.0.0.1:6379> lrange listkey 0 -1 1) "c" 2) "b" 3) "a"
(2)从左边插入元素
lpush key value [value ...]
使用方法和rpush相同,只不过从左侧插入
(3)向某个元素前或者后插入元素
linsert key before|after pivot value
linsert命令会从列表中找到等于pivot的元素,在其前(before)
或者后(after)插入一个新的元素value,例如下面操作会在列表
的b前插入java:127.0.0.1:6379> linsert listkey before b java (integer) 4
返回结果为4,代表当前命令的长度,当前列表变为:
127.0.0.1:6379> lrange listkey 0 -1 1) "c" 2) "java" 3) "b" 4) "a"
-
查找
(1)获取指定范围内的元素列表
lrange key start end
lrange操作会获取猎豹知道你个索引范围所有的元素。索引下标有
两个特点:第一,索引下标从左到右分别是0到N-1,但是从右到左分
别是-1到-N。第二,lrange中的end选项包含了自身,这个和很多编
程语言不包含end不太相同,例如想获取列表的第2到第4个元素,可
以执行如下操作:127.0.0.1:6379> lrange listkey 1 3 1) "java" 2) "b" 3) "a"
(2)获取列表指定索引下标的元素
lindex key index
例如当前列表最后一个元素为a:
127.0.0.1:6379> lindex listkey -1 "a"
(3)获取列表长度
llen key
例如,下面示例当前列表长度为4:
127.0.0.1:6379> llen listkey (integer) 4
-
删除
(1)从列表左侧弹出元素
lpop key
如下操作将列表最左侧的元素c会被弹出,弹出后列表变为java、
b、a:127.0.0.1:6379> lpop listkey "c" 127.0.0.1:6379> lrange listkey 0 -1 1) "java" 2) "b" 3) "a"
(2)从列表右侧弹出
rpop key
它的使用方法和lpop是一样的,只不过从列表右侧弹出
(3)删除指定元素
lrem key count value
lrem命令会从列表中找到等于value的元素进行删除,根据count的
不同分为三种情况:count > 0,从左到右,删除最多count个元素。
count < 0,从右到左,删除最多count绝对值个元素。
count = 0,删除所有。
例如向列表从左向右插入5个a,那么当前列表变为“a a a a a
java b a”,下面操作将从列表左边开始删除4个为a的元素:127.0.0.1:6379> lrem listkey 4 a (integer) 4 127.0.0.1:6379> lrange listkey 0 -1 1) "a" 2) "java" 3) "b" 4) "a"
(4)按照索引范围修剪列表
ltrim key start end
例如,下面操作会只保留列表listkey第2个到第4个元素:
127.0.0.1:6379> ltrim listkey 1 3 OK 127.0.0.1:6379> lrange listkey 0 -1 1) "java" 2) "b" 3) "a"
-
修改
修改指定索引下标的元素:
lset key index newValue
下面操作会将列表listkey中的第3个元素设置为python:
127.0.0.1:6379> lset listkey 2 python OK 127.0.0.1:6379> lrange listkey 0 -1 1) "java" 2) "b" 3) "python"
-
阻塞操作
阻塞时弹出如下:
blpop key [key ...] timeout brpop key [key ...] timeout
blpop和brpop是lpop和rpop的阻塞版本,他们除了弹出方向不同,使用
方法基本相同,所以下面以brpop命令进行说明,btpop命令包含两
个参数:- key [key ...]:多个列表的键。
- timeout:阻塞时间(单位:秒)
1)列表为空:如果timeout=3,那么客户端要等3秒后返回,如果
timeout=0,那么客户端一直阻塞等下去:127.0.0.1:6379> brpop list:test 3 (nil) 127.0.0.1:6379> brpop list:test 0 ...阻塞...
如果此期间添加了数据element1,客户端立即返回:
127.0.0.1:6379> brpop list:test 3 1) "list:test" 2) "element1" (2.06s)
2)列表不为空:客户端会立即返回。
127.0.0.1:6379> brpop list:test 0 1) "list:test" 2) "element1"
在使用brpop时,有两点需要注意。
第一点,如果是多个建,那么brpop会从左至右遍历键,一旦有一个
键能弹出元素,客户端立即返回:127.0.0.1:6379> brpop list:1 list:2 list:3 0 ...阻塞...
此时另一个客户端分别向list:2和list:3插入元素:
client-lpush> lpush list:2 element2 (integer) 1 client-lpush> lpush list:3 element3 (integer) 1
客户端会立即返回list:2中的element2,因为list:2最先有可以弹
出的元素:127.0.0.1:6379> brpop list:1 list:2 list:3 0 1) "list:2" 2) "element2_1"
第二点,如果多个客户端对同一个键执行brpop,那么最先执行
brpop命令的客户端可以获取到弹出的值。客户端1:
client-1> brpop list:test 0 ...阻塞...
客户端2:
client-2> brpop list:test 0 ...阻塞...
客户端3:
client-3> brpop list:test 0 ...阻塞...
此时另一个客户端lpush一个元素到list:test列表中:
client-lpush> lpush list:test element (integer) 1
那么客户端1最会获取到元素,因为客户端1最先执行brpop,而客户
端2和客户端3继续阻塞:client> brpop list:test 0 1) "list:test" 2) "element"
下表是这些命令的时间复杂度
操作类型 命令 时间复杂度 添加 rpush key value [value ...] O(k),k是元素个数 添加 lpush key value [value ...] O(k),k是元素个数 添加 linsert key before after pivot value O(n),n是pivot距离 列表头或尾的距离
查找|lrange key start end|O(s+n),s是start偏移量,n是start到
end的范围
查找|lindex key index|O(n),n是索引的偏移量
查找|llen key|O(1)
删除|lpop key|O(1)
删除|rpop key|O(1)
删除|lremkey count value|O(n),n是列表长度
删除|ltrim key start end|O(n),n是要裁剪的元素总数
修改|lset key index value|O(n),n是索引的偏移量
阻塞操作|blpop brpop|O(1) -
-
内部编码
列表类型的内部编码有两种。
ziplist(压缩列表):当列表的元素个数小于
list-max-ziplist-entries配置(默认512个),同时列表中每个元素
的值小于list-max-ziplist-value配置时(默认64字节),Redis会选
用ziplist来作为列表的内部实现来减少内存的使用。linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis
会使用linkedlist作为列表的内部实现。
下面实例演示了列表类型的内部编码,以及相应的变化。
1)当元素个数较少且没有大元素时,内部编码为ziplist:
127.0.0.1:6379> rpush listkey e1 e2 e3\ (integer) 3 127.0.0.1:6379> object encoding listkey "ziplist"
2.1)当元素个数超过512个,内部编码变为linkedlist:
127.0.0.1:6379> rpush listkey e4 e5 ...忽略... e512 e513 (integer) 513 127.0.0.1:6379> object encoding listkey "linkedlist"
2.2)或者当某个元素超过64字节,内部编码也会变为linkedlist:
127.0.0.1:6379> rpush listkey "one string is bigger than 64 byte..." (integer) 4 127.0.0.1:6379> object encoding listkey "linkedlist"
-
使用场景
- 消息队列
Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用
lpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的
“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。- 文章列表
每个用户又属于自己的文章列表,先需要分页展示文章列表。此时可以
考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元
素。1)每篇文章使用哈希结构存储,例如每篇文章有3个属性title、
timestamp、content:hmset article:1 title xx timestamp 1476536196 content xxxx ... hmset article:k title yy timestamp 1476512536 content yyyy ...
2)向用户文章列表添加文章,user:{id}:articles作为用户文章列表
的键:lpush user:1:articles article:1 article:3 ... lpush user:k:articles article:5 ...
3)分页获取用户文章列表,例如下面伪代码获取用户id=1的前10篇文
章:articled = lrange user:1:articles 0 9 for article in {articles} hgetall {article}
使用列表类型保存和或文章列表会存在两个问题。第一,如果每次分页获
取的文章个数较多,需要制定多次hgetall操作,此时可以考虑Pipeline
批量获取,或者考虑将文章数据序列化为字符串类型,使用mget批量获
取。第二,分页获取文章列表时,lrange命令在列表两端性能较好,但是
如果列表较大,获取列表中间范围的元素性能变差,此时可以考虑将列表
做二级拆分,或者使用Redis3.2的quicklist内部编码实现,他结合
ziplist和linkedlist的特点,获取列表中间范围的元素时也可以高效完
成。实际上列表的使用场景很多,在选择时可以参考:
lpush + lpop = Stack (栈) lpush + rpop = Queue (队列) lpush + ltrim = Capped Collection (有限集合) lpush + brpop = Message Queue (消息队列)