Redis
的列表是链表而不是数组。这意味着list
的插入和删除操作非常快,时间复杂度为O(1)
,但是索引定位很慢,时间复杂度为O(n)
。
当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
Redis
的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进Redis
的列表,另一个线程从这个列表中轮询数据进行处理。
Redis
在列表元素较少的情况下会使用一块连续的内存来存储列表,这个结构是ziplist
,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist
。
因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。比如这个列表里存的只是int
类型的数据,结构上还需要两个额外的指针prev
和next
。所以Redis
将链表和ziplist
结合起来组成了quicklist
。也就是将多个ziplist
使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
1. list 类型相关命令
2. 使用示例
127.0.0.1:6379> lpush list 1(integer) 1127.0.0.1:6379> lpush list 2(integer) 2127.0.0.1:6379> lpush list 3(integer) 3127.0.0.1:6379> lpush list 4(integer) 4127.0.0.1:6379> lpush list 5(integer) 5127.0.0.1:6379> lrange list 0 -11) "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 -11) "4"2) "3"3) "2"
127.0.0.1:6379> rpush list 1(integer) 4127.0.0.1:6379> rpush list -1(integer) 5127.0.0.1:6379> rpush list -2(integer) 6127.0.0.1:6379> lrange list 0 -11) "4"2) "3"3) "2"4) "1"5) "-1"6) "-2"127.0.0.1:6379> lset list 0 00OK127.0.0.1:6379> lrange list 0 -11) "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' command127.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 4OK127.0.0.1:6379> lrange list 0 -11) "3"2) "2"3) "1"4) "-1"127.0.0.1:6379> blpop list 31) "list"2) "3"127.0.0.1:6379> brpop list 21) "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:简单消息队列
玩转 Redis — 延时消息队列
Redis 笔记(04)— list类型(作为消息队列使用 在列表头部添加元素 尾部删除元素 查看列表长度 遍历指定列表区间元素 获取指定区间列表元素 阻塞式获取列表元素)