压缩列表是Redis为了节省内存而设计的特殊编码的双端链表,可以存储字符串和整数,整数值保存为实际的整数而不是字符数组,它允许在链表两端进行push和pop操作,时间复杂度为O(1),很多的操作需要对内存进行重新分配,所以实际情况还与内存使用的大小有关。
1. 定义
1.1 压缩列表
从本质上来说,ziplist就是一串按照某种固定方式存储的字节数组,不依赖c语言的strcut,完全是内存的一些约束和操作,它包括5个组成部分zlbytes, zltail, zllen, entries, zlend。
| 属性 | 类型 | 长度 | 作用 |
|---|---|---|---|
| zlbytes | uint32_t | 4字节 | 整个压缩列表使用的总字节数 |
| zltail | uint32_t | 4字节 | 尾结点到压缩列表头部有多少字节 |
| zllen | uint16_t | 2字节 | 结点的数量,超过UINT16_MAX则需要变量列表计算数量 |
| entries | entry | 长度不定 | 压缩列表的结点,多个结点间分割边界,通过结点属性值区分 |
| zlend | uint8_t | 1字节 | 结束标识符0XFF(255),表示压缩列表的结尾 |
1.2 压缩列表结点
结点可以存储字符串或在整数。
- 字符串
- 长度(1 ~ 2^6-1) 的字节数组
- 长度(1 ~ 2^14-1) 的字节数组
- 长度(1 ~ 2^32-1) 的字节数组
- 整数值
- 长度4bit,介于0 ~ 12的无符号整数
- 长度1字节,有符号整数
- 长度3字节,有符号整数
- int16_t类型整数
- int32_t类型整数
- int64_t类型整数
每个结点由三部分组成,prevlen,encoding,entry-data
prevlen
该属性表示当前结点的前驱结点的长度。
- 若前驱结点长度小于254字节,使用1byte存储prevlen
- 若前驱结点长度大于等于254字节,使用5byte存储prevlen,其中第一字节被设置为0XFE,表示长度由后4字节表示。
因为每个结点都保存了前一个结点的长度,所以可以通过地址计算出上一个结点的地址。
encoding
该属性表示当前结点保存数据所需要的类型和长度,有时候encoding也表示数据本身(小的整数值)
| 编码 | 描述 |
|---|---|
| 00pppppp(1字节) | 使用6bit存储字符串长度 |
| 01pppppp qqqqqqqq (2字节) | 使用14bit存储字符串长度 |
| 10000000 xxxxx……..(5字节) | 使用32bit存储字符串长度 |
| 11000000(1字节) | 使用2字节表示int16_t |
| 11010000(1字节) | 使用4字节表示int32_t |
| 11100000(1字节) | 使用8字节表示int64_t |
| 11110000(1字节) | 使用3字节表示24位有符号整数 |
| 11111110(1字节) | 使用1字节表示8位有符号整数 |
| 1111xxxx(1字节) | 使用4bit表示 0 ~ 12无符号整数 |
| 11111111(1字节) | 0xFF表示ziplist结束标识 |
entry-data
结点的数据值可以是一个字节数组或者整数,类型有encoding决定。
2. 连锁更新
压缩列表中使用prevlen保存前驱结点的长度,长度小于254需要一个字节存储prevlen,但若长度大于254则需要5个字节进行存储,那么可能会存在这样的一种情况:
在一个ziplist中,存在多个连续的结点,大小介于250~253字节,如结点 A,B,C,D,E,F,…。这时,要插入一个大于254结点的O到B结点之前,由于B结点之前的prevlen保存的是A结点的长度(小于254),所以只需要1个字节,但要保存结点O的话需要扩展prevlen为5个字节,这样导致B结点的大小大于了254,而C结点也需要扩展,如此产生连锁更新的效应。
添加结点可能触发压缩列表的连锁更新操作,这个操作的开销还是比较大的,在最坏的情况下需要执行N次重新分配内存操作。
删除结点也可能导致这种情况发生,但为了防止一会扩展,一会缩小的抖动发生,当删除结点时出现这种情况并不进行处理。
实际情况中,这种极端情况比较少见,所以并不会对性能造成很大影响。
3. 源码剖析
- 连锁更新
1 | /** |
- 插入结点
1 | unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) { |
- 删除结点
1 | unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) { |