本文根据5.0.2版本的redis源码详细解析ziplist数据结构。
1 创建一个ziplist
ziplist的内存结构如上图。
- 一个uint32_t的totalsize,保存当前ziplist所占用内存总量
- 接着一个uint32_t的offset,指向最后一个元素的位置,ziplist中的元素称作zlentry,起始时,offset指向ZIP_END
- 之后是uint16_t的len,这个存储的是 当前 ziplist中元素的个数
- 最后是一个字节的ZIP_END,ZIP_END的值是 0xFF(255),用来标记ziplist的结束
- ziplist的头部,共占用10个字节,尾部1个字节的标识符,额外占用了11个字节
2 ziplist中的元素
ziplist中的元素称作zlentry,内存结构如上。
首先是 前一个元素的长度 prevlen , 接着是 当前元素长度 curlen的编码encoding , 之后是当前元素实际数据 value。
zlentry中包含了以下信息:
- prevrawlensize:前一个元素的内存大小的编码空间
- prevrawlen:前一个元素的内存大小
- lensize:当前元素value部分占用内存大小的编码空间
- len:当前元素value部分占用内存大小
- _encoding:编码类型,标志value的类型和占用的字节数
以上信息,在经过一定的编码后进行存储。prevrawlensize和prevrawlen信息存储在prevlen部分;lensize、len、_encoding信息存储在encoding部分;当value是数值类型,同时小于一定阈值的时候,会同时将value存储在encoding中。value也是按照大端存储。
详细的存储编码过程如下:
前一个元素 长度 prevlen,保存的是前一个zlentry所占内存的总大小,prevlen可以占用 1个字节 或者 5个字节 :
- prevrawlen长度 < 254,此时prevlen使用一个字节, 直接存储 prevrawlen长度
- prevrawlen长度 >= 254, 此时prevlen使用五个字节, 第一个字节存储254作为标记, 后面四个字节存储 prevrawlen长度,用大端的方式存储
- 在ziplist插入元素的过程中,会产生 prevrawlen长度 < 254 但是占用5个字节的情况。这个在后面会有说到。
- 解码时,判断第一个字节的大小,确定使用的字节数,然后获得对应的长度
- 255已经被用作了ZIP_END,所以,这里用了254作为标志
- 疑问:这里改为varint方式编码是否更好?
- 答:个人认为,varint可以进一步压缩数据。不过,因为varint会使得prevlen编码长度取值范围更大(1到5),会使插入过程造成连锁反应的概率更大。所以,varint会进一步降低ziplist的写效率。在短数据的情况下,现在的编码方式能有效压缩数据;同时,只有两种编码长度的情况下,连锁反应发生的概率也比较低,这算是在数据压缩和写入效率之间得到的一个平衡点吧。
当前元素 长度curlen 经过编码之后存储,区分字符串和数值类型,可以占用 1个字节 、2个字节或者5个字节。编码过程如下:
- value是数值的情况下,根据value值的大小进行编码,此时encoding使用一个字节:
- value >= 0 && value <= 12 :encoding的 高四位是 1111(0xF),低四位存储value + 1,此时value长度是0,此时encoding的取值范围是 1111 0001 - 1111 1101
- 疑问:这里低四位为什么存储 value + 1,不是直接存储 value?
- 答:如果直接存储value,那么value=0的时候,encoding的值是0xF0(1111 0000)