内存映射数据结构
内存映射数据结构是一系列经过特殊编码的字节序列,创建它们所消耗的内存通常比作用类似的内部数据结构要少得多,如果使用得 当,内存映射数据结构可以为用户节省大量的内存。不过,因为内存映射数据结构的编码和操作方式要比内部数据结构要复杂得多,所以内存映射数据结构所占用的 CPU 时间会比作用类似 的内部数据结构要多。
整数集合
整数集合是Redis用于保存整数值的集合抽象数据结构,它可以保存的类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。
整数集合(intset)是集合键的底层实现之一: 当一个集合只包含整数值元素, 并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现。
整数集合定义在intset.h文件中
具体定义:
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
复制代码
各属性说明如下
-
contents:contents数组是整数集合的底层实现,整数集合的每个元素都是contents数组的一个数组项(item),各个项在数组中按值的大小从小到大有序的排列,并且数组中不包含任何重复项;
-
length:整数集合包含的元素数量;
-
encoding:contents数组中元素类型(编码),例如:
- encoding为INTSET_ENC_INT16,那么contents就是一个int16_t类型的数组,数组里每个项都是int16_t类型的整数值;
- encoding为INTSET_ENC_INT32,那么contents就是一个int32_t类型的数组,数组里每个项都是int32_t类型的整数值;
- encoding为INTSET_ENC_INT64,那么contents就是一个int64_t类型的数组,数组里每个项都是int64_t类型的整数值;
注意:contens数组虽然声明为int8_t类型,但其实真正的类型取决于encoding的值
整数集合的升级
假设我们有一个整数集合,他现在存储的都是int16_t类型的整数,而现在我们需要向其中添加一个int32_t类型的整数。
这个时候因为C语言是静态性语言,我们不能将两种不同类型的值放在同一个数据结构中,这样会带来类型错误。
所以我们就需要将原有的数据结构做相应的改变,让他能存储更大的整数,同时将原有的整数类型也改变为更大的类型,这个过程就是整数集合的升级(upgrade)
升级的好处:
-
因为整数集合可以自动升级,所以我们可以随意的往里面存放int16_t、int32_t、int64_t等不同类型的整数,而不会发生类型错误。
-
节约内存,因为如果没有升级,我们要在一个数组里面存放不同类型的数,就只能在一开始把数组定义成int64_t类型的,而有时候我们其实只在里面存储了int16_t类型的,就造成了内存浪费。而有了升级功能以后,我们就可以在需要的时候在去扩展内存,节约了资源。整数集合只支持升级,无法降级。
总结
- 整数集合是集合键的底层实现之一。
- 整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的类型,改变这个数组的类型。
- 升级操作为整数集合带来了操作上的灵活性,并且尽可能的节约了内存。
- 整数集合只支持升级操作,不支持降级操作。
压缩列表
当列表项或者哈希键的键和值是一些小整数或是长度较短的字符串时,redis就使用压缩列表作为列表键的底层实现。
压缩列表的实现
-
压缩列表,压缩,就是为了节约内存。
-
它是由一系列特殊编码的连续内存块组成的顺序型数据结构。
-
一个压缩列表可包含任意多个节点,每个节点可保存一个字节数组或一个整数值。
压缩列表结构
一个压缩列表由以下几个部分组成:
zlbytes、zltail、zllen、N个entry以及zlend
节点构成
每个节点由previous_entry_length
、encoding
和content
三部分组成。
previous_entry_length:记录前一个节点长度,单位字节。
使用
-
通过该部分的取值,可以计算前一个节点的起始地址:当前节点起始地址减去该部分记录的值。
-
因此从表尾到表头遍历的原理就是基于此,不断计算前一个节点的起始地址,直到到达表头。
encoding:记录content的类型和长度。
-
长度为1字节、2字节或5字节时
-
值的最高位为00,01或10时的编码,表示content保存的是字节数组。
-
数组长度就是去掉前两位之后的记录。
-
如:00001011, 长度为1字节,高两位为00,所以content保存的是字节数组,后6位001011表名content长度为11,如content为:“wmlwmlwmlww”
-
-
长度为1字节时
- 值的最高位为11,则表示content保存的是整数值。长度为去掉最高位的两个,其他位的记录。
content:保存节点的值(字节数组或整数)
连锁更新
- 添加节点导致更新
如上,假如节点entry1到entryN长度都小于254字节,则每个节点的previous_entry_length属性都使用了1字节空间保存前一个节点的长度。
而如果此时新增一个长度大于254字节的节点new,那么entry1原本的1字节长度就无法保存new的长度,就需要进行空间重分配,扩展到5字节。
同样的,entry2也因为entry1变为5字节而进行扩展,这样一直到entryN都要进行空间重分配。这就是连锁更新现象。
- 删除节点导致连锁更新
假如big节点大于254字节,则small使用1字节存储其长度,后面N个entry同上面一样都使用1字节存储长度。
假如此时删除了节点small,则entry1的前一个节点变成big,因为大于254字节,所以entry1需要扩展都5字节,就会出现和4.1一样的连锁更新操作
分析:
最坏情况下需要对压缩列表进行N次空间重分配,每次重分配的最坏复杂度为O(N),连锁更新的最坏复杂度为O(N²)。
但是因为进行连锁更新,要求列表由连续多个长度在250到253字节的节点,这种情况并不多见。
且就算出现连锁更新,更新节点不多也基本不会对性能造成影响,像这种整体全部更新的情况也是更少。
所以基本不用担心它的性能问题。
总结:
①压缩列表是一种为节约内存而开发的顺序性数据结构
②压缩列表被用作列表键和哈希键的底层实现。(redis3.0改为了quicklist)有序集合也采用了压缩列表。
③压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值
④添加新节点到压缩列表或者从压缩列表中删除几点,可能会引发连锁更新操作,但这种操作出现几率不高。这种操作最坏的时间复杂度为On^2