Redis
的列表是链表而不是数组。这意味着 list
的插入和删除操作非常快,时间复杂度为 O(1)
,但是索引定位很慢,时间复杂度为 O(n)
。
当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
Redis
的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进 Redis
的列表,另一个线程从这个列表中轮询数据进行处理。
Redis
在列表元素较少的情况下会使用一块连续的内存来存储列表,这个结构是 ziplist
,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成 quicklist
。
因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。比如这个列表里存的只是 int
类型的数据,结构上还需要两个额外的指针 prev
和 next
。所以 Redis
将链表和 ziplist
结合起来组成了 quicklist
。也就是将多个 ziplist
使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
1. list 类型相关命令
命令 | 说明 |
---|---|
lpush key string | 在key对应list的头部添加字符串元素 |
rpop key | 在list的尾部删除元素,并返回删除元素 |
rpush key string | 在key对应list的尾部添加字符串元素 |
lpop key | 在list的头部删除元素,并返回删除元素 |
lset key index value | 将列表key下标为index的元素值设置为value |
llen key | 返回对应list的长度 |
lrange key start end | 返回指定区间内的元素,从下标 0 开始 |
ltrim key start end | 截取list, 保留指定区间内元素 |
lindex key 下标 | 获取列表下标对应的指定元素 |
blpop key[key…] time out | 删除,并获得该列的第一元素, 或阻塞,直到有一个可用 |
brpop key[key…] time out | 删除, 并获得该列的最后一个元素, 或阻塞,直到有一个可用 |
rpoplpush source destination | 删除列表中的最后一个元素,将其追加到另一个列表 |
brpoplpush source destination timeout | 弹出一个列表的值,将他推到另一个列表,并返回他,直到有一个可用 |
2. 使用示例
127.0.0.1:6379> lpush list 1
(integer) 1
127.0.0.1:6379> lpush list 2
(integer) 2
127.0.0.1:6379> lpush list 3
(integer) 3
127.0.0.1:6379> lpush list 4
(integer) 4
127.0.0.1:6379> lpush list 5
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
127.0.0.1:6379> lpop list
"5"
127.0.0.1:6379> rpop list
"1"
127.0.0.1:6379> lrange list 0 -1
1) "4"
2) "3"
3) "2"
127.0.0.1:6379> rpush list 1
(integer) 4
127.0.0.1:6379> rpush list -1
(integer) 5
127.0.0.1:6379> rpush list -2
(integer) 6
127.0.0.1:6379> lrange list 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
5) "-1"
6) "-2"
127.0.0.1:6379> lset list 0 00
OK
127.0.0.1:6379> lrange list 0 -1
1) "00"
2) "3"
3) "2"
4) "1"
5) "-1"
6) "-2"
127.0.0.1:6379> llen list
(integer) 6
lindex
需要对链表进行遍历,性能随着参数 index
增大而变差。
127.0.0.1:6379> lindex -2
(error) ERR wrong number of arguments for 'lindex' command
127.0.0.1:6379> lindex list -2
"-1"
127.0.0.1:6379> lindex list 0
"00"
ltrim
和字面上的含义不太一样,个人觉得它叫 lretain
(保留) 更合适一些,因为 ltrim
跟的两个参数start_index
和 end_index
定义了一个区间,在这个区间内的值,ltrim
要保留,区间之外统统砍掉。我们可以通过 ltrim
来实现一个定长的链表,这一点非常有用。
index
可以为负数,index=-1
表示倒数第一个元素,同样 index=-2
表示倒数第二个元素。
127.0.0.1:6379> ltrim list 1 4
OK
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "2"
3) "1"
4) "-1"
127.0.0.1:6379> blpop list 3
1) "list"
2) "3"
127.0.0.1:6379> brpop list 2
1) "list"
2) "-1"
127.0.0.1:6379>
3. 消息队列
Redis
的 list
(列表) 数据结构常用来作为异步消息队列使用,使用 rpush/lpush
操作入队列,使用 lpop
和 rpop
来出队列。
lpush
和 rpop
命令可以实现队列功能,只需要生产者将任务使用 lpush
命令加入到某个键中,而消费者使用 rpop
命令将任务从对应的键中取出来即可。
brpop
命令和 rpop
命令相似,区别在于 brpop
是阻塞式的,当队列中没有任务时,brpop
命令会一直阻塞住连接,直到队列中有任务。
brpop
命令格式
brpop key seconds
seconds
为 0 表示不限制等待时间,即永远阻塞下去。
3.1 队列空情况
客户端是通过队列的 pop
操作来获取消息,然后进行处理。处理完了再接着获取消息,再进行处理。如此循环往复,这便是作为队列消费者的客户端的生命周期。
如果队列空了,客户端就会陷入 pop
的死循环,不停地 pop
,没有数据,接着再 pop
,又没有数据。这就是浪费生命的空轮询。空轮询不但拉高了客户端的 CPU
,Redis
的 QPS
也会被拉高。
通常我们使用 sleep
来解决这个问题,让线程睡一会,睡个 1s 钟就可以了。不但客户端的 CPU
能降下来,Redis
的 QPS
也降下来了。
time.sleep(1) # python 睡 1s
3.2 阻塞队列
用上面睡眠的办法可以解决问题。但是有个小问题,那就是睡眠会导致消息的延迟增大。更好的解决方法是使用 blpop/brpop
,这两个指令的前缀字符 b
代表的是 blocking
,也就是阻塞读。
阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消息的延迟几乎为零。用 blpop/brpop
替代前面的 lpop/rpop
可以完美解决队列延迟问题。
3.3 空闲连接自动断开
如果线程一直阻塞在哪里,Redis
的客户端连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用。这个时候 blpop/brpop
会抛出异常来。
所以编写客户端消费者的时候要小心,注意捕获异常,还要重试。
代码参考:
玩转 Redis:简单消息队列