一 数据结构与对象
5 整数集合
整数集合是集合键的底层实现之一,当一个集合键只包括整数值元素,且数量不多时,Redis就会使用整数集合做为集合的底层实现
5.1 整数集合的实现
typedef struct intset{
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
}intset;
- contents数组用于保存集合数据,并且按照从小到大的顺序排放,并且数组不包含任何重复项。
- length记录数据个数,即数组的长度。
- contents数组输入使用int8_t,但实际的数据类型由encoding来决定。encoding决定底层实现
- encoding的数据类型,由集合中字节数最大的数来决定,这便使得整数集合有升级。
5.2 升级
当新添加一个数到集合时,数的类型比encoding大时,整数集合需要先进行升级,然后再添加数据。
步骤:
- 根据新元素类型,拓展整数集合底层数组的空间大小,并为新元素分配空间。
- 将所有数据转化为新的数据类型,并按顺序放在到正确的位置,一般先移动后面的数,这样移动复杂度为O(N)。
- 将新元素添加进来
- 会引起升级操作的数必定小于或大于集合中的所有数,所有引起升级的数必定放在集合开头或末尾。
5.3 升级的好处
整数集合的升级策略有两个好处,一个是提升整数集合的灵活性,另一个是尽可能的节约内存。
5.3.1 提升灵活性
通过集合自动升级底层数组来适应新元素,所以可以将多种类型元素放进去,而不用担心出现类型错误
5.3.2 节约内存
要想保存所有类型的值,最简单的做法就是直接使用int64_t,不过这样就容易出现内存浪费的情况。
5.4 降级
整数集合,暂时不支持降级功能。
6 压缩列表
压缩列表是列表键和哈希键的底层实现之一,当一个列表键只包含少量列表项,并且每个列表项是小整数值,或长度较短的字符串时,那么就会使用压缩列表来做底层实现
6.1 压缩列表的构成
压缩列表是为了节约内存而开发的,由一系列特殊编码的连续内存块组成的顺序性数据结构,一个压缩列表包含任意多个节点,每个节点保存一个字节数组或一个整数值。
压缩列表的组成部分
zlbytes | zltail | zllen | entry1 | entry2 | … | entryN | zlend |
---|
各部分说明:
属性 | 类型 | 长度 | 用途 |
---|---|---|---|
zlbytes | uint32_t | 4byte | 记录整个压缩列表所占用的字节数,用作内存重分配和计算zlend的位置使用 |
zltail | uint32_t | 4byte | 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,无需遍历整个压缩列表就能确定表尾节点的地址。 |
zllen | uint16_t | 2byte | 记录压缩列表的节点个数,当列表节点数量小于UINT16_MAX(65535)时,这个zllen的值就是节点个数,若大于这个值,zllen存放不下,节点的个数需要遍历整个列表 |
entryX | 列表节点 | 不定 | 节点长度由节点保存的内容决定 |
zlend | uint8_t | 1byte | 特殊值0xFF(十进制255),用于标记压缩列表的末尾 |
6.2压缩列表节点的构成
每个压缩列表节点可以保存一个字节数组或一个整数值,其中字节数组可以是以下三种长度之一
- 长度小于等于63(2^6-1)字节的字节数组
- 长度小于等于16383(2^14-1)字节的字节数组
- 长度小于等于4294967295(2^32-1)字节的字节数组
整数值可以是以下六种长度之一
- 4位长,0-12之间的数
- 1byte长的有符号数
- 3byte长的有符号数
- int16_t类型整数
- int32_t类型整数
- int64_t类型整数
每个压缩列表节点由以下三部分组成
previous_entry_length | encoding | content |
---|
6.2.1 previous_entry_length
previous_entry_length属性以字节为单位,可以说1byte,也可以是5byte,记录了压缩列表前一个节点的长度
- 如果前一个节点的长度小于254byte,那么previous_entry_length属性为1byte,保存前一个节点的具体长度。
- 如果前一个节点的长度大于等于254byte,那么previous_entry_length属性的长度为5byte,其中previous_entry_length属性的第一个字节被设置为0XFE(十进制254),之后的四个字节保存前一个节点的长度。
压缩列表从表尾向表头遍历,我们只需要拥有一个指向某个节点起始位置的指针,接着凭借previous_entry_length属性就能一直往前遍历。
6.2.2 encoding
encoding属性记录节点中content属性所保存数据的类型及长度,最前方两位用于区分content是自己数组还是整数。
- 00,01,10开头分别表示encoding的长度为1byte,2byte,5byte长,且content是一个字节数组,除开最前面两位用于标记,后面的剩余位表示content的字节数组的长度。
- 11开头的表示content是一个整数值,除开前方两位,后面六位用于标记整数值的长度和编码。
字节数组编码
encoding编码 | encoding编码长度 | content属性的值 |
---|---|---|
00aaaaaa | 1byte | 长度小于等于63字节的字节数组 |
01aaaaaa bbbbbbbb | 2byte | 长度小于等于16386字节的字节数组 |
10aaaaaa bbbbbbbb cccccccc dddddddd | 5byte | 长度小于等于4294967295字节的字节数组 |
整数编码
encoding编码 | encoding编码长度 | content属性的值 |
---|---|---|
11000000 | 1byte | int16_t类型的整数 |
11010000 | 1byte | int32_t类型的整数 |
11100000 | 1byte | int64_t类型的整数 |
11110000 | 1byte | 24位有符号整数 |
11111110 | 1byte | 8位有符号整数 |
1111xxxx(0000-1100) | 1byte | 并没有对应的content属性,编码本身的xxxx保存的就是值,一个0-12的整数 |
6.2.3 content
根据encoding确定类型的一个值。
6.3 连锁更新
压缩列表从表尾向表头遍历,利用节点中的previous_entry_length属性
- 若前一个节点长度小于254byte,那么previous_entry_length长为1byte
- 若前一个节点长度大于等于254byte,那么previous_entry_length长为5byte
zlbytes | zltail | zllen | entry1 | entry2 | … | entryN | zlend |
---|
现在假设一种情况,entry1到entryN节点长度都介于250byte到253byte之间,则这些节点的previous_entry_length都是1byte。这时一个长度大于等于254byte的新节点new设置到表头。
zlbytes | zltail | zllen | new | entry1 | entry2 | … | entryN | zlend |
---|
因为entry1的previous_entry_length属性只有1byte,无法保存new的长度,则需要进行空间重分配,将entry1的previous_entry_length属性变成5字节。
接着entry1的长度也就变得大于等于254byte了,entry2的previous_entry_length属性只有1byte,无法保存entry1,也需要进行空间重分配。如此,后面的的节点全部需要进行内存重分配。
Redis将这种特殊情况下产生的连续多次内存重分配称为“连锁更新”。而类似添加新节点会产生连锁更新,删除节点时,也同样可能引起连续性的previous_entry_length属性的大小需要从5byte变成1byte的情况,这也是连锁更新。
一次空间重分配的复杂度最坏可以达到O(N),所以连锁更新的复杂度最坏可以达到O(N^2),但尽管连锁更新的复杂度很高,但真正造成性能问题的概率很小
- 一方面,压缩列表要恰好有多个连续,长度在特点范围内的节点,连锁更新才能被触发,实际中,这样的情况并不多见
- 另一方面,及时出现连锁更新,若连锁更新的节点不多,就不会对性能造成任何影响。