Redis04:底层:压缩列表ziplist、intset、紧凑列表listpack

底层:压缩列表ziplist、intset、紧凑列表listpack

ziplist和它的级联更新

ziplist是hash类型和list类型在底层的实现之一。当list中只包含少量元素,且元素要么是小整数要么是短字符串时,此时list底层就采用ziplist的方式存储。当hash只保存少量键值对,而且每个键值对要么是小整数要么是短字符串时,此时hash底层就采用ziplist存储。

列表定义

压缩列表是redis为了节约内存而开发的,是由连续内存块组成的顺序型数据结构,一个压缩列表包含多个entry,每个entry可以保存一个字节数组或者一个整数值。

下图代表压缩列表的各组成部分和详细说明:

下图表示了一个压缩列表示例:

zlbytes的值是80,代表压缩列表的总长为80字节;zltail的值是60,代表从起始指针p开始偏移60,就能找到最后一个entry的位置;zllen的值是3,代表压缩列表有3个节点。

entry定义

entry分为三个部分:

previous_entry_length以字节为单位记录了前一个节点的长度,如果前一个节点的长度小于254字节,那么该字段就占1字节的空间,前一节点的长度就保存在这个字节中;如果前一个节点的长度大于等于254字节,那么该字段的就占5字节的空间,其中属性的第一个字节被设置为0xFE,也就是十进制254,之后的四个字节则用于保存前一个节点的长度。压缩列表根据这个字段就可以很轻松的从表尾节点遍历到表头节点。

encoding属性记录了节点保存的数据类型和长度,entry可以保存字节数组和整数,encoding的具体含义和content中保存的值的关系如下:

其中开头为11代表存的是整数,开头为00、01、10分别代表不同长度的字节数组,字节数组的具体长度由去掉前两位的编码来表示。如00001011中的00代表content中是一个长度小于等于63字节的字节数组,该数组的具体长度是001011,也就是11.

级联更新

级联更新是现有压缩列表构造的一个弊端。

这个弊端源于previous_entry_length属性的设定,这个属性可能占1字节,也可能占5字节,实际占用空间大小和前一个节点大小有关。如果在某个位置新插入一个较大的节点,或者删除一个大节点后的节点,可能就会导致后面节点的previous_entry_length属性由1字节变成5字节,而因为该属性的改变也会导致该entry大小的改变,从而可能引发该entry大于等于254字节,进而导致后面的entry大小继续改变。这个改变可能会波及到整个压缩列表,所以称之为级联更新。

级联更新在最坏情况下需要对压缩列表执行N次空间重分配操作,每次重分配的复杂度是ON,级联更新最坏复杂度为ON方。

虽然级联更新存在,但是出现概率很低,几乎可以忽略不计。

intset

intset是set类型的底层实现之一,当set只包含整数值元素,且数量不多时就会采用intset形式存储。

intset是一种用于保存整数值的集合,它可以保存类型为int16_t、int32_t、int64_t的整数值,并且保证集合中不会出现重复元素。

定义

intset结构定义如下:

typedef struct intset{
	//编码方式
	uint32_t encoding;
	//集合包含元素的数量
	uint32_t length;
	//保存元素的数组
	int8_t contents[];
}

contents是用来保存整数的,其中保存的数字从小到大排列,不含任何重复项。虽然contents被声明为int8_t,但是contents却不保存任何int8_t类型的值,它的真正类型取决于encoding的值,它可以保存类型为int16_t、int32_t、int64_t的整数值。

下图代表一个保存值类型为int16_t的,元素个数为5的intset,此时数组的大小是5*16=80位。

下图代表一个保存值类型为int64_t的,元素个数为4的intset,此时数组的大小是4*64=256位。

升级

当把一个新元素加入intset中时,如果intset当前的类型无法满足该类型所需要的空间时,intset就会执行升级操作。如当我们往intset中存入1、3、5时,此时intset底层存储的是int16_t类型,当我们存入一个很大的数,必须用int64_t类型表达的时候,此时contents数组保存的数值类型就会全部变为int64_t的。

升级的大致步骤如下:

1、根据元素的新类型,创造新的数组空间。

2、将底层数组的所有元素都转换为新类型,然后将转换后的元素移动到新数组中,在这个过程中保持有序性质不变。

3、最后将新元素添加到底层数组。

因为要添加的新值一定是突破原来类型的,所以它要么比现有的所有元素小,要么比现有的所有元素都大,所以新元素的位置一定是在数组的开头或者末尾。

升级的好处主要有两点:

1、通过底层类型的动态调整,我们可以将不同类型的数据放入redis中,而不必保持一种类型,灵活性很强。

2、节省空间。

值得注意的是,intset没有降级操作,即使引发升级的那个新元素被删除了,底层的编码方式还是保持不变。

listpack

listpack是对ziplist的改进,它比ziplist少了一个定位最后一个元素的属性(最后一个元素距离起始偏移量),listpack无需这个字段辅助逆序遍历。它的entry将长度字段放在了尾部,且记录的是本entry的长度,有了它就可以快速定位前一个entry的末尾,依然可以逆序遍历(在listpack定位到最后一个元素只需要总长度减去最后一个entry的长度即可)。这样本entry的属性不会出现在其他entry中了,消除了级联更新。但listpack目前只用在stream中。

原文地址:https://www.cnblogs.com/yinyunmoyi/p/11521385.html