利用双向循环链表实现长整数的存储_Redis数据结构整数集合/压缩列表

一、回顾

通过对Redis的数据结构学习,简单回顾一下数据结构在Redis中的实现有哪些?

简单动态字符串(SDS),它用于Redis字符串键值对的底层实现。

链表,它是列表键的底层实现之一,以及发布、订阅、慢查询、监视器等。

字典,它是哈希键的底层实现之一,以及Redis数据库的底层实现。

跳跃表,它是有序集合键的底层实现之一。

通过以上的数据结构学习,基本上覆盖了Redis中所支持的五大数据类型,字符串、列表、哈希、集合、有序集合的底层实现。

那么今天,要继续学习的是,Redis中的另外两个数据结构。

整数集合,它是集合键的底层实现之一。

压缩列表,它是列表键和哈希键的底层实现之一。

70ba87f0571a6421cb4b2b4ececb6500.png

二、整数集合

整数集合是Redis用于保存整数值的集合抽象数据结构。它支持编码为 int16、int32、int64 的整数值。存储元素的值必须唯一。

# 每个 intset.h/intset结构表示一个整数集合typedef struct intset {    # 编码方式    uint32_t encoding;    # 集合包含的元素数量    uint32_t length;    # 保存元素的数组    int8_t contents[];}intset;

contents 数组是整数集合的底层实现,整数集合的每个元素都是contents数组的一个数组项。各个项从小到大的排序,并且每个项元素值唯一。

length 属性记录了整数集合包含元素的数量,即contents的长度。

encoding 属性的值为 INTSET_ENC_INT16、INTSET_ENC_INT32,INTSET_ENC_INT64

如果encoding 的值为INTSET_ENC_INT16,则 contents 就是一个 int16_t类型的数组,数组内都是 int16_t类型的整数值。范围(-32768 至 32767)

如果encoding 的值为INTSET_ENC_INT32,则 contents 就是一个 int32_t类型的数组,数组内都是 int32_t类型的整数值。范围(-2147483648 至 2147483647)

如果encoding 的值为INTSET_ENC_INT64,则 contents 就是一个 int64_t类型的数组,数组内都是 int64_t类型的整数值(-9223372036854775808 至 9223372036854775807)

8942ba6cf638d457d11328ae9a8aac6f.png

例如上图是一个整数集合,集合中的编码为 int16_t 所以,contents 的数组项的大小需要在编码为 int16 的范围之内。

三、整数集合-升级

假设我们有一个整数集合,并且这个整数集合的编码为 int16_t

此时,我们需要将一个 int32_t 的数据项存储到该整数集合中。应该发生哪些动作呢?

1)对整数集合进行升级

2)再将新元素添加到整数集合中

3.1 升级整数集合一般需要三个步骤

  • 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间

  • 将底层数组现有的元素都转换成新元素相同的类型,再将转换后的元素存储到正确的位置上,并从小到大排列

  • 将新元素添加到底层数组中

因为每次向整数集合中添加新的元素都有可能发生升级,而每次升级都需要对底层数组中的元素进行类型转换。所以,整数集合添加新元素需要升级的时间复杂度为O(n) 。

3.2 升级的好处

  • 提升整数集合的灵活性

因为,C语言是静态类型语言,为了避免类型错误,不支持两种不同数据类型的值放在同一个数据结构中。

在整数集合中,就可以将多种类型元素,存储到同一个整数集合中。因为整数集合底层采用类型升级方案,即便你使用 int16 位、还是 32、64位的编码方式,都可通过向上升级的方式解决类型不一致的问题。

  • 尽可能的节约内存

因为,整数集合可以同时保存三种不同类型的值。又可以确保升级操作,只会在有需要的时候进行升级,这可以尽量节省内存。

3.3 重点回顾

  • 整数集合是集合键的底层实现之一。

  • 整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在有需要时,会对数组进行类型升级。

  • 升级操作为整数集合带来了操作上的灵活性,并尽可能的节约了内存。

  • 整数集合只支持升级操作,不支持降级操作。

四、压缩列表

压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。和数组数据结构不同的是,大小可变、类型可变。

列表键的底层实现之一就有链表,那么,为什么有了链表数据结构,还会有一个压缩列表呢?因为,节省内存呀!

链表中的每个节点都需要维护指针,相当占用内存。如果列表中存储的元素比较少,字符串也比较短,如果采用了链表作为底层实现,是没有必要的。

压缩列表最擅长的是,存储字符串相对较短,元素个数相对较少的数据场景。毕竟,压缩列表底层存储的是一块连续的内存空间,除了所存储的元素以为,没有任何多余的空闲空间。

所以当列表键和哈希键只包含少量键值对,并且每个键值对都是小整数或长度较短的字符串时,就可以采用压缩列表来作为底层实现。

4.1 压缩列表结构

# 压缩列表数据结构struct ziplist {    # 整个压缩列表占用的字节数    int32 zlbytes;    # 最后一个元素距离压缩列表起始位置的偏移量,用户快速定位最后一个节点    int32 zlbail_offset;    # 元素个数    int16 zllength;    # 元素内容列表、依次紧凑存储    T[] entries;    # 标志压缩列表的结束,值为OxFF    int8 zlend;}

c93009a35c0fa531d8b00e585cdfc462.png

一个压缩列表可以包含任意多个节点,每个节点保存一个字节数组或一个整数值。

zlbytes 属性记录着整个压缩列表,占用的内存字节数,对压缩列表进行重分配,或计算zlend的位置时使用。

zltail 属性记录压缩列表,表尾节点距离压缩列表的起始地址有多少字节,确定表尾节点地址。

zllen 属性记录压缩列表,包含节点的数量,当这个属性值小于UINT16_MAX(65535)时可信,否则需要遍历整个压缩列表获得最新的长度。

entryX  属性是压缩列表,包含的各个节点,节点的长度由节点保存的内容决定。

zlend 属性特殊值0xff(十进制255) 用于标记压缩列表的末端。

4.2 压缩列表-节点

每个压缩列表节点都包含以下三个部分:

struct entry {    # 前一个entry的字节长度    int prevlen;    # 元素类型编码    int encoding;    # 元素内容    optional byte[] content;}

731c3b3c439e0e07a58335cadee152e1.png

1、previous_entry_length

previous_entry_length 属性以字节为单位,记录了压缩列表中前一个节点的长度。

previous_entry_length 的属性值的长度可以是 1 字节  也可以是 5 字节。

如果前一个节点的长度小于 254 则 length 的属性值是 1 个字节。

如果前一个节点的长度大于 254 则 length 的属性值是 5 个字节。

2、encoding

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

一个字节、两个字节或五个字节的最高位是 00、01、10 的字节数组编码。它意味着content 属性保存着字节数组。数组的长度由编码除去最高两位之后的其他位记录。

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

3、content

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

4.3 区分节点 数组、整数

压缩列表中,所包含的多个节点,可以是一个长度受限的字符串数组(非\0结尾的字符串数组)也可以是一个整数。那怎么区分这个节点存储的是数组还是整数呢?

字符数组可以是以下三种长度的其中一种

  • 长度小于等于 63 字节的字符数组

  • 长度小于等于 16383 字节的字符数组

  • 长度小于等于 4294967295 字节的字符数组

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

  • 4 位长,介于 0 至 12 之间的无符号整数

  • 1 字节长,有符合整数

  • 3 字节长,有符合整数

  • int16_t 类型整数

  • int32_t 类型整数

  • int64_t 类型整数

4.4 倒序遍历

因为每个压缩列表的节点中都存储着一个 previous_entry_length 属性。所以程序可以通过指针进行运算,根据当前节点的起始位置来计算出前一个节点的起始地址。

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

所以压缩列表的为了支持双向遍历,通过 ztail_offset 这个字段来找到最后一个节点的地址,在通过上面的公式找到前一个节点地址,以此方法来进行倒序遍历。

4.5、级联更新

每个节点都有一个 previous_entry_length 属性,它以字节为单位,并且是变长的,以253为临界点,大于253用 5 个字节表示,小于或等于 则用 1 个字节表示。如果由 253 变成了 254 则会发生级联更新。

如果每个节点都恰好存储了253个字节内容,那么修改第一个节点内容仍旧会导致后续的节点发生级联更新,

删除中间节点也会发生级联更新

4.6 重点回顾

  • 压缩类别是为了节省内存而开发的顺序型数据结构

  • 列表键和哈希键的底层实现之一就是压缩列表

  • 一个压缩列表中包含了多个节点,每个节点可以保存一个字节数组或者整数值

  • 添加新节点到压缩列表,或从中删除节点会引发级联更新,但是概率比较小

五、参考文献

《Redis 设计与实现》

《Redis 深度历险》

36edacf7d712460102c395d96951ade7.gif

c93d2bdb44a8c6a4537293e57f20eeed.gif

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值