介绍

List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。

列表的最大长度为 2^32 - 1,也即每个列表支持超过 40 亿个元素。

内部实现

List类型的底层数据结构式由双向链表或压缩列表实现的:

  • 如果列表的元素小于512个(默认值,可由list-max-ziplist-entries配置),列表每个元素的值都小于64 字节(默认值,可由list-max-ziplist-value 配置),Redis会使用压缩列表作为list类型的底层数据结构;
  • 如果列表的元素不满足上面的条件,Redis会使用双向链表作为list类型的底层数据结构;

但是在Redis3.2版本之后,List数据类型底层数据结构就只有quicklist实现了,替代了双向链表和压缩列表。

常用命令

# 将一个或多个值value插入到key列表的表头(最左边),最后的值在最前面
LPUSH key value [value ...] 
# 将一个或多个值value插入到key列表的表尾(最右边)
RPUSH key value [value ...]
# 移除并返回key列表的头元素
LPOP key     
# 移除并返回key列表的尾元素
RPOP key 

# 返回列表key中指定区间内的元素,区间以偏移量start和stop指定,从0开始
LRANGE key start stop

# 从key列表表头弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BLPOP key [key ...] timeout
# 从key列表表尾弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BRPOP key [key ...] timeout

应用场景

消息队列

消息队列在存取消息是,必须满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性。

redis的list 和 stream两种数据类型,就可以满足消息队列的这三个需求。我们先来了解下基于List的消息队列实现方法,后面在介绍stream数据类型的时候,在详细说stream。

如何满足消息保序的需求?

List本身就是按照先进先出的顺序对数据进行存取的,所以,如果使用list作为消息队列保存消息的话,就已经满足消息保序的需求了。

list可以使用LPUSH+RPOP啊(或者反过来,RPUSH+LPOP)命令实现消息队列

  • 生产者使用LPUSH key value[value...] 将消息插入到队列的头部,如果key不存在则会创建一个空的队列在插入消息
  • 消费者使用RPOP key 依次读取队列消息,先进先出。

不过,在消费者读取数据时,有一个潜在的性能风险点。

再生产这往list写入数据时,list并不会主动通知消费者有新的消息写入,如果消费者想即时消费,就需要不停的调用RPOP 命令,如果有新的消息写入,RPOP 命令就会返回结果,否则,RPOP 命令返回空值,再继续循环。

所以,即使没有新的消息写入list,消费者也要不断调用RPOP 命令,这就会导致消费者程序的CPU一直消耗在执行RPOP 命令上,带来不必要的性能损失。

为了解决这个问题,redis提供了BRPOP 命令。BRPOP命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新的数据。和消费程序自己不停调用RPOP命令相比,这种方式节省CPU开销。

如何处理重复的消息?

消费者要实现重复消息的判断,需要 2 个方面的要求:

  • 每个消息都有一个全局ID
  • 消费者要记录处理过的消息ID。当收到一条消息后,消费者程序就可以对比收到的小弟ID和记录的已处理过的消息ID,来判断当前收到消息有没有已经处理过,那么,消费程序就不再进行处理了。

但是List并不会为每个消息生成ID号,所以我们需要自行为每个消息生成一个全局唯一ID,生成之后,我们在LPUSH 命令把消息插入list是,需要在消息中包含这个全局唯一ID。

例如,我们执行以下命令,就把全局ID为111000102、库存量为99的消息插入了消息队列

> LPUSH mq "111000102:stock:99"
(integer) 1

如何保证消息可靠性?

当消费者程序从 List 中读取一条消息后,List 就不会再留存这条消息了。所以,如果消费者程序在处理消息的过程出现了故障或宕机,就会导致消息没有处理完成,那么,消费者程序再次启动后,就没法再次从 List 中读取消息了。

为了留存消息,List 类型提供了 BRPOPLPUSH 命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存

这样一来,如果消费者程序读了消息但没能正常处理,等它重启后,就可以从备份 List 中重新读取消息并进行处理了。

好了,到这里可以知道基于 List 类型的消息队列,满足消息队列的三大需求(消息保序、处理重复的消息和保证消息可靠性)。

  • 消息保序:使用 LPUSH + RPOP;
  • 阻塞读取:使用 BRPOP;
  • 重复消息处理:生产者自行实现全局唯一 ID;
  • 消息的可靠性:使用 BRPOPLPUSH

List 作为消息队列有什么缺陷?

List 不支持多个消费者消费同一条消息,因为一旦消费者拉取一条消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费。

要实现一条消息可以被多个消费者消费,那么就要将多个消费者组成一个消费组,使得多个消费者可以消费同一条消息,但是 List 类型并不支持消费组的实现

这就要说起 Redis 从 5.0 版本开始提供的 Stream 数据类型了,Stream 同样能够满足消息队列的三大需求,而且它还支持「消费组」形式的消息读取。

Last modification:May 28, 2024
如果觉得我的文章对你有用,请收藏本站