这是我参与更文挑战的第5天,活动详情查看:更文挑战
1、列表对象简介
Redis列表是简单的字符串列表,按照插入顺序进行排序,支持从头部或者尾部添加元素,因此列表对象也经常用作为队列。一个列表键最多可以含个元素。
列表对象常用命令如下:
lpush key value1[value2] //将一个或多个值插入列表头中并返回列表长度,如果key不存在则创建key后插入,如果key存在但不是列表类型则返回一个错误信息
lpushsx key value1[value2] //将一个或多个值插入到已存在的列表头部并返回列表长度,列表不存在则操作无效
lindex key index //获取指定位置元素
lrange key start stop //获取列表指定范围内的元素
lpop key //移除并返回列表第一个元素,key不存在时返回nil
lrem key count value //移除值为value的元素,count>0 则从表头开始移除count个元素,count<0 则从表尾开始移除count绝对值个元素,count=0 则移除所有值为value的元素
lset key index value //通过索引设置列表元素的值
llen key //获取列表长度
ltrim key start stop //对一个列表进行修剪,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
blpop key1 [key2 ] timeout //移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
linsert key BEFORE|AFTER pivot value //在列表的元素前或者后插入元素
rpush key value1 [value2] //在列表尾中添加一个或多个值
rpush key value //为已存在的列表尾添加值
rpop key //移除并获取列表最后一个元素
brpop key1 [key2 ] timeout //移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
rpoplpush source destination //移除列表的最后一个元素,并将该元素添加到另一个列表并返回
brpoplpush source destination timeout //从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
2、列表对象编码
Redis列表对象的编码可以是ziplist 或者 linkedlist。ziplist编码的底层结构为压缩列表;linkedlist编码的底层为双端链表的数据结构。
如下为ziplist编码的列表对象示意图
如下为linkedlist编码的列表对象示意图
注:链表中的每个节点是一个字符串对象,而不是一个简单的字符串。
2.1 编码转换
当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist 编码:
-
列表对象保存的所有字符串元素的长度都小于
64字节; -
列表对象保存的元素数量小于
512个;
不能满足这两个条件的列表对象需要使用 linkedlist 编码。
注:以上两个条件的上限值是可以修改的, 具体请看配置文件中关于
list-max-ziplist-value选项和list-max-ziplist-entries选项的说明。
3、压缩列表(ziplist)
压缩列表是 Redis 为了节约内存而开发的, 由一系列特殊编码的连续内存块组成的顺序型数据结构。
3.1 压缩列表构成
一个压缩列表可以包含任意多个节点(entry), 每个节点可以保存一个字节数组或者一个整数值。如下,展示了压缩列表的各个组成部分。
| 属性 | 类型 | 长度 | 用途 |
|---|---|---|---|
zlbytes | uint32_t | 4 字节 | 记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配, 或者计算 zlend 的位置时使用。 |
zltail | uint32_t | 4 字节 | 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节: 通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址。 |
zllen | uint16_t | 2 字节 | 记录了压缩列表包含的节点数量: 当这个属性的值小于 UINT16_MAX (65535)时, 这个属性的值就是压缩列表包含节点的数量; 当这个值等于 UINT16_MAX 时, 节点的真实数量需要遍历整个压缩列表才能计算得出。 |
entryX | 列表节点 | 不定 | 压缩列表包含的各个节点,节点的长度由节点保存的内容决定。 |
zlend | uint8_t | 1 字节 | 特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。 |
3.2 压缩列表节点的构成
每个压缩列表节点可以保存一个字节数组或者一个整数值
字节数组长度可以有以下3种
- 长度小于等于
63( )字节的字节数组;- 长度小于等于
16383()字节的字节数组;- 长度小于等于
4294967295()字节的字节数组;整数值则可以有以下6种长度
4位长,介于0至12之间的无符号整数;1字节长的有符号整数;3字节长的有符号整数;int16_t类型整数;int32_t类型整数;int64_t类型整数。
每个压缩列表节点都由 previous_entry_length 、 encoding 、 content 三个部分组成。
3.2.1 previos_entry_length属性
previos_entry_length 以字节为单位,记录压缩列表中前一个节点的长度,previos_entry_length属性长度可以是1字节或5字节
- 如果前一个节点长度小于254字节,
previos_entry_length长度为1字节 - 如果前一个节点长度大于等于254字节,
previos_entry_length长度为5字节
通过previos_entry_length属性可以实现ziplist从表尾向表头遍历。
3.2.2 encoding 属性
节点的 encoding 属性记录了节点的 content 属性所保存数据的类型以及长度:
-
一字节、两字节或者五字节长, 值的最高位为
00、01或者10的是字节数组编码: 这种编码表示节点的content属性保存着字节数组, 数组的长度由编码除去最高两位之后的其他位记录; -
一字节长, 值的最高位以
11开头的是整数编码: 这种编码表示节点的content属性保存着整数值, 整数值的类型和长度由编码除去最高两位之后的其他位记录;编码 编码长度 content属性保存的值110000001字节int16_t类型的整数。110100001字节int32_t类型的整数。111000001字节int64_t类型的整数。111100001字节24位有符号整数。111111101字节8位有符号整数。1111xxxx1字节使用这一编码的节点没有相应的 content属性,xxxx保存content值
3.2.3 content属性
节点的 content 属性负责保存节点的值, 节点值可以是一个字节数组或者整数, 值的类型和长度由节点的 encoding 属性决定。
3.3 连锁更新
在一个压缩列表中, 有多个连续的、长度介于 250 字节到 253 字节之间的节点 e1 至 eN 。因为 e1 至 eN 的所有节点的长度都小于 254 字节, 所以记录这些节点的长度只需要 1 字节长的 previous_entry_length 属性, 换句话说, e1 至 eN 的所有节点的 previous_entry_length 属性都是 1 字节长的。
如果我们将一个长度大于等于 254 字节的新节点 new 设置为压缩列表的表头节点, 那么 new 将成为 e1 的前置节点,因为 e1 的 previous_entry_length 属性仅长 1 字节, 它没办法保存新节点 new 的长度, 所以程序将对压缩列表执行空间重分配操作, 并将 e1 节点的 previous_entry_length 属性从原来的 1 字节长扩展为 5 字节长。
现在, 麻烦的事情来了 —— e1 原本的长度介于 250 字节至 253 字节之间, 在为 previous_entry_length 属性新增四个字节的空间之后, e1 的长度就变成了介于 254 字节至 257 字节之间, 而这种长度使用 1 字节长的 previous_entry_length 属性是没办法保存的。
因此, 为了让 e2 的 previous_entry_length 属性可以记录下 e1 的长度, 程序需要再次对压缩列表执行空间重分配操作, 并将 e2 节点的 previous_entry_length 属性从原来的 1 字节长扩展为 5 字节长。
正如扩展 e1 引发了对 e2 的扩展一样, 扩展 e2 也会引发对 e3 的扩展, 而扩展 e3 又会引发对 e4 的扩展……为了让每个节点的 previous_entry_length 属性都符合压缩列表对节点的要求, 程序需要不断地对压缩列表执行空间重分配操作, 直到 eN 为止。
这就是连锁更新,因为一次操作,导致连续多次的空间扩展操作。除了添加节点会引发连锁更新,删除节点也是有可能引起连锁更新的。因为连锁更新在最坏情况下需要对压缩列表执行 N 次空间重分配操作, 而每次空间重分配的最坏复杂度为 O(N) , 所以连锁更新的最坏复杂度为 O(N^2) 。
要注意的是, 尽管连锁更新的复杂度较高, 但它真正造成性能问题的几率是很低的:
- 首先, 压缩列表里要恰好有多个连续的、长度介于
250字节至253字节之间的节点, 连锁更新才有可能被引发, 在实际中, 这种情况并不多见; - 其次, 即使出现连锁更新, 但只要被更新的节点数量不多, 就不会对性能造成任何影响: 比如说, 对三五个节点进行连锁更新是绝对不会影响性能的;
4、双端列表(linkedlist)
双端链表中的每一个节点都有两个指针指向其前驱节点和后继节点。**表头节点前驱节点指向NULL,表尾节点后继节点指向NULL。**链表和链表节点的结构如下。
//链表节点结构,双端链表结构
typedef struct listNode {
// 前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}listNode;
//链表结构
typedef struct list {
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//节点数量
unsigned long len;
//节点值复制函数,复制链表节点所保存的值
void *(*dup)(void *ptr);
//节点值释放函数,释放链表节点所保存的值
void (*free)(void *ptr)
//节点值对比函数,对比链表节点所保存的值和另一个输入值是否相等
int (*match)(void *ptr,void *key)
}list;
Redis的链表实现特性
- 双端,每个节点带有prev和next指针,获取某个节点的前后节点复杂度都是O(1)。
- 无环,表头节点的prev指针和表尾节点的next指针都指向NULL。
- 带有表头节点指针和表尾指针,获取表头表尾节点复杂度都是O(1)。
- 带有链表长度计数器,获取链表长度复杂度为O(1)。
- 多态,节点使用void*指针保存节点值,并且可以通过dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。