当列表项或者哈希键的键和值是一些小整数或是长度较短的字符串时,redis就使用压缩列表作为列表键的底层实现。
压缩列表的实现
压缩列表,压缩,就是为了节约内存。
它是由一系列特殊编码的连续内存块组成的顺序型数据结构。
一个压缩列表可包含任意多个节点,每个节点可保存一个字节数组或一个整数值。
二、压缩列表结构
一个压缩列表由以下几个部分组成:
zlbytes、zltail、zllen、N个entry以及zlend:
属性 | 类型 | 长度 | 作用 |
---|---|---|---|
zlbytes | uint32_t | 4 | 压缩列表占用的内存字节数 |
zltail | uint32_t | 4 | 表尾节点距离列表起始地址的字节数,因此可通过起始字节直接计算得到表尾地址 |
zllen | uint16_t | 2 | 节点数量。小于65535时,值就是节点数量;大于时,需要遍历列表才能计算得出节点数量。 |
entryX | 列表的节点 | 即保存的节点。长度由各自节点内容决定 | |
zlend | uint8_t | 1 | 特殊值0xFF(255),标记压缩列表末端 |
三、节点构成
每个节点由previous_entry_length
、encoding
和content
三部分组成。
3.1 previous_entry_length
作用
记录前一个节点长度,单位字节。
取值
取值 | 情况 |
---|---|
1字节 | 前一节点长度小于254字节 |
5字节 | 前一节点大于等于254字节。第一个字节为0xFE(254),后面的4个字节保存长度前一个节点长度 |
使用
通过该部分的取值,可以计算前一个节点的起始地址:当前节点起始地址减去该部分记录的值。
因此从表尾到表头遍历的原理就是基于此,不断计算前一个节点的起始地址,直到到达表头。
3.2 encoding
作用
记录content的类型和长度。
-
长度为1字节、2字节或5字节时
值的最高位为00,01或10时的编码,表示content保存的是字节数组。
数组长度就是去掉前两位之后的记录。
如:00001011, 长度为1字节,高两位为00,所以content保存的是字节数组,后6位001011表名content长度为11,如content为:“wmlwmlwmlww”
-
长度为1字节时
值的最高位为11,则表示content保存的是整数值。长度为去掉最高位的两个,其他位的记录。
编码 编码长度(字节) content保存的值 11000000 1 int16_t类型整数 11010000 1 int32_t类型整数 11100000 1 int64_t 11110000 1 24位有符号整数 11111110 1 8位有符号整数
3.3 content
保存节点的值(字节数组或整数)。
四、连锁更新
4.1 添加节点导致更新
如上,假如节点entry1到entryN长度都小于254字节,则每个节点的previous_entry_length
属性都使用了1字节空间保存前一个节点的长度。
而如果此时新增一个长度大于254字节的节点new,那么entry1原本的1字节长度就无法保存new的长度,就需要进行空间重分配,扩展到5字节。
同样的,entry2也因为entry1变为5字节而进行扩展,这样一直到entryN都要进行空间重分配。这就是连锁更新现象。
4.2 删除节点导致连锁更新
假如big节点大于254字节,则small使用1字节存储其长度,后面N个entry同上面一样都使用1字节存储长度。
假如此时删除了节点small,则entry1的前一个节点变成big,因为大于254字节,所以entry1需要扩展都5字节,就会出现和4.1一样的连锁更新操作。
最坏情况下需要对压缩列表进行N次空间重分配,每次重分配的最坏复杂度为O(N),连锁更新的最坏复杂度为O(N²)。
但是因为进行连锁更新,要求列表由连续多个长度在250到253字节的节点,这种情况并不多见。
且就算出现连锁更新,更新节点不多也基本不会对性能造成影响,像这种整体全部更新的情况也是更少。
所以基本不用担心它的性能问题。
参考:《Redis设计与实现》