共计 3515 个字符,预计需要花费 9 分钟才能阅读完成。
压缩列表 (ziplist) 是列表键和哈希键的底层实现之一.
- 当一个列表键只蕴含大量列表项, 并且每个列表项要么就是小整数值, 要么就是长度比拟短的字符串, 那么 Redis 就会应用压缩列表来做列表键的底层实现.
- 当一个哈希键只蕴含大量键值对, 并且每个键值对的键和值要么就是小整数值, 要么就是长度比拟短的字符串, 那么 Redis 就会应用压缩列表来做哈希键的底层实现.
1. 压缩列表的形成
压缩列表是 Redis 为了节约内存而开发的, 由一系列非凡编码的间断内存块组成的程序型 (sequential) 数据结构.
一个压缩列表能够蕴含任意多个节点 (entry) , 每个节点能够保留一个字节数组或者一个整数值.
<!–more–>
压缩列表的整体布局:
<zlbytes><zltail><zllen><entry><entry><zlend>
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),用于标记压缩列表的末端. |
2 压缩列表节点的形成
每个压缩列表节点能够保留一个字节数组或者一个整数值, 其中, 字节数组能够是以下三种长度的其中一种:
- 长度小于等于 63 (2^{6}-1)字节的字节数组;
- 长度小于等于 16383 (2^{14}-1) 字节的字节数组;
- 长度小于等于 4294967295 (2^{32}-1)字节的字节数组;
而整数值则能够是以下六种长度的其中一种:
- 4 位长,介于 0 至 12 之间的无符号整数;
- 1 字节长的有符号整数;
- 3 字节长的有符号整数;
- int16_t 类型整数;
- int32_t 类型整数;
- int64_t 类型整数.
压缩列表节点的形成:<previous_entry_length><encoding><content>
2.1 previous_entry_length
节点的 previous_entry_length 属性以字节为单位, 记录了压缩列表中前一个节点的长度.
previous_entry_length 属性的长度能够是 1 字节或者 5 字节:
- 如果前一节点的长度小于 254 字节, 那么 previous_entry_length 属性的长度为 1 字节: 前一节点的长度就保留在这一个字节外面.
- 如果前一节点的长度大于等于 254 字节, 那么 previous_entry_length 属性的长度为 5 字节: 其中属性的第一字节会被设置为 0xFE (十进制值 254), 而之后的四个字节则用于保留前一节点的长度.
因为节点的 previous_entry_length 属性记录了前一个节点的长度, 所以程序能够通过指针运算, 依据以后节点的起始地址来计算出前一个节点的起始地址.
压缩列表的从表尾向表头遍历操作就是应用这一原理实现的: 只有咱们领有了一个指向某个节点起始地址的指针, 那么通过这个指针以及这个节点的 previous_entry_length 属性, 程序就能够始终向前一个节点回
溯, 最终达到压缩列表的表头节点.
2.2 encoding
节点的 encoding 属性记录了节点的 content 属性所保留数据的类型以及长度:
- 一字节, 两字节或者五字节长, 值的最高位为 00 , 01 或者 10 的是字节数组编码: 这种编码表示节点的 content 属性保留着字节数组, 数组的长度由编码除去最高两位之后的其余位记录;
- 一字节长, 值的最高位以 11 结尾的是整数编码: 这种编码表示节点的 content 属性保留着整数值, 整数值的类型和长度由编码除去最高两位之后的其余位记录;
编码 | 编码长度 | content 属性保留的值 |
00bbbbbb | 1 字节 | 长度小于等于 63 字节的字节数组. |
01bbbbbb xxxxxxxx | 2 字节 | 长度小于等于 16383 字节的字节数组. |
10______ aaaaaaaa bbbbbbbb cccccccc dddddddd | 5 字节 | 长度小于等于 4294967295 的字节数组. |
编码 | 编码长度 | content 属性保留的值 |
11000000 | 1 字节 | int16_t 类型的整数. |
11010000 | 1 字节 | int32_t 类型的整数. |
11100000 | 1 字节 | int64_t 类型的整数. |
11110000 | 1 字节 | 24 位有符号整数. |
11111110 | 1 字节 | 8 位有符号整数. |
1111xxxx | 1 字节 | 应用这一编码的节点没有相应的 content 属性, 因为编码自身的 xxxx 四个位曾经保留了一个介于 0 和 12 之间的值, 所以它毋庸 content 属性. |
2.3 content
节点的 content 属性负责保留节点的值, 节点值能够是一个字节数组或者整数, 值的类型和长度由节点的 encoding 属性决定.
3. 连锁更新
后面说过, 每个节点的 previous_entry_length 属性都记录了前一个节点的长度:
- 如果前一节点的长度小于 254 字节, 那么 previous_entry_length 属性须要用 1 字节长的空间来保留这个长度值.
- 如果前一节点的长度大于等于 254 字节, 那么 previous_entry_length 属性须要用 5 字节长的空间来保留这个长度值.
如果 原有的节点都小于 254 字节, 忽然间插入一个大于等于 254 字节, 压缩列表将会产生空间重调配 (连锁更新);
删除节点, 也会产生导致连锁更新.
因为连锁更新在最坏状况下须要对压缩列表执行 N 次空间重调配操作, 而每次空间重调配的最坏复杂度为 O(N) , 所以连锁更新的最坏复杂度为 O(N^2).
要留神的是, 只管连锁更新的复杂度较高, 但它真正造成性能问题的几率是很低的:
- 首先, 压缩列表里要恰好有多个间断的, 长度介于 250 字节至 253 字节之间的节点, 连锁更新才有可能被引发, 在理论中, 这种状况并不多见;
- 其次, 即便呈现连锁更新, 但只有被更新的节点数量不多, 就不会对性能造成任何影响: 比如说, 对三五个节点进行连锁更新是相对不会影响性能的;
因为以上起因, ziplistPush 等命令的均匀复杂度仅为 O(N) , 在理论中, 咱们能够释怀地应用这些函数, 而不用放心连锁更新会影响压缩列表的性能.
4. 压缩列表 API
函数 | 作用 | 算法复杂度 |
ziplistNew | 创立一个新的压缩列表. | O(1) |
ziplistPush | 创立一个蕴含给定值的新节点, 并将这个新节点增加到压缩列表的表头或者表尾. | 均匀 O(N),最坏 O(N^2). |
ziplistInsert | 将蕴含给定值的新节点插入到给定节点之后. | 均匀 O(N),最坏 O(N^2). |
ziplistIndex | 返回压缩列表给定索引上的节点. | O(N) |
ziplistFind | 在压缩列表中查找并返回蕴含了给定值的节点. | 因为节点的值可能是一个字节数组, 所以查看节点值和给定值是否雷同的复杂度为 O(N) , 而查找整个列表的复杂度则为 O(N^2). |
ziplistNext | 返回给定节点的下一个节点. | O(1) |
ziplistPrev | 返回给定节点的前一个节点. | O(1) |
ziplistGet | 获取给定节点所保留的值. | O(1) |
ziplistDelete | 从压缩列表中删除给定的节点. | 均匀 O(N),最坏 O(N^2). |
ziplistDeleteRange | 删除压缩列表在给定索引上的间断多个节点. | 均匀 O(N),最坏 O(N^2). |
ziplistBlobLen | 返回压缩列表目前占用的内存字节数. | O(1) |
ziplistLen | 返回压缩列表目前蕴含的节点数量. | 节点数量小于 65535 时 O(1) , 大于 65535 时 O(N). |
因为 ziplistPush , ziplistInsert , ziplistDelete 和 ziplistDeleteRange 四个函数都有可能会引发连锁更新, 所以它们的最坏复杂度都是 O(N^2).
5. 总结
- 压缩列表是一种为节约内存而开发的程序型数据结构.
- 压缩列表被用作列表键和哈希键的底层实现之一.
- 压缩列表能够蕴含多个节点,每个节点能够保留一个字节数组或者整数值.
- 增加新节点到压缩列表, 或者从压缩列表中删除节点, 可能会引发连锁更新操作, 但这种操作呈现的几率并不高.
文章来源于自己博客,公布于 2018-06-02,原文链接:https://imlht.com/archives/141/