Lists

介绍

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

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

List 列表的底层实现是一个双向链表,因此在头部和尾部添加元素的时间复杂度为 O(1)。

如果列表的元素个数小于 512(默认值,可通过 list-max-ziplist-entries 配置项修改),并且列表中的每个元素的长度都小于 64 字节(默认值,可通过 list-max-ziplist-value 配置项修改),列表会使用压缩列表作为底层实现。

如果列表的元素个数大于 512,或者列表中的某个元素的长度大于 64 字节,列表会使用双向链表作为底层实现。

在 Redis 3.2 之后,Redis 为 List 列表添加了快速列表(quicklist)的实现,快速列表是一种新的列表实现方式,它同时兼具压缩列表和双向链表的优点,可以在列表元素个数较少时使用压缩列表,元素个数较多时使用双向链表,从而兼顾了两者的优点。

Drawing

常用命令

  • LPUSH key value [value ...]: 将一个或多个值插入到列表头部

  • RPUSH key value [value ...]: 将一个或多个值插入到列表尾部

  • LPOP key: 移除并返回列表的第一个元素

  • RPOP key: 移除并返回列表的最后一个元素

  • LINDEX key index: 通过索引获取列表中的元素

  • LLEN key: 获取列表的长度

  • LRANGE key start stop: 获取列表指定范围内的元素

  • LSET key index value: 通过索引设置列表元素的值

  • LTRIM key start stop: 对列表进行修剪

  • LINSERT key BEFORE|AFTER pivot value: 在列表中指定元素的前或后插入元素

  • LREM key count value: 移除列表元素

  • RPOPLPUSH source destination: 移除列表的最后一个元素,并将该元素添加到另一个列表并返回

  • BRPOP key [key ...] timeout: 移除并获取列表最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止

  • BLPOP key [key ...] timeout: 移除并获取列表第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止

  • RPOPLPUSH source destination: 移除列表的最后一个元素,并将该元素添加到另一个列表并返回

  • BRPOPLPUSH source destination timeout: 移除列表的最后一个元素,并将该元素添加到另一个列表并返回,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止

应用场景

  • 队列:先进先出

  • 微信公众号订阅:例如当发布者发了一条新的微信公众号文章时,订阅者会收到一条消息,当发布者发布了一条新的微信公众号文章时,订阅者会收到一条消息。

  • 消息队列

Redis 的 List 和 Stream 数据结构都可以用作消息队列,List 列表是最简单的消息队列实现方式,Stream 数据结构是 Redis 5.0 新增的数据结构,提供了更多的功能,例如消息的消费确认、消息的消费者组等。

List 列表可以用作消息队列,通过 LPUSHRPOP 命令实现消息的生产和消费。

生产者通过 LPUSH 命令将消息插入到列表头部,消费者通过 RPOP 命令从列表尾部获取消息。

在生产者往 List 中写入数据时,List 并不会主动通知消费者,消费者需要主动轮询 List 列表,检查是否有新的消息。这就会导致消费者需要不断地轮询 List 列表,这会导致消费者程序的 CPU 一直消耗在执行 RPOP 命令上,带来不必要的性能开销。

为了解决这个问题,可以使用阻塞式的命令 BLPOPBRPOP,这两个命令会在列表为空时阻塞,直到列表中有新的元素时才会返回。

如何保证消息可靠性

当消费者程序从 List 列表中读取一条消息后,List 就不会再保存这条消息了,如果消费者程序在处理消息的过程中出现异常,消息就会丢失。为了保证消息不丢失,List 类型提供了 BRPOPLPUSH 命令,该命令可以将消息从一个列表移动到另一个列表,这样即使消费者程序在处理消息的过程中出现异常,消息也不会丢失。

在消费者程序处理完消息后,可以通过 LREM 命令将消息从 processing-queue 列表中移除。

如何处理消息重复消费

在消息队列中,消息可能会被重复消费,为了避免消息重复消费,可以在消费者程序处理消息之前,先检查消息是否已经被处理过。例如,可以为每条消息添加一个唯一的 ID,然后在消费者程序处理消息之前,先检查消息的 ID 是否已经处理过。

但是 List 并不会为每个消息声称一个唯一的 ID,因此需要在消息中添加一个唯一的 ID,例如 UUID,然后在消费者程序处理消息之前,先检查消息的 ID 是否已经处理过。

例如,下方命令就把一条全局 ID 为 10000111 、库存量为 1001 的消息插入到 message-queue 队列中。

List 作为消息队列的优点是实现简单,易于理解,但是在高并发场景下,List 的性能可能会成为瓶颈,因为 List 是一个全局的数据结构,所有的生产者和消费者都需要访问同一个 List 列表,这会导致 List 列表的读写压力较大。同时,List 不支持多个消费者消费同一条消息,因为一旦消息被一个消费者消费,这条消息就会从 List 列表中移除,其他消费者就无法再次消费这条消息。

最后更新于

这有帮助吗?