前言
Redis 在一定条件下会使用压缩链表替代常规链表,压缩链表具有紧凑型内存的特点,可以充分利用 CPU 高速内存金和节省内存的特性,但是也有一个致命缺点,连锁更新。
listpack 是为了替代压缩链表而设计出来的一个新数据结构,它也具备紧凑型内存的特点,但是每一个元素内部将不再记录前一个元素的长度,因此不会发生连锁更新。
这也充分说明了,尽量减少依赖。
结构
Listpack 的结构如图所示,它有 2 个头部,1 个尾部
- lpbyte,表示 listpack 使用的总字节数,占用 4 byte
- lpnum,表示 listpack 一共有多少个 entry,占用 2 byte
- entry,表示每一个元素
- lpend,表示 listpack 结束的标识符,值始终为 255,占用 1 byte
在每一个 entry 中都占有 3 部分
- encode,对 data 的编码,表示 data 记录数据的类型和长度
- data,记录的实际数据
- len,记录(encode 占用字节数 + data 占用字节数)
encode
- 0xxx xxxx,当最高位为 0 时,表示 7 位的无符号整数,由低 7 位组成数据。此时不会有 data 字段。
- 10xx xxxx,当最高两为 10 时,表示这是一个字符串,字符串长度由低 6 位记录。实际字符串会记录在后续的 data 字段中。
- 110x xxxx,当最高三位为 110 时,表示这是一个 13 位整数,数据由后续的 13 位组成。
- 1110 xxxx,当最高四位为 1110 时,表示这是一个字符串,字符串长度由后续的 12 位组成,字符串记录在 data 字段中。
- 1111 0000,当最高八位为 1111 0000 时,表示这是一个字符串,字符串长度由后续 32 位组成,字符串记录在data字段中。
- 1111 0001,当最高八位为 1111 0001 时,表示一个 16 位整数,数据记录在后续 2 个字节的 data 中。
- 1111 0002,当最高八位为 1111 0002 时,表示一个 24 位整数,数据记录在后续 3 个字节的 data 中。
- 1111 0003,当最高八位为 1111 0003 时,表示一个 32 位整数,数据记录在后续 4 个字节的 data 中。
- 1111 0004,当最高八位为 1111 0004 时,表示一个 64 位整数,数据记录在后续 8 个字节的 data 中。
len
len 是一个变长的部分,最多占用 5 个字节。len 里面每个字节的第一位都是标识符,如果是 0 的话,就表示这个字节是 len 部分的最后一个字节;如果是 1 的话,就不是。也就是说,每个字节的剩余 7 位才携带有效信息。
但是 len 在内存空间中是逆序存储的,也就是实际存储是这样的
举个例子,假如现在长度为 1000。
为什么要这么存储,我们将在稍后解释。
ziplist 的异同
我们可以发现 ziplist 和 listpack 的头部都会记录总共字节数,总元素数量;尾部都使用 255 作为结束标识符。
但是 ziplist 还会在头部中记录 zltail,表示最后一个元素的偏移量,这样可以直接定位到最后一个元素,然后实现从后往前遍历的操作。为什么在 listpack 中则不需要记录呢?
在 entry 中,zipList 记录的是 prevlen,是前一个元素的长度;而 listpack 记录的是 len,是元素本身的长度。
在 ziplist 中是通过 zltail + prevlen 属性实现了从后往前遍历的。而 listpack 怎么才能实现从后往前遍历呢?
其实很简单,我们知道 lpbyte 表示总字节数,我们可以通过 lpbyte 直接定位到内存空间的最后面。然后还知道尾部使用 1 字节标识符。那么我们 lpbyte - 1,就可以定位到最后一个元素的最末位,P 所指向的地方。
通过上一节我们知道,entry 的最后面存储的是该 entry 的长度,并且采用逆序存储,我们只需要再往左继续读取字节,直到读到一个最高位为 0 的字节,则表示长度 len 读取完成,可以计算出长度 len,长度表示的是 encode + data 的字节数。我们继续往左边读取 len 字节,就可以定位到 entry 的开头处。
依次类推,可以实现从后往前遍历。
优点
- listpack 使用了紧凑型内存,可以充分利用 CPU 高速缓存和节省内存
- 避免了 ziplist 的连锁更新。