Redis源码分析--- 压缩列表

Redis源码分析--- 压缩列表

压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。

列表里面包含的都是1、3、5、10086这样的小整数值,以及‘hello’、‘world’这样的短字符串。

另外当一个哈希键只包含少量键值对,并且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做哈希键的底层实现。

   压缩列表的构成

压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存组成的顺序型数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。

图7-2展示了一个压缩列表示例:

列表zlbytes属性的值为0x50(十进制80),表示压缩列表的总长为80字节。

列表zltail属性的值为0x3c(十进制60),这表示如果我们有一个指向压缩列表起始地址的指针p,那么只要用指针p加上偏移量60,就可以计算出表尾节点的entry3的地址。

列表zllen属性的值为0x3(十进制3),表示压缩列表包含三个节点。

列表zlbytes属性的值为0xd2(十进制210),表示压缩列表的总长为210字节。

列表zltail属性的值为0xb3(十进制179),这表示如果我们有一个指向压缩列表起始指针p,那么只要用指针p加上偏移量179,就可以计算出表尾节点entry5的地址。

列表zllen属性的值为0x5(十进制5),表示压缩列表包含五个节点。

   压缩列表节点的构成

每个压缩列表可以保存一个字节数组或者一个整数,其中,字节数组可以是以下三种长度的其中一种:

1)长度小于等于63(2^6 - 1 )字节的字节数组;

2)长度小于等于16383(2^14 - 1)字节的字节数组;

3)长度小于等于4294967295(2^32 - 1)字节的字节数组

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

1)4位长,介于-至12之间的无符号整数

2)1字节长的有符号整数

3)3字节长的有符号整数

4)int16_t类型整数

5)int32_t类型整数

6)int64_t类型整数

每个压缩列表节点都由previous_entry_length、encoding、content三个部分组成,如图7-4所示;

(1)prevoius_entry_length

节点的previous_entry_length属性以及字节为单位,记录了压缩列表中前一个节点的长度。previous_entry_length属性的长度可以是1字节或者5字节。

如果前一节点的长度小于254字节,那么previous_entry_length属性的长度为1字节:前一节点的长度就保存在这个字节里面。

如果前一节点的长度大于等于254字节,那么previous_entry_length属性的长度为5字节:其中属性的第一个字节会被设置为0xFE(十进制254),而之后的四个字节则用于保存前一个节点的长度。

图7-5展示了一个包含一字节长previous_entry_length属性的压缩列表节点,属性值为0x05,表示前一节点的长度为5字节。

图7-6展示了一个包含五字节长度previous_entry_length属性的压缩节点,属性的值为0xFE00002766,其中值的最高位字节0xFE表示这是一个五字节长的previous_entry_length属性,而之后的四字节0x00002766(十进制10086)才是前一节点的实际长度。

因为节点previous_entry_length属性记录了前一个节点的实际长度,所以程序可以通过指针运算,根据当前节点的起始地址计算出前一个节点的起始地址。

举个例子,如果我们有一个指向当前节点起始地址的指针C,那我们只要用指针c减去当前节点previous_entry_length属性的值,就可以得出一个指向前一个节点起始地址的指针P.

压缩列表的从表尾向表头遍历操作就是这一原理实现的,只要我们有一个指向某个节点的其实地址的指针,那么通过这个指针以及这个节点的previous_entry_length属性,程序就可以一直向前一个节点回溯,最终到达压缩列表的表头节点。

(2)encoding

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

一字节、两字节或者五字节长,值的最高位为00、01或者10的是字节数组的编码:这种编码表示节点的content属性保存着字节数组,数组的长度由编码去除最高位两位之后的其他位记录;

一字节长,值的最高位以11开头的是整数编码:这种编码表示节点的content属性保存着整数值,整数值的类型和长度由编码去除最高位两位之后的其他位记录。

(3)content

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

   连锁更新

前面说过,每个节点的previous_entry_length属性都记录前一个节点的长度:

如果前一个节点的长度小于254字节,那么privious_entry_length属性需要用1字节长度的空间来保存这个长度值。

如果前一个节点的长度大于等于254字节,那么previous_entry_length属性需要用5字节长的空间来保存这个长度值。

Redis将这种在特殊情况下产生的连续多次空间扩展操作称之为“连锁更新”。

除了添加新节点可能会引发连锁更新之外,删除节点也可能会引发连锁更新。

因为连锁更新在最坏情况下需要对压缩列表执行N次空间重新分配操作,而每次空间重新分配的最坏复杂度为O(N),所以连锁更新的最坏复杂度为O(N^2)。

要注意的是,尽管连锁更新的复杂度较高,但是它真正造成性能问题的几率是很低的:

首先,压缩列表里要恰好有多个连续的、长度介于250字节至253字节之间的额节点,连锁更新才有可能被引发,在实际中,这种情况并不多见。

其次,即使出现连锁更新,但是只要更新的节点数量不多,就不会对性能造成任何影响:比如说对三五个节点进行连锁更新是绝对不会影响性能的。

因为以上原因,ziplistPush等命令的平均复杂度仅为O(N),在实际中,我们可以放心的使用这些函数,而不必担心连锁更新会影响压缩列表的性能。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值