type
status
date
slug
summary
tags
category
icon
password
Property
列表对象的编码可以是
ziplist
或者 linkedlist
。ziplist
编码的列表对象使用压缩列表作为底层实现, 每个压缩列表节点保存了一个列表元素。如果执行以下
RPUSH
命令, 那么服务器将创建一个列表对象作为 numbers
键的值:如果
numbers
键的值对象使用的是 ziplist
编码, 这个这个值对象:另一方面,
linkedlist
编码的列表对象使用双端链表作为底层实现, 每个双端链表节点都保存了一个字符串对象, 而每个字符串对象都保存了一个列表元素。如果前面的
numbers
键创建的列表对象使用的不是ziplist
编码, 而是linkedlist
编码, 那么numbers
键的值对象:linkedlist
编码的列表对象在底层的双端链表结构中包含了多个字符串对象, 字符串对象是Redis
五种类型的对象中唯一一种会被其他四种类型对象嵌套的对象。为了简化字符串对象的表示,上图用了一个带有
StringObject
字样的格子来表示一个字符串对象, 而 StringObject
字样下面的是字符串对象所保存的值。编码转换
当列表对象可以同时满足以下两个条件时, 列表对象使用
ziplist
编码:- 列表对象保存的所有字符串元素的长度都小于
64
字节
- 列表对象保存的元素数量小于
512
个
不能满足这两个条件的列表对象需要使用
linkedlist
编码。但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由quicklist实现了,替代了双向链表和压缩列表。
两个条件的上限值是可以修改的, 具体请看配置文件中关于
list-max-ziplist-value
选项和 list-max-ziplist-entries
选项的说明。对于使用
ziplist
编码的列表对象来说, 当使用 ziplist
编码所需的两个条件的任意一个不能被满足时, 对象的编码转换操作就会被执行: 原本保存在压缩列表里的所有列表元素都会被转移并保存到双端链表里面, 对象的编码也会从 ziplist
变为 linkedlist
。Redis 列表命令
- BLPOP:移出并获取列表的第一个元素
- BRPOP:移出并获取列表的最后一个元素
- BRPOPLPUSH:从列表中弹出一个值,并将该值插入到另外一个列表中并返回它
- LINDEX:通过索引获取列表中的元素
- LINSERT:在列表的元素前或者后插入元素
- LLEN:获取列表长度
- LPOP:移出并获取列表的第一个元素
- LPUSH:将一个或多个值插入到列表头部
- LPUSHX:将一个值插入到已存在的列表头部
- LRANGE:获取列表指定范围内的元素
- LREM:移除列表元素
- LSET:通过索引设置列表元素的值
- LTRIM:对一个列表进行修剪(trim)
- RPOP:移除并获取列表最后一个元素
- RPOPLPUSH:移除列表的最后一个元素,并将该元素添加到另一个列表并返回
- RPUSH:在列表中添加一个或多个值
- RPUSHX:为已存在的列表添加值
应用场景
消息队列
消息队列在存取消息时,必须要满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性。
Redis的List和Stream两种数据类型,就可以满足消息队列的这三个需求。
如何满足消息保序需求?
List 本身就是按先进先出的顺序对数据进行存取的,所以,如果使用 List 作为消息队列保存消息的话,就已经能满足消息保序的需求了。
List 可以使用 LPUSH + RPOP (或者反过来,RPUSH+LPOP)命令实现消息队列。
- 生产者使用
LPUSH key value[value...]
将消息插入到队列的头部,如果 key 不存在则会创建一个空的队列再插入消息。
- 消费者使用
RPOP key
依次读取队列的消息,先进先出。
不过,在消费者读取数据时,有一个潜在的性能风险点
在生产者往 List 中写入数据时,List 并不会主动地通知消费者有新消息写入,如果消费者想要及时处理消息,就需要在程序中不停地调用
RPOP
命令(比如使用一个while(1)循环)。如果有新消息写入,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 的消息插入了消息队列:
如何保证消息可靠性?
当消费者程序从 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 同样能够满足消息队列的三大需求,而且它还支持「消费组」形式的消息读取。