列表类型是用来存储多个有序的字符串,如图所示,a、b、c、d、e 五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素 (element),一个列表最多可以存储2的32次方 -1个元素。在 Redis 中,可以对列表两端插入(push)和弹出 (pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。
列表两端插入和弹出操作
列表的获取、删除等操作
列表类型的特点:
- 列表中的元素是有序的,这意味着可以通过索引下标获取某个元素或者某个范围的元素列表,例如要获取下图的第5个元素,可以执行lindex user:1:messages 4 或者倒数第1个元素,lindexuser:1:messages-1就可以得到元素e。
- 区分获取和删除的区别,例如图中的rem 1b 是从列表中把从左数遇到的前1个b元素删除,这个操作会导致列表的长度从5变成4;但是执行 index4 只会获取元素,但列表长度是不会变化的。
- 列表中的元素是允许重复的,例如图中的列表中是包含了两个a元素的。
列表中允许有重复元素
命令
lpush
将一个或多个值插入到 Redis 列表的头部。如果 key 不存在,一个空列表会被创建并执行 LPUSH
操作。当 key 存在但不是列表类型时,返回一个错误。
语法:
LPUSH key value1 [value2 ...]
key
是列表的名字。value1
是要插入的值。可以同时指定多个值,Redis 会从左到右的顺序将它们插入到列表的头部。
时间复杂度:只插入一个元素为 O(1),插入多个元素为 O(N),N 为插入元素个数
返回值:返回列表的长度。
示例:
127.0.0.1:6379> lpush mylist "world"
(integer) 1
127.0.0.1:6379> lpush mylist "hello"
(integer) 2
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "world"
lpushx
在 key 存在时,将一个或者多个元素从左侧放入(头插)到 list 中。不存在,直接返回
语法:
LPUSHX key value1 [value2 ...]
时间复杂度:只插入一个元素为 O(1),插入多个元素为 O(N),N 为插入元素个数
返回值:插入后 list 的长度。
示例:
127.0.0.1:6379> lpush mylist "World"
(integer) 1
127.0.0.1:6379> lpushx mylist "Hello"
(integer) 2
127.0.0.1:6379> lpushx myotherlist "Hello"
(integer) 0
127.0.0.1:6379> lrange mylist 0 -1
1) "Hello"
2) "World"
127.0.0.1:6379> lrange myotherlist 0 -1
(empty array)
rpush
将一个或者多个元素从右侧放入(尾插)到 list 中。
语法:
RPUSH key value [value ...]
时间复杂度:只插入一个元素为 O(1),插入多个元素为 O(N),N 为插入元素个数
返回值:插入后 list的长度
示例:
127.0.0.1:6379> rpush mylist "world"
(integer) 1
127.0.0.1:6379> rpush mylist "hello"
(integer) 2
127.0.0.1:6379> lrange mylist 0 -1
1) "world"
2) "hello"
rpushx
在 key 存在时,将一个或者多个元素从右侧放入(尾插)到list 中。
语法:
RPUSHX key value1 [value2 ...]
时间复杂度:只插入一个元素为 O(1),插入多个元素为 O(N),N 为插入元素个数
返回值:插入后 list 的长度。
示例:
127.0.0.1:6379> rpush mylist "World"
(integer) 1
127.0.0.1:6379> rpushx mylist "Hello"
(integer) 2
127.0.0.1:6379> rpushx myotherlist "Hello"
(integer) 0
127.0.0.1:6379> lrange mylist 0 -1
1) "World"
2) "Hello"
127.0.0.1:6379> lrange myotherlist 0 -1
(empty array)
lrange
获取从 start 到 end 区间的所有元素,左闭右闭。
语法:
LRANGE key start stop
时间复杂度:O(N)
返回值:指定区间的元素。
示例:
127.0.0.1:6379> rpush mylist "one"
(integer) 1
127.0.0.1:6379> rpush mylist "two"
(integer) 2
127.0.0.1:6379> rpush mylist "three"
(integer) 3
127.0.0.1:6379> lrange mylist 0 0
1) "one"
127.0.0.1:6379> lrange mylist -3 2
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> lrange mylist -100 100
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> lrange mylist 5 10
(empty array)
lpop
从 list 左侧取出元素(即头删)
语法:
LPOP key
时间复杂度:O(1)
返回值:取出的元素或者 nil。
示例:
127.0.0.1:6379> rpush mylist "one" "two" "three" "four" "five"
(integer) 5
127.0.0.1:6379> lpop mylist
"one"
127.0.0.1:6379> lpop mylist
"two"
127.0.0.1:6379> lpop mylist
"three"
127.0.0.1:6379> lrange mylist 0 -1
1) "four"
2) "five"
rpop
从 list 右侧取出元素 (即尾删)
语法:
RPOP key
时间复杂度:O(1)
返回值:取出的元素或者 nil。
示例:
127.0.0.1:6379> rpush mylist "one" "two" "three" "four" "five"
(integer) 5
127.0.0.1:6379> rpop mylist
"five"
127.0.0.1:6379> lrange mylist 0 -1
1) "one"
2) "two"
3) "three"
4) "four"
lindex
获取从左数第 index 位置的元素。
语法:
LINDEX key index
时间复杂度:O(N)
返回值:取出的元素或者 nil。
示例:
127.0.0.1:6379> lpush mylist "World"
(integer) 1
127.0.0.1:6379> lpush mylist "Hello"
(integer) 2
127.0.0.1:6379> lindex mylist 0
"Hello"
127.0.0.1:6379> lindex mylist -1
"World"
127.0.0.1:6379> lindex mylist 3
(nil)
linsert
在特定位置插入元素。
语法:
LINSERT key BEFORE|AFTER pivot value
这里:
key
是列表的键名。BEFORE
或AFTER
是插入位置选项,用于指定新值是插在参考值的前面还是后面。pivot
是列表中的现有值,新值会插入到这个值的前面或后面。value
是要插入的新值。
时间复杂度:O(N)
返回值:插入后的 list 长度。
示例:
127.0.0.1:6379> rpush mylist "Hello"
(integer) 1
127.0.0.1:6379> rpush mylist "World"
(integer) 2
127.0.0.1:6379> linsert mylist before "World" "There"
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "Hello"
2) "There"
3) "World"
llen
获取 list 长度
语法:
LLEN key
时间复杂度:O(1)
返回值:list 的长度。
示例:
127.0.0.1:6379> lpush mylist "World"
(integer) 1
127.0.0.1:6379> lpush mylist "Hello"
(integer) 2
127.0.0.1:6379> llen mylist
(integer) 2
阻塞版本命令
blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,和对应非阻塞版本的作用基本⼀致,除了:
- 在列表中有元素的情况下,阻塞和非阻塞表现是一致的。但如果列表中没有元素,非阻塞版本会理解返回nil,但阻塞版本会根据timeout,阻塞一段时间,期间 Redis 可以执行其他命令,但要求执行该命令的客户端会表现为阻塞状态。
- 命令中如果设置了多个键,那么会从左向右进行遍历键,一旦有一个键对应的列表中可以弹出元素,命令立即返回。
- 如果多个客户端同时多一个键执行 pop,则最先执行命令的客户端会得到弹出的元素。
阻塞版本的 blpop 和 非阻塞版本 lpop 的区别
blpop
语法:
BLPOP key [key ...] timeout
时间复杂度:O(1)
返回值:取出的元素或者 nil。
示例:
127.0.0.1:6379> blpop list1 0
1) "list1"
2) "a"
127.0.0.1:6379> blpop list1 60
1) "list1"
2) "b"
127.0.0.1:6379> blpop list1 60
1) "list1"
2) "c"
#开启另一个终端
127.0.0.1:6379> lpush list1 d
(integer) 1
#原来终端
127.0.0.1:6379> blpop list1 60
1) "list1"
2) "d"
(14.63s)
brpop
语法:
BRPOP key [key ...] timeout
时间复杂度:O(1)
返回值:取出的元素或者 nil。
示例:
127.0.0.1:6379> del list1
(integer) 0
127.0.0.1:6379> rpush list1 a b c
(integer) 3
127.0.0.1:6379> brpop list1 60
1) "list1"
2) "c"
127.0.0.1:6379> brpop list1 60
1) "list1"
2) "b"
127.0.0.1:6379> brpop list1 60
1) "list1"
2) "a"
#另一个终端运行
127.0.0.1:6379> lpush list1 d
(integer) 1
#原来终端
127.0.0.1:6379> brpop list1 60
1) "list1"
2) "d"
(7.83s)
内部编码
list 类型的内部编码有两种:
- 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)当元素个数超过 512 时,内部编码为 linkedlist:
127.0.0.1:6379> rpush listkey e1 e2 e3 ... 省略 e512 e513
OK
127.0.0.1:6379> object encoding listkey
"linkedlist"
3)当某个元素的长度超过 64 字节时,内部编码为 linkedlist:
127.0.0.1:6379> rpush listkey "one string is bigger than 64 bytes ... 省略 ..."
OK
127.0.0.1:6379> object encoding listkey
"linkedlist"
使用场景
消息队列
Redis 可以使用 lpush + brpop 命令组合实现经典的阻塞式生产者-消费者模型队列, 生产者客户端使用 lpush 从列表左侧插⼊元素,多个消费者客户端使用 brpop 命令阻塞式地从队列中 “争抢” 队首元素。通过多个客户端来保证消费的负载均衡和高可用性。
Redis 阻塞消息队列模型:
分频道的消息队列
Redis 同样使用 lpush + brpop 命令,但通过不同的键模拟频道的概念,不同的消费者可以通过 brpop 不同的键值,实现订阅不同频道的理念。
Redis 分频道阻塞消息队列模型:
微博 Timeline
每个用户都有属于自己的 Timeline (微博列表),现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
1)每篇微博使用哈希结构存储,例如微博中 3 个属性:title、timestamp、content:
hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx
2)向用户 Timeline 添加微博,user::mblogs 作为微博的键:
lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9
3)分页获取用户的 Timeline,例如获取用户 1 的前 10 篇微博:
keylist = lrange user:1:mblogs 0 9
for key in keylist {
hgetall key
}
此方案在实际中可能存在两个问题:
- 1+n 问题。即如果每次分页获取的微博个数较多,需要执行多次 hgetall 操作,此时可以考虑使用 pipeline (流水线)模式批量提交命令,或者微博不采用哈希类型,而是使用序列化的字符串类型,使用 mget 获取。
- 分裂获取文章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表做拆分。
选择列表类型时,请参考:
同侧存取(lpush + lpop 或者 rpush + rpop)为栈
异侧存取(lpush + rpop 或者 rpush + lpop)为队列