06 Redis-压缩列表

本文内容均来自《Redis设计与实现》一书 

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

1.压缩列表

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

结构 

zlbytes(4字节):记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分配或者计算zlend位置时使用。

zltail(4字节):记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,程序无需遍历整个压缩列表就可以确定表尾节点的位置。

zllen(2字节):记录压缩列表包含的节点数量,当这个属性的值小于UINT16_MAX(65535)时,这个属性的值就是压缩列表包含的节点数量,当这个值等于UINT64_MAX时,节点的真实数量需要遍历整个压缩列表才能计算出。

entryX(不定):压缩列表包含的各个节点,节点的长度由节点保存的内容决定。

zlend(1字节):特殊值0xFF,用于标记压缩列表的末端。

2.压缩列表节点

每个压缩列表节点可以保存一个字节数组或者一个整数值。

字节数组的长度(最大字节数):2^6-1字节、2^14-1字节、2^32-1字节。

整数值的长度:4位无符号整数、1字节有符号整数、3字节有符号整数、int16_t整数、int32_t整数、int64_t整数。

结构

 previous_entry_length:记录压缩列表中前一个节点的长度。长度可以是1字节或5字节。

  • 如果前一节点的长度小于254字节,那么previous_entry_length属性的长度为1,前一节点的长度就保存在这一个字节里面。
  • 如果前一节点的长度大于等于254字节,那么previous_entry_length属性的长度为5,其中属性的第一字节会被设置为0xFE(十进制值254),而之后的4个字节用于保存前一节点的长度。

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

  • 前一个节点的起始地址 = 当前节点的起始地址 - 当前节点的previous_entry_length的值

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

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

  • 1字节、2字节或者5字节,值的最高位为00、01或者10的是字节数组编码:这种编码表示节点的content属性保存着字节数组,数组的长度由编码除去最高两位之后的其他位记录。
  • 1字节长,值得最高为以11开头的是整数编码:这种编码表示节点的content属性保存着整数值,整数值的类型和长度由编码除去最高两位之后的其他位记录。

content:记录节点的值,可以是一个字节数组或整数,值的类型和长度由节点的encoding属性决定。

3.连锁更新

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

示例

假设在一个压缩列表中,有多个连续的、长度介于250字节到253字节之间的节点e1至eN:

因为e1至eN的所有节点的长度都小于254字节,所以e1至eN的所有节点的previous_entry_length属性都是1字节长,如果我们将一个长度大于254字节的新节点new设置为压缩列表的表头节点,那么new将成为e1的前置节点:

因为e1的previous_entry_length属性为1字节,它没办法保存新的节点new的长度,所以程序将对压缩列表执行空间重分配操作,并将e1的previous_entry_length属性从原来的1字节扩展为5字节。在e1的previous_entry_length属性扩展为5字节后,e1的长度就大于254字节了,此时e2的previous_entry_length的1字节就无法保存新的e1的长度,同理需要对e2进行扩展。为了让每个节点的previous_entry_length属性都符合压缩列表对节点的要求,程序需要不断地压缩列表执行空间重分配操作,直到eN为止。

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

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

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

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

  • 压缩列表里恰好有多个连续的、长度介于250字节和253字节之间的节点,连锁更新才有可能被引发,在实际中,这种情况并不多见。
  • 即使出现连锁更新,但只要被更新的节点数量不多,就不会对性能造成任何影响。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值