压缩列表是列表键和哈希键的底层实现之一,为了优化数据存储,节约内存,在列表、字典(哈希键)和有序集合的底层实现了使用压缩列表这一优化方案,当列表键只包含少量列表项,且列表项是小整数值、比较短的字符串,就使用压缩列表实现。
压缩列表构成
压缩列表是Redis为了节约内存开发的,由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或一个整数值
uint32_t zlbytes //整个压缩列表占用的内存字节数 p+zltail=尾节点地址
uint32_t zltail //尾节点距离列表起始地址的字节数
uint16_t zllen //节点数量 小于UINT16_MAX(65535) 实际节点数;等于时,需要遍历计算
entryX //各个节点,实际长度由节点保存的内容决定
uint8_t zlend //特殊值0xFF(255),用于标记压缩列表末端
压缩列表节点构成
字节数组的长度可以是以下的其中一种
- 长度小于等于63字节(2的6次方)
- 长度小于等于16383字节(2的14次方)
- 长度小于等于4294967295字节(2的32次方)
整数值可能是以下六种中的其中一种
- 4位长,介于0-12之间的无符号整数
- 1字节长的有符号整数
- 3字节长的有符号整数
- int16_t类型整数
- int32_类型整数
- int64_t类型整数
压缩列表节点构成 previous_entry_length+encoding+content
1、previous_entry_length属性以字节为单位,记录压缩列表前一个节点的长度,长度可以是1字节或者5字节
- 如果前一个节点的整体长度小于254字节,previous_entry_length属性只需要1个字节的空间来保存这个长度值。
- 前一个节点大于254字节的时候,previous_entry_length属性要用5个字节长的空间来记录长度值,其中第一个字节设置为0xFE(254),之后四个字节用于存储长度值
可以根据当前节点的起始地址计算出前一个节点的起始地址 p = c - previous_entry_length
2、encoding属性记录了节点的content属性所保存数据的类型以及长度
- 一字节、两字节或者五字节长,值得最高位为00、01、10是字节数组编码,数组长度由编码除去最高两位之后的其它位记录
00bbbbbb //长度小于等于63字节
01bbbbbb xxxxxxxx //长度小于等于16383字节
10______ aaaaaaaa bbbbbbbb cccccccc dddddddd //长度小于等于4294967295字节
- 一字节长,值得最高位为11开头的是整数编码,整数值的类型和长度由由编码除去最高两位之后的其它位记录
11000000 //int16_t类型整数
11010000 //int32_类型整数
11100000 //int64_t类型整数
11110000 //3字节长的有符号整数
11111110 //1字节长的有符号整数
1111xxxx //4位长,介于0-12之间的无符号整数,无须content属性
3、content属性负责保存节点的值
连锁更新
previous_entry_length在特殊情况下,添加或删除节点都有可能引起连锁更新
- 压缩列表里恰好有多个连续的,长度介于250字节至253字节之间的节点,才有可能引发连锁更新,实际情况不多见
- 出现连锁更新,如果节点数量不多,也不会对性能造成任何影响
压缩列表如何节约内存
压缩列表是一种序列化的数据结构,这种数据结构的功能是将一系列数据与其编码信息存储在一块连续的内存区域,这块内存物理上是连续的,但逻辑上被分为多个组成部分,即节点,目的是为了在一定可控的时间复杂度条件下尽可能的减少不必要的内存开销,从而达到节省内存的效果。
我们对比下不同点:
比如双端链表,每一个值都是用一个节点来表示,每个节点都会有指向前一个节点和后一个节点的指针,以及指向节点包含的字符串值的指针,而字符串值又分为三个部分存储,第一部分存储字符串长度,第二部分存储字符串值中剩余可用的字节量,第三部分存储的则是字符串数据本身,所以一个节点往往都需要存储三个指针、两个记录字符串信息的整数、字符串本省和一个额外的字节,总体上额外的开销是很大的(21字节),显然压缩列表这种方式节约了不少存储空间。