Redis-ziplist压缩列表底层实现

606 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

概览

前文提到,Redis 在有序集合和哈希的数据规模较小时,底层会采用 ziplist 压缩列表进行数据存储。

当有序集合的数据规模满足以下条件时,会使用 ziplist 作为底层数据结构;

当哈希的数据规模满足以下条件时,会使用 ziplist 作为底层数据结构;

可以在 redis.conf 配置文件中配置该属性。

压缩列表 ziplist 作为 Redis 底层核心的数据结构,重点在于如何节省内存空间,以及提升内存操作效率,下文会详细说明它的实现原理。

原理

压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构,有点类似 java 中 ArrayList 的底层数组实现,能够很好地利用空间局部性提升内存访问效率。

另外它通过对数据的动态编码,能够很大程度节省内存使用,这对 Redis 来说非常重要。

压缩列表整体的数据结构如下:

  • zlbytes,长度为 4 字节,记录整个压缩列表占用的内存字节数,在进行内存重分配或者计算 zlend 位置时使用
  • zltail,长度为 4 字节,记录压缩列表尾节点距离起始位置的偏移字节数,可以通过它快速定位到尾结点,而无需遍历列表
  • zllen,长度为 2字节,记录了列表包含的节点数量,但需要注意的是当节点数量大于 65535 时,节点数量需要遍历整个列表才能计算得到
  • entry,长度不定,列表中的节点,长度由节点中保存的数据决定
  • zlend,长度 1 字节,特殊字符标识列表结尾

节点构成

前面提到压缩列表节点的长度是不定的,因为它可以根据具体存储的内容,动态地调整其占用的空间大小。

节点可以保存一个字节数组或者一个整数值,其中字节数组可以是以下长度的其中一种:

  • 长度小于等于63(2^6-1)字节的字节数组
  • 长度小于等于16383(2^14–1)字节的字节数组
  • 长度小于等于4294967295(2^32–1)字节的字节数组

整数值则可以是以下六种长度的其中一种:

  • 4位长,介于0至12之间的无符号整数
  • 1字节长的有符号整数
  • 3字节长的有符号整数
  • int16_t类型整数
  • int32_t类型整数
  • int64_t类型整数

节点的数据结构如下:

previous_entry_length属性记录了前一个节点的长度:

  • 如果前一个节点长度 < 254 字节,previous_entry_length 属性长度为 1 字节表示
  • 如果前一个节点长度 >= 254 字节,则 previous_entry_length 属性长度为 1 字节表示

通过 previous_entry_length 属性节点可以计算出前一个节点的起始地址,达到节点回溯的效果。

encoding属性记录了节点的content属性所保存数据的类型以及长度:

  • encodings属性为一字节长,值的最高位以11开头的是整数编码,即 content存储的是整数内容
  • encodings属性为一字节、两字节或者五字节长,值的最高位为00、01或者10的是字节数组编码,即content存储的是字符内容;数组的长度由编码除去最高两位之后的其他位记录

content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。

举个例子:

连锁更新

前面提到节点的 previous_entry_length 属性是变长的,会根据前一个节点的长度动态变化。

而连锁更新指的是极端情况下,由于节点的插入或者删除操作,导致列表中节点的前驱节点发生变化,进而影响 previous_entry_length 属性发生变化,最终导致多个节点长度发生变化的连锁反应。

因节点长度发生变化时需要执行空间的重分配,因此如果大面积发生连锁更新,对于 Redis 服务器的整体性能会有一定影响。

当然,尽管连锁更新的复杂度较高,但真正造成性能问题的概率还是很低的,原因如下:

  • 首先,压缩列表里要恰好有多个连续的、长度介于250字节至253字节之间的节点,连锁更新才有可能被引发,在实际中,这种情况并不多见(如果实际真的存在,应尽量避免)
  • 其次,即使出现连锁更新,但只要被更新的节点数量不多,就不会对性能造成任何影响,比如说,对三五个节点进行连锁更新是绝对不会影响性能的

因此实际的使用场景中,基本不必担心压缩列表的性能问题。

推荐阅读