redis数据结构与持久化(Redis数据结构-整数集合)(1)

一、回顾

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

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

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

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

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

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

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

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

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

redis数据结构与持久化(Redis数据结构-整数集合)(2)

二、整数集合

整数集合是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)

redis数据结构与持久化(Redis数据结构-整数集合)(3)

例如上图是一个整数集合,集合中的编码为 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 { #整个压缩列表占用的字节数 int32zlbytes; #最后一个元素距离压缩列表起始位置的偏移量,用户快速定位最后一个节点 int32 zlbail_offset; #元素个数 int16 zllength; #元素内容列表、依次紧凑存储 T[] entries; #标志压缩列表的结束,值为OxFF int8 zlend; }

redis数据结构与持久化(Redis数据结构-整数集合)(4)

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

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

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

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

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

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

4.2 压缩列表-节点

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

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

redis数据结构与持久化(Redis数据结构-整数集合)(5)

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结尾的字符串数组)也可以是一个整数。那怎么区分这个节点存储的是数组还是整数呢?

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

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

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 深度历险》

,