底层:压缩列表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中。