# 书接上回
上一篇文章 Redis 的数据结构 string 我们一起学习了这种类型的常用命令,并且还学习了 Redis
中的字符串的结构表示以及好处,这里我们接着学习另外一种数据结构 list
。
# list
简介
list
, 一般都会称为列表。在 Redis
中,这种数据结构是一种比较灵活的结构,由于其元素的是有序的,所以可以充当栈和队列这两种数据结构。实际在开发总也有很多应用场景。
一个 List
最多可以包含 2^32-1
个元素。
很多人都会以为 list
是用数组来实现的,非也,非也。它内部是 quicklist
这种数据结构。想要先睹为快的,那么坐电梯直达吧。
# list
的相关命令
# LPUSH
命令
- 语法
LPUSH key value [value …]
- 解释
lpush
: left push
。
将一个或者多个值插入到列表 key
的表头,返回列表的长度。元素可以是重复的。
如果 key
不存在,那么会先穿件一个列表,然后再执行 push
操作.
如果 key
值存在,但是 value
类型不是列表类型时,会返回一个错误。
- 演示
1 | 设置一个列表 |
# lpushx
命令
- 语法
LPUSHX key value
- 解释
仅当 key
存在的时候,才将 value
插入列表的表头。返回列表中元素的个数。
- 演示
1 | # 当key值不存在的时候,不会放入列表中 |
# rpush
命令
- 语法
RPUSH key value [value ...]
- 解释
rpush
就是 right push
。将一个或多个值 value
插入到列表 key
的表尾 (最右边)。返回列表的长度。
如果 key
不存在的时候,会创建一个空列表,然后在执行 rpush
操作。
如果 key
存在,但是不是一个列表类型时,返回一个错误。
- 演示
1 | 往列表中加入数据 |
# rpushx
命令
- 语法
rpushx key value
- 解释
与 lpushx
类似,如果 key
不存在时,什么都不会操作。如果 key
存在,才会将元素添加到表尾。
- 演示
1 | key不存在的时候,不会插入数据 |
# lpop
命令
- 语法
LPOP key
- 解释
left pop
;
移除并返回列表的头元素。当 key
不存在的时候,返回 nil
- 演示
1 | key不存在的时候,返回nil |
tip:
lpush
+lpop
=> 栈,rpush
+lpop
=> 队列。
# rpop
命令
- 语法
rpop key
- 解释
rpop
: right pop
;
和 lpop 相反。移除并返回列表的尾元素。如果 key 不存在返回 nil。
- 演示
1 | # key 不存在,返回nil |
tip:
lpush
+rpop
=> 队列,rpush
+rpop
=> 栈。
# lrange
命令
- 语法
LRANGE key start stop
- 解释
获取指定区间内的元素。0 表示第一个元素。如果超过了实际范围就返回空数组。
- 演示
1 | 127.0.0.1:6379> LRANGE k22 0 10 |
# rpoplpush
命令
- 语法
RPOPLPUSH source destination
- 解释
将 source
的尾元素插入到 destination
列表的头元素中,返回该元素。 注意,这是一个原子操作。
比如: source
: a,b,c
distination
: 1,2,3
使用 RPOPLPUSH source distination
, 则:
source
: a,b
distination
: c,1,2,3
- 演示
1 | # 设置列表1 |
# lrem
命令
- 语法
LREM key count value
- 解释
至多移除列表中 count
个与参数 value
相等的元素。
有以下情況:
count > 0
: 从表头开始向表尾搜索,移除与 value
相等的元素,最多移除 count
个 。
count < 0
: 从表尾开始向表头搜索,移除与 value
相等的元素,最多移除 |count|
个。
count = 0
: 移除表中所有与 value
相等的值。
- 演示
1 | 演示 count>0 时 |
# llen
命令
- 语法
LLEN key
- 解释
获取列表的长度。
如果 key
不存在的时候,返回 0
.
如果 key
对应类型不是 list
,则返回一个错误。
- 演示
1 | 127.0.0.1:6379> llen k30 |
# lindex
命令
- 语法
lindex key index
- 解释
返回列表中,下标为 index
的元素. -1
表示列表的最后一个元素,如果 key
不存在,或者 index
超出范围,返回 nil
, 如果 key 不是一个列表类型,返回一个错误。
- 演示
1 | 127.0.0.1:6379> lpush k31 v31_3 v31_2 v31_1 |
# linsert
命令
- 语法
linsert key BEFORE|AFTER pivot value
- 解释
将 value
插入到 key
队列 pivot
值之前或者之后。返回插入完成之后列表的长度。
如果 pivot
不存在 或者 key
不存在,不执行任何操作。
如果 key
对应的不是一个列表类型,返回一个错误。
- 演示
1 | 127.0.0.1:6379> linsert k32 BEFORE k31_1 k31_0 |
# lset 命令
- 语法
lset key index value
- 解释
将列表中的 索引为 index
的值设置为 value
。 如果 index
超出范围,则返回一个错误
- 演示
1 | 127.0.0.1:6379> lpush k33 v33_3 v33_1 |
# ltrim
命令
- 语法
ltrim key start stop
- 解释
保留列表从 start
到 stop
之间的元素。其他元素都将被删除。 注意:包含 (不删除) start
和 stop
两个元素.
如果 key
不存在,直接返回 OK
, 如果 key
对应的不是列表,直接返回错误。
- 演示
1 | 127.0.0.1:6379> lpush k34 v34_1 v34_2 v34_3 v34_4 v34_5 v34_6 |
# blpop 命令
- 语法
BLPOP key [key ...] timeout
- 解释
lpop
的 阻塞版本。 block left pop
当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。
当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。
- 演示
1 | # push到三组列表,分别三个元素 |
# brpop
命令
- 语法
BRPOP key [key ...] timeout
- 解释
rpop
的阻塞版本。 block right pop
当给定多个 key
的时候,按照 key
的先后顺序依次检查各个列表。直到弹出一个元素或者超时。
- 演示
1 | # 设置两个列表 |
Tips:
lpush
+brpop
=> 阻塞队列。
# brpoplpush
命令
- 语法
BRPOPLPUSH source destination timeout
- 解释
rpoplpush
的阻塞版本。 block right left push
。
当列表 source
为空的时候,该命令将阻塞,直到超时,或者 source 中有一个元素可以 pop。
- 演示
1 | # 设置一个列表 |
# list
内部结构之 quicklist
# quicklist
我们来看一下 list
的内部实现 quicklist
结构.
特别注明: quicklist
是双向的链表结构。
在 Redis
中使用如下结构体表示.
1 | typedef struct quicklist { |
quicklist
是回一个通用的双向链接快速列表实现。它的每个节点用 quicklistNode
表示。
一起来看下 qucklistNode
是什么吧。
# quicklistNode
1 | typedef struct quicklistNode { |
quicklistNode
是一个 32byte
的结构体,用于描述一个 quicklist
的一个节点。从代码中可看出,使用了位图来节约空间。在上面的代码中我们还提到两种数据结构,对应的是代码中 zl
指针,指向的位置,如果数据被压缩,指向 quicklistLZF
和 没有数据没有被压缩就是指向 ziplist
.
# ziplist
ziplist
这种结构比较复杂,而且在源码中也没有给出明确定义。那 ziplist
这么神秘的结构到底是什么样的呢?
别着急,我们先大体熟悉下 ziplist
这种结构的设计意图。
ziplist
是一个经过特殊编码的双向链表,它的设计意图就是 提高存储效率, ziplist
可以用于存储字符串或者整数,其中整数是按照真正的二进制进行编码的。 它能以 O(1)
的效率在表的两端进行 pop
和 push
操作。
我们都知道,普通的链表每项都是一块独立的内存空间,各项之间都是通过指针连接起来的。这种方式,会带来大量的空间碎片,指针引用也会占用部分空间内存。所以 ziplist
是将表中每项放在连续的空间内存中 (类似数组), ziplist
还对值采取了一个可变长度的存储方式,大的值就用大空间,小的值就用小空间。
# ziplist 结构的官方定义。
The general layout of the ziplist is as follows:
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
<uint32_t zlbytes>
is an unsigned integer to hold the number of bytes that the ziplist occupies, including the four bytes of the zlbytes field itself. This value needs to be stored to be able to resize the entire structure without the need to traverse it first.
<uint32_t zltail>
is the offset to the last entry in the list. This allows a pop operation on the far side of the list without the need for full traversal.
<uint16_t zllen>
is the number of entries. When there are more than 2^16-2 entries, this value is set to 2^16-1 and we need to traverse the entire list to know how many items it holds.
<uint8_t zlend>
is a special entry representing the end of the ziplist. Is encoded as a single byte equal to 255. No other normal entry starts with a byte set to the value of 255.
根据上面中解释我们可以得出以下这种模型:
如果没有特殊指定的话,都是采用小尾数法存储的。
-
zlbytes
: 存储一个无符号整数,用于存储 ziplist 的所用的字节数,(包括 zlbytes 字段本身的四个字节),当重新分配内容的时候,不需要遍历整个列表来计算内存大小。 -
zltail
: 一个无符号整数,表示 ziplist 中最后一个元素的偏移字节数,这样可以方便的找到最后一个元素,从而可以以 O (1) 的复杂度在尾端进行 pop 和 push。 -
zllen:压缩列表包含的结点的个数,即entry的个数。
这里的zllen
是占用16bit
, 也就是说最多存储2^16-2
个。但是ziplist
超了2^16-2
个也是可以表示的。那种情况就是16
个1
的时候,只需要从头遍历到尾就好了。 -
entry
: 真正存放数据的数据项,每个数据项都有自己的内部结构。 -
zlend
:ziplist
的最后一个字节,值固定等于255
,就是一个结束标记。
# entry 结构
entry
是由三部分构成的。
-
previous length(pre_entry_length)
: 表示前一个数据节点占用的总字节数,这个字段的用处是为了让 ziplist 能够从后向前遍历(从后一项的位置,只需向前偏移previous length
个字节,就找到了前一项)。这个字段采用变长编码。 -
encoding
(encoding&cur_entry_length
):表示当前数据节点 content 的内容类型以及长度。也采用变长编码。 -
entry-data
:表示当前节点存储的数据,entry-data
的内容类型有整数类型和字节数组类型,且某些条件下entry-data
的长度可能为0
。
所以我们可以得出 ziplist
是一个这样的结构。
有时,encoding 也可以代表 entry 本身,就像小整数一样。
这里就是大体的了解下 ziplist 这种数据结构。
这里是一篇关于 ziplist 详细解读的文章。
# quicklistLZF
看完了比较神秘的 ziplist
结构,我们来看一个比较简单的 quicklist
的压缩节点的结构 quicklistLZF
。
1 | /** |
# 总结
list
相关的命令。以及常见的应用场景。比如栈和队列等等。list
其实是一种链表结构,但是不是一个普通的链表结构。list
是由quicklist
这种数据结构实现的。quicklist
中的每个节点是quicklistNode
, 而quicklist
中zl
指针,指向的是 一个ziplist
或者quickListLZF
。ziplist
是一个比较神秘的数据结构,有5
部分构成,是连续存储的,可以实现O(1)
的尾端pop
和push
操作。
# 最后
期望与你一起遇见更好的自己