一、回顾
通过对Redis的数据结构学习,简单回顾一下数据结构在Redis中的实现有哪些?
简单动态字符串(SDS),它用于Redis字符串键值对的底层实现。
链表,它是列表键的底层实现之一,以及发布、订阅、慢查询、监视器等。
字典,它是哈希键的底层实现之一,以及Redis数据库的底层实现。
跳跃表,它是有序集合键的底层实现之一。
通过以上的数据结构学习,基本上覆盖了Redis中所支持的五大数据类型,字符串、列表、哈希、集合、有序集合的底层实现。
那么今天,要继续学习的是,Redis中的另外两个数据结构。
整数集合,它是集合键的底层实现之一。
压缩列表,它是列表键和哈希键的底层实现之一。
二、整数集合
整数集合是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)
例如上图是一个整数集合,集合中的编码为 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;}
一个压缩列表可以包含任意多个节点,每个节点保存一个字节数组或一个整数值。
zlbytes 属性记录着整个压缩列表,占用的内存字节数,对压缩列表进行重分配,或计算zlend的位置时使用。
zltail 属性记录压缩列表,表尾节点距离压缩列表的起始地址有多少字节,确定表尾节点地址。
zllen 属性记录压缩列表,包含节点的数量,当这个属性值小于UINT16_MAX(65535)时可信,否则需要遍历整个压缩列表获得最新的长度。
entryX 属性是压缩列表,包含的各个节点,节点的长度由节点保存的内容决定。
zlend 属性特殊值0xff(十进制255) 用于标记压缩列表的末端。
4.2 压缩列表-节点
每个压缩列表节点都包含以下三个部分:
struct entry { # 前一个entry的字节长度 int prevlen; # 元素类型编码 int encoding; # 元素内容 optional byte[] content;}
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 深度历险》