1,前言
压缩列表(ziplist)是列表键和哈希键的底层实现之一。
当列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么是短字符串,Redis就会用压缩列表作为列表键的底层实现
Redis的其他数据结构相关如下:
简单动态字符串
链表
字典
整数集合
2,压缩列表
2.1,列表结构
压缩列表是Redis为了节约内存开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(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),用于标记压缩列表的末端 |
以下面的压缩列表为例
zlbytes
:该值是0x50(十进制的80),表示压缩列表的总长度位80字节zltail
:该值是0x3c(十进制的60),假如此时有一个指针p
指向压缩列表起始地址,那么用该指针加上偏移量60就可以计算出表尾节点entry3
的地址zllen
:该值是0x3(十进制的3),表示该压缩列表包含三个节点
2.2,列表节点结构
每个压缩列表节点都可以保存一个字节数组或者一个整数值
如果是字节数组:
- 长度小于等于63(26-1)字节的字节数组
- 长度小于等于16383(214-1)字节的字节数组
- 长度小于等于4 294 967 295(232-1)字节的字节数组
如果是整数值的话:
- 4位长,介于0-12之间的无符号整数
- 1字节长的有符号整数
- 3字节长的有符号整数
int16_t
类型整数int32_t
类型整数int64_t
类型整数
每个列表节点的结构示意图如下:
2.2.1,previous_entry_length
节点的previous_entry_length
属性以字节为单位,记录了前一个节点的长度,该属性长度可以是一个字节也可以是五个字节。
- 如果前一个节点的长度小于254字节,那么
previous_entry_length
属性的长度为1字节;前一字节的长度就保存在这一个字节里面 - 如果前一字节的长度大于等于254字节,那么
previous_entry_length
属性的长度为5字节;其中属性的第一字节就会被设置为0xFE
(十进制254),而之后的四个字节则用于保存前一节点的长度
示例
- 图7-5表示前一个结点的长度为0x05(十进制5个字节)
- 图7-6中,
0xFE
表示这是一个五字节长的previous_entry_length
属性,而之后的0x00002766
(十进制10086)才是前一节点的长度
好处
因为previous_entry_length
记录了前一节点的长度,所以只要拿到当前节点的地址,就能实现从表尾节点向表头方向进行遍历操作(用当前指针减去previous_entry_length
属性的值就能获得上一节点的起始地址)
2.2.2,encoding
上面提及到,压缩列表节点可以保存字节数组或者整数,encoding
属性就是用来表示当前节点的存储数据类型和长度
- 一字节长、两字节长或者五字节长:值的最高位为00、01或者10代表的是字节数组编码,这种编码表示节点的
content
属性存储的是字节数组。数组的长度由encoding
出去最高两位之后的其他位记录 - 一字节长:值的最高位以11开头的是整数编码,这种编码表示
content
属性存储的是整数值,整数值的类型和长度由encoding
出去最高两位之后的其他位记录。
2.2.3,content
节点的content
属性负责保存节点值,节点值可以是一个字节数组或者整数,值得类型和长度由encoding
属性决定。
图7-9是一个存储字节数组的节点
- 编码最高位是00,表示节点保存的是字节数组
- 编码后六位是字节数组的长度,也就是十进制的11
content
表示该节点存储的是hello world
图7-10是一个存储整数值的节点- 编码
11000000
表示节点保存的是一个int16_t
类型的整数值 content
属性表示保存的是10086
2.3,连锁更新
回忆上面每个节点的结构中,都有一个previous_entry_length
用于存储前一个节点的长度。
如果前一节点的长度小于254,则该节点的previous_entry_length
用一个字节表示,反之需要五个字节。
考虑这样的一种极端情况:
- 一个压缩列表A中,存储了10个数据,而每个数据的长度都是介于250-253个字节之间
- 此时往第一个节点(称其为节点e1)前面,插入一个新的节点(称其为节点n),而这个节点的长度是大于254个字节的
- 这时会导致一个问题,节点e1要记录节点n的长度,并且由于节点n的长度大于254,所以e1的
previous_entry_length
需要由原先的一个字节扩展为五个字节 - 而e1的
previous_entry_length
扩展会导致e1节点长度介于254-257之间,这会导致e1的后续节点也引发相应的previous_entry_length
扩展,并且后续的所有节点都会发生这种问题 - 为了让每个列表的
previous_entry_length
属性都符合压缩列表对于节点的要求,程序需要不断地对压缩列表执行空间重分配
上述这种特殊情况下产生的连续多次空间扩展操作称之为”连锁更新“。除了添加新节点有可能引发连锁更新外,删除某个节点也有可能引发。
实际上连锁更新发生的条件并不常见;并且即使出现连锁更新,只要被更新的节点数量不多,就不会导致任何影响。