第六章:进入Redis的List核心
一.List概念
列表类型是⽤来存储多个有序的字符串,如图 2-19 所示,a、b、c、d、e 五个元素从左到右组成
了⼀个有序的列表,列表中的每个字符串称为元素(element),⼀个列表最多可以存储
个元素。在 Redis 中,可以对列表两端插⼊(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等(如下图所示。列表是⼀种比较灵活的数据结构,它可以 当栈和队列的角色,在实际开发上有很多应用场景
列表两端插⼊和弹出操作
列表的获取、删除等操作
列表类型的特点:
- 列表中的元素是有序的,这意味着可以通过索引下标获取某个元素或者某个范围的元素列表
- 区分获取和删除的区别
- 列表中的元素是允许重复的
- 列表(List)相当于数组或者顺序表。
- 注意:List 内部的结构和访问方式并非是一个简单的数组,而是更接近于 “双端队列”(deque)。
- 的边最左的元素下标是 0,Redis 的下标支持负下标,getrange。
- 区分获取和删除的区别:
- Index 能获取到元素的值。
- lrem 也能返回被删除元素的值。
- 像 hash 这样的类型,field 是不能重复的。
- 因为当前的 List,头和尾都能高效的插入删除元素,所以可以把这个 List 当做一个栈 / 队列来使用了。
- Redis 有一个典型的应用场景,就是作为消息队列,最早的时候,就是通过 List 实现。
- 后来 Redis 又提供了一个 stream 类型。
二List核心命令
2.1LPUSH,LPRUSHX
LPUSH命令:将⼀个或者多个元素从左侧放入(头插)到 list 中。
LPUSH key element [element ...]
时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.
返回值:插⼊后 list 的⻓度
LPRUSH命令:在 key 存在时,将⼀个或者多个元素从左侧放入(头插)到 list 中。不存在,直接返回
LPUSHX key element [element ...]
时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.
返回值:插⼊后 list 的⻓度
如何搭配使用?
- Redis 中的 list 是一个双端队列。
- 从两头插入 / 删除元素都是非常高效 O (1)。
- 搭配使用 rpush 和 lpop,就相当于队列了。
- 搭配使用 rpush 和 rpop,就相当于栈了。
2.2LPOP
- LPOP key
- RPOP key [count]
- LPOP 和 RPOP 在当前的 redis 5 版本中,都是没有 count 参数。
- 从 redis 6.2 版本,新增了一个 count 参数。
- count 就描述这次要删几个元素。
LPOP 命令:从 list 左侧取出元素(即头删)
LPOP key
时间复杂度:O(1)
返回值:取出的元素或者 nil。
2.3LRANGE
Lrange 命令:查看 list 中指定范围的元素。
此处描述的区间也是闭区间,下标支持负数。
- 谈到下标,往往会关注超出范围的情况:
- C++ 中,下标超出范围,一般会认为这是一个 “未定义行为”。
- 缺点:程序不一定能第一时间发现问题!!
- 优点:效率是最高的!!
- 可能会导致程序崩溃。
- 也可能会得到一个不合法的数据。
- 还可能会得到一个看起来对,但是错误的数据。
- 也有可能恰好得到一个符合要求的数据。[开盲盒]
- Java 中,下标超出范围,一般会抛出异常。多出一步下标合法性的验证。
- 缺点:速度比上面要慢。
- 优点:出现问题能及时发现。
- 在 Redis 中并没有采用上述的两种设定。
- Redis 的做法,是直接尽可能的获取到给定区间的元素,如果给定区间非法,比如超出下标,就会尽可能的获取对应的内容。
- 此处对于越界下标的处理方式,更接近于 python 的处理方式。(python 的切片)
- C++ 中,下标超出范围,一般会认为这是一个 “未定义行为”。
- “鲁棒性”(你对我越粗鲁,我就表现的越棒),程序的容错能力更强。
- 是机器执行的快重要?还是程序代码好写的快重要?利益相关。
LRANGE命令:获取从 start 到 end 区间的所有元素,左闭右闭
LRANGE key start stop
时间复杂度:O(N)
返回值:指定区间的元素。
2.4RPUSH,RPUSHX
RPUSH命令:将⼀个或者多个元素从右侧放⼊(尾插)到 list 中
RPUSH key element [element ...]
时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.
返回值:插⼊后 list 的⻓度
RPUSHX命令:在 key 存在时,将⼀个或者多个元素从右侧放⼊(尾插)到 list 中
RPUSHX key element [element ...]
时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.
返回值:插⼊后 list 的⻓度
2.5RPOP
- LPOP key
- RPOP key [count]
- LPOP 和 RPOP 在当前的 redis 5 版本中,都是没有 count 参数。
- 从 redis 6.2 版本,新增了一个 count 参数。
- count 就描述这次要删几个元素。
RPOP命令:从 list 右侧取出元素(即尾删)。
RPOP key
时间复杂度:O(1)
返回值:取出的元素或者 nil。
2.6LINDEX ,LINSERT ,LLEN
LINDEX 命令:获取从左数第 index 位置的元素
LINDEX key index
时间复杂度:O(N)
返回值:取出的元素或者 nil。
(注意插入有多个基准值后端情况是怎么样的)
LINSERT 命令:在特定位置插⼊元素。
- 返回值是插入之后,得到的新的 list 的长度。
- 万一要插入的列表中,基准值,存在多个,咋办?
- linsert 进行插入的时候,要根据基准值,找到对应的位置,从左往右找,找到第一个符合基准值的位置即可。
- O (N),N 表示列表的长度。
LINSERT key <BEFORE | AFTER> pivot element
LLEN 命令:获取 list ⻓度。
LLEN key
时间复杂度:O(1)
返回值:list 的长度
2.7LREM
当 count > 0 时
- 规则:从列表的头部开始向尾部查找,删除最多
count
个值等于value
的元素。
当 count < 0 时
- 规则:从列表的尾部开始向头部查找,删除最多
Math.abs(count)
个值等于value
的元素。
当 count = 0 时
- 规则:删除列表中所有值等于
value
的元素。
2.8LSET
LSET命令:根据下标修改元素
- LSET key index element
- O(N)
- lindex 可以很好的处理下标越界的情况,直接返回 nil。
- lset 来说则会报错,不会像 js 那样,直接在 10 这个下标这里搞出个元素来
2.9LTRIM
- LTRIM key start stop
- 保留 start 和 stop 之间区间内的元素(区间外面两边的元素就直接被删除了)
2.10BLPOP,BRPOP(阻塞版本)
阻塞队列相关:
- 但阻塞不会本质慢很多,阻塞一段时间后,回到 Redis 可以执行其他命令。
- 使用 brpop 和 blpop 的时候,这里是可以显式设置超时时间的!!(不一定是无休止的等待)
- 此处的 blpop 和 brpop 看起来好像耗时很久,但是实际上并不会对 redis 服务产生负面影响!
- 命令中如果设置了多个键,那么会从左到右进行遍历,一旦有一个对应的列表中可以弹出元素,命令立即返回。
- blpop 和 brpop 都可以同时去尝试获取多个 key 的列表的元素的。
- 多个 key 对应多个 list。
- 这多个 list 哪个有元素了,就会返回哪个元素。
- 如果多个客户端同时对一个键进行 lpop,则最先执行命令的客户端会得到弹出的元素。
- blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,和对应非阻塞版本的作⽤基本⼀致
- 在列表中有元素的情况下,阻塞和非阻塞表现是⼀致的。但如果列表中没有元素,非阻塞版本会立即返回 nil,但阻塞版本会根据 timeout,阻塞⼀段时间,期间 Redis 可以执行其他命令,但要求执行该命令的客⼾端会表现为阻塞状态
- 命令中如果设置了多个键,那么会从左向右进⾏遍历键,⼀旦有⼀个键对应的列表中可以弹出元素,命令立即返回。 如果多个客⼾端同时多⼀个键执行pop,则最先执行命令的客⼾端会得到弹出的元素
BROPO和上述同理。
- BLPOP key [key ...] timeout
- 此处可以指定一个 key 或者多个 key,每个 key 都对应一个 list。
- 如果这些 list 有任何一个非空,blpop 都能够把这里的元素给获取到,立即返回。
- 如果这些 list 都为空,此时就需要阻塞等待,等待其他客户端往这些 list 中插入元素了。
- 此处还可以指定超时时间,单位是秒(Redis 6,超时时间允许设定成小数,Redis 5 中,超时时间,得是整数)。
- 针对一个非空的列表进行操作:
- 返回的结果相当于一个 pair(二元组)。
- 一方面是告诉我们当前的数据来自于哪个 key。
- 一方面是告诉我们取到的数据是啥。
- 针对一个空的列表进行操作。
- 针对多个 key 进行操作。
- 针对一个非空的列表进行操作:
- brpop 效果和 blpop 完全一样。(这里是尾端了)
- 此处的这俩阻塞命令用途主要就是用来作为 “消息队列”。
- 当前这俩命令虽然可以一定程度的满足 “消息队列” 这样的需求,整体来说,这俩命令功能还是比较有限。
- blpop 和 brpop 有些类似逻辑。
三.内部编码
- 压缩列表(ziplist):一种把数据按照更紧凑的压缩形式进行表示,以节省空间的数据结构,但当元素个数增多时,操作效率会降低。
- 链表(linkedlist):当列表类型⽆法满⾜ ziplist 的条件时,Redis 会使⽤ linkedlist 作为列表的内部实现。
- 快速列表(quicklist):相当于链表和压缩列表的结合体,整体是一个链表,链表的每个节点是一个压缩列表。它通过控制每个压缩列表的大小,并将多个压缩列表通过链式结构连接起来,在空间和效率之间取得较好的平衡。
- 相关配置:
- list-max-ziplist-entries 配置和 list-max-ziplist-value 配置,不过现在已经不再使用。
list-max-ziplist-size 配置,有不同的取值对应不同的最大大小, - -5 对应 64KB(不推荐用于常规工作负载)、
- -4 对应 32KB(不推荐
- -3 对应 16KB(可能不推荐)、
- -2 对应 8KB(良好)、
- -1 对应 4KB(良好)等。
可以通过 OBJECT encoding key 命令查看某个键对应的数据结构编码,例如图中显示为 quicklist。
四.list的经典应用场景
列表(list)类型的应用场景
- 用 list 作为 “数组” 这样的结构,来存储多个元素。
- redis 提供的查询功能,不像 mysql 这么强大。
举个栗子~
- MySQL 表结构:
- 学生表(student):包含 studentId(学生 ID)、studentName(学生姓名)、age(年龄)、score(分数)、classId(班级 ID)字段。
- 班级表(class):包含 classId(班级 ID)、className(班级名称)字段。
- 在上表结构中,可以很方便地实现 “查询指定班级中有哪些同学”。
- Redis 存储结构:
- 以特定的 key 来存储学生和班级信息,如 student:1 存储学生 1 的姓名、年龄、分数;class:1 存储班级 1 的名称等。
- 还通过 classStudents:1 、classStudents:2 等键来存储对应班级的学生 ID 列表。
- 具体 redis 中的数据如何组织,都要根据实际的业务情况来决定。
4.1消息队列 和 分频道的消息队列
消息队列 :
Redis 可以使用 lpush + brpop 命令组合实现经典的阻塞式生产者-消费者模型队列,生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式地从队列中“争抢”队首元素。通过多个客户端来保证消费的负载均衡和高可用性。
Redis 阻塞消息队列模型:
- 生产者通过 lpush 操作向 Redis 的列表中添加元素。
- 多个消费者通过 brpop 操作从列表中获取元素,只有一个消费者能 “抢到” 元素,谁先执行 brpop 命令谁就能拿到新来的元素,这样的设定能构成一个 “轮询” 式的效果。
- 假设消费者执行顺序是 1、2、3,当新元素到达后,首先是消费者 1 拿到元素(按照执行 brpop 命令的先后顺序决定)。消费者 1 拿到元素后,就从 brpop 中返回(相当于这个命令执行完了),如果消费者 1 还想继续消费,就需要重新执行 brpop。此时再来新元素,就是消费者 2 拿到该元素并从 brpop 中返回,如果消费者 2 还想继续消费,也需要重新执行 brpop。接着再来新元素,就是消费者 3 拿到这个元素。
- brpop 操作是阻塞操作,当列表为空时,brpop 就会阻塞等待,一直等到其他客户端 push 了元素。
分频道的消息队列 :
Redis 同样使⽤ lpush + brpop 命令,但通过不同的键模拟频道的概念,不同的消费
者可以通过 brpop 不同的键值,实现订阅不同频道的理念。


举个栗子~
假设我们有一个社交应用,有 “通知”(notifications)和 “私信”(messages)两个频道。
- 生产者方面,当有新通知时,用lpush notifications "new like"生产通知消息;当有新私信时,用lpush messages "hello"生产私信消息。
- 消费者方面,有的消费者可能只关心通知,就执行brpop notifications 0;有的消费者可能只关心私信,就执行brpop messages 0;还有的消费者可能同时关心通知和私信,就执行brpop notifications messages 0,这样当notifications或messages列表中有新消息时,这个消费者就可能获取到消息进行处理。并且对于notifications列表来说,多个只订阅它的消费者中只有一个能抢到新通知消息;对于messages列表同理。
4.2微博 Timeline
每个用户都有属于自己的 Timeline(微博列表),现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
1.每篇微博使用哈希结构存储,例如微博中 3 个属性
1.每篇微博使用哈希结构存储,例如微博中 3 个属性
hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx
2.向用户 Timeline 添加微博,user:<uid>:mblogs 作为微博的键:
lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9
3.分页获取用户的 Timeline,例如获取用户 1 的前 10 篇微博:
keylist = lrange user:1:mblogs 0 9
for key in keylist {hgetall key
}
此方案在实际中可能存在两个问题:
- 1 + n 问题。即如果每次分页获取的微博个数较多,需要执行多次 hgetall 操作,此时可以考虑使用 pipeline(流水线)模式批量提交命令,或者微博不采用哈希类型,而是使用序列化的字符串类型,使用 mget 获取。
- 分裂获取文章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表做拆分。