介绍
List是一个简单的字符串列表,按照元素的插入顺序进行排序。您可以从头部或尾部添加元素到这个列表中。
列表的最大长度为2^32 - 1,即支持多达40亿个元素。
内部实现
List 类型的底层数据结构在 Redis 中可以采用双向链表或压缩列表(ziplist),具体使用哪种结构取决于列表的元素数量和每个元素的大小:
-
如果List中的元素数量少于 512 个(默认值,可通过配置 list-max-ziplist-entries 调整),且每个元素的值小于 64 字节(默认值,可通过配置 list-max-ziplist-value 调整),Redis 会使用压缩列表作为底层数据结构。这种结构有助于节省内存。
-
如果List的元素数量超过上述限制或单个元素的大小超过了规定的字节数,Redis 会改用双向链表作为底层数据结构,以支持更复杂的操作。
不过从 Redis 3.2 版本开始,List 数据类型的底层数据结构被统一为 quicklist,这种结构结合了压缩列表和双向链表的优点,取代了原有的双向链表和压缩列表。
常用命令
以下是Redis List类型的一些常用命令:
#将一个或多个值从List左边插入。
LPUSH key value [value...]#将一个或多个值从List右边插入。
RPUSH key value [value...]#删除并返回List左边的元素。
LPOP key#删除并返回List右边的元素。
RPOP key# 获取List中元素的个数。
LLEN key#获取List指定范围内的元素。
LRANGE key start stop#按指定数量移除List中的元素
LREM key count value#设置List中指定索引处的元素。
LSET key index value#修剪List
LTRIM key start stop:
示例:
> lpush mq 10001:stock:9
(integer) 1
> lpush list v1
(integer) 1
> rpush list v2
(integer) 2
> rpush list v3
(integer) 3
> llen list
(integer) 3
> lrange list 0 2
1) "v1"
2) "v2"
3) "v3"
> lpop list
"v1"
> llen list
(integer) 2
应用场景
1. 消息队列
消息队列在处理和存储消息时,必须满足以下三个要求:
- 保证消息顺序
- 处理重复消息
- 确保消息的可靠性。
Redis 的两种数据类型——List 和 Stream都可以用来实现消息队列的功能。
首先,我们将探讨如何使用 List 实现消息队列的功能,后面在介绍 Stream 数据类型时,我们将详细讨论 Stream 的实现方式。
1. 如何满足消息有序性
List本身是按照先进先出的顺序访问和存储数据的,所以如果使用List作为消息队列来保存消息,已经可以满足消息有序性的要求了。
List可以使用LPUSH+RPOP(或反之,RPUSH+LPOP)命令实现消息队列。
- 生产者:使用命令LPUSH key value[value…]将消息插入到List头部,如果key不存在,则会创建一个空List然后插入消息。
- 消费者:使用命令RPOP key按顺序读取List的消息,先进先出。
然而,这种方法存在一个问题:当生产者往 List 中写入数据时,Redis 不会主动通知消费者有新数据到达。
为了让消费者能够及时处理消息,通常需要在程序中不断调用 RPOP 命令,比如通过 while(1) 循环来轮询队列。如果队列中有新数据,RPOP 命令会返回该消息;如果队列为空,RPOP 命令将返回空值,消费者则会继续循环等待新的数据。
因此,即使没有新的消息写入 List,消费者也需要不断调用 RPOP 命令,这会导致消费者程序的 CPU 不断消耗在执行 RPOP 命令上,带来不必要的性能损失。
为了解决这个问题,Redis 提供了 BRPOP 命令。BRPOP 命令是一个阻塞读取命令,当客户端没有读取到队列数据时,BRPOP 会自动阻塞,直到有新数据写入队列后才会继续执行。与消费者程序自己不断调用 RPOP 命令不同,BRPOP 能有效节省 CPU 开销,因为它只在有新数据时才会返回结果。
2. 如何处理重复消息?
消费者要实现重复消息的判断,需要满足两个要求:
- 每条消息都应具有一个全局唯一的 ID。
- 消费者需要记录已经处理过的消息 ID。在收到一条新消息后,消费者程序可以通过比较消息的 ID 与已记录的 ID 来判断消息是否已经处理过,如果消息已处理过,则跳过处理。
List 本身并不为每条消息生成 ID,因此我们需要手动为每条消息生成一个全局唯一的 ID。在生成 ID 后,当我们使用 LPUSH 命令将消息插入 List 时,消息内容中需要包含这个唯一的 ID。
例如,我们可以执行以下命令,将一条全局唯一 ID 为 10001、库存数量为 9 的消息插入到消息队列中:
> LPUSH mq "10001:stock:9"
(integer) 1
3. 如何保证消息的可靠性?
当消费者程序从 List 中读取一条消息后,该消息将从 List 中移除。如果消费者在处理这条消息时发生故障或崩溃,那么这条消息就会丢失,消费者在重启后将无法再次从 List 中读取到这条消息。
为了解决这个问题,List 类型提供了 BRPOPLPUSH 命令。这个命令允许消费者程序从一个 List 中读取一条消息,同时将这条消息插入到另一个 List(可以称为备份 List)中进行保留。这样,如果消费者在处理消息时出现问题,消息将保留在备份 List 中。在消费者程序重启后,可以从备份 List 中重新读取并处理这些消息。
通过这种方式,基于 List 类型的消息队列能够满足三大需求:消息有序、处理重复消息和保证消息的可靠性。
- 消息有序:使用 LPUSH + RPOP。
- 阻塞读:使用 BRPOP。
- 重复消息处理:生产者自行实现全局唯一 ID。
- 消息可靠性:使用 BRPOPLPUSH。
List作为消息队列有什么缺陷?
不支持多个消费者消费同一条消息:一旦一个消费者拉取了某条消息,这条消息就会从 List 中删除,其他消费者将无法再读取这条消息。
为了解决这一问题,Redis 从 5.0 版本开始引入了 Stream 数据类型。Stream 不仅可以满足消息队列的三大需求,还支持以“消费者组”的形式读取消息。有关 Stream 数据类型的更多信息,我们将在后续的文章中详细介绍。