写在前面
以下的知识都是建立在目前线上稳定版6.2.6版本的
而目前github上的redis源代码又更新优化了许多,譬如:hash数据类型默认的编码格式已经被替换为listpack,hashtable数据结构也被重新优化(哈希表对象dictht被废弃,字典dict直接引用dictEntry节点)
Redis hash数据类型-编码格式
redis中hash数据类型使用了两种编码格式:ziplist(压缩列表)、hashtable(哈希表)
具体使用哪种编码格式,由redis根据情况自己选择更合适的编码格式,这对于上层用户完全透明
- ziplist,又称为压缩列表,是hash数据类型默认使用的编码格式,当满足某种条件后,redis会将ziplist优化为hashtable编码格式,我们也可通过配置来更改达到条件的阈值
hash-max-ziplist-entries 512 //配置当field-value超过512时(合起来1024),使用hashtable编码 //至于为什么是1024,与ziplist有关,后面会讲述 hash-max-ziplist-value 64 //配置当key的单个field或value长度超过64时,使用hashtable编码
- hashtable,又称为哈希表,是hash数据类型中的数据过多或多长时由ziplist优化而来,底层数据结构与ziplist完全不同,且数据结构不可逆(也就是某个key的数据从ziplist演变为hashtable后,就算其数据减少到了ziplist可接受范围,也不会再从hashtable还原为ziplist格式)
ziplist
ziplist(压缩列表)是一个经过特殊编码的双向链表,用于存储字符串或整数,可以提高存储效率。
与普通双向链表的对比
- 内存开销不同,普通双向链表的元素一般需要两个指针,这会占用额外的内存,在数据较小的情况下,可能指针对于内存的占用比数据还大,有点得不偿失。而ziplist压缩列表,是一块连续的内存,数据之间紧密相连,不需要指针这种额外的开销。
- 遍历速度不同,普通链表内的元素一般是分开的,通过指针来查找到下一个元素。而ziplist是在一块连续内存上,所以内部元素遍历起来更快,并且由于ziplist独特的数据结构,它存储了末尾元素的偏移位置,所以它可以轻松定位到末尾元素,所以它获取首尾元素时所需的时间复杂度为O(1)。
ziplist的缺点
- 元素变动,虽然ziplist可以有效提高存储效率,可是这种数据项紧密相连的存储结构不擅长做修改操作,一旦元素发生变动,极大概率会引发内存重新分配造成数据拷贝,从而降低性能
- 数据项过多,当它数据项过多时,遍历查找指定元素也会耗时更久,所以这种数据结构也不宜存储过多的元素
因此,虽然redis精心设计了ziplist这种存储效率很高的数据结构,但是只会在适宜的场景下使用,当数据项过多或元素过长时,redis会将ziplist编码格式优化为更加合适的编码格式
ziplist数据结构
ziplist主要由下图几个部分组成,各部分所占字节如图所示:
字节存储的是16进制数据,最大值为0XFF,也就是255,而类似zlen字段占用两个字节,所能表述的最大值也就是FF FF,也就是2^16 - 1,也就是65535,当小于这个值时,zlen表示entry数据项的个数,当这个值为最大值时,zlen已经不能表示entry数据项的大小了,只能通过遍历可知。
entry数据项数据结构
entry数据项主要由下图几个部分构成:
我们来对其进行简单介绍:
1.首先是prevlen,它存储的是前一个数据项entry的长度,占用1或5个字节,可依据此字段进行反向遍历
- 如果长度在0XFE也就是254之内,则prevlen占用1个字节。
- 如果长度大于等于254,则prevlen占用5个字节。此时,第一个字节值设为0XFE(254),意味着使用后面更大的值,而剩余的4个字节用来表示前一个数据项的长度。(注:至于为什么不使用0XFF,也就是255,是因为255作为ziplist的结尾符,如果在遍历读取entry时,由于entry本身长度就不固定,那么此时就可能将其误认为结尾符,从而引发错误)
2.其次是encoding,这个字段相对复杂,它根据entry-data存入的是整数还是字符串会有不同的格式,所占的字节数也不相同,一般通过第一个字节就可确定数据项是整数还是字符串,整数的话,第一个字节的前2bit是11,否则为字符串
如果为字符串时,占用字节不固定
- 字符串长度小于64时,占用1个字节,二进制结构为00pppppp,后6bit表示长度,最高可表示2^6 - 1,也就是63
- 字符串长度小于16384时,占用2个字节,二进制结构为01pppppp qqqqqqqq,后14bit表示长度,最高可表示2^14 - 1,也就是16283
- 字符串长度大于等于16384时,占用5个字节,二进制结构为10000000 qqqqqqqq rrrrrrrr ssssssss tttttttt,第一个字节后6bit不使用,后面4个字节的32bit位用来表示长度,最高可表示2^32 - 1,也就是4294967295
如果为整数时,都只占1字节,前2bit确定是否整数节点,后6bit确定整数节点的类型
- 二进制结构为11000000,值为0XC0,entry-data存储int16_t类型数据(占用2字节)
- 二进制结构为11010000,值为0XD0,entry-data存储int32_t类型数据(占用4字节)
- 二进制结构为11100000,值为0XE0,entry-data存储int64_t类型数据(占用8字节)
- 二进制结构为11110000,值为0XF0,entry-data存储3个字节的整数
- 二进制结构为11111110,值为0XFE,entry-data存储1个字节的整数
- 二进制结构为1111xxxx,这是特殊情况,此时xxxx的值在0001至1101之间(因为0000和1110都已经被上面使用了),所以xxxx换算为十进制也就是1-13,而数值应该从0开始,所以得剪1,代表的就是0-12这几个值。此时xxxx直接用来表示entry-data的数据值,也就是不存在entry-data了
3.最后是entry-data,存储整数或字节数组。
- 当存入的是字符串时,会存入每个字符在ASCII表中对应的16进制值,然后形成一个字节数组。
- 当存入的是数字时,如果是上面所讲的特殊情况(即encoding为1111xxxx这种),那么该字段不存在,否则会按所属情况存储不同类型的数据,数据类型不同所占用字节也不同
ziplist数据结构实例分析
例如:逐步分析下图中的ziplist字节数据
- zlbytes,4个字节,由于redis采用是小端模式存储,所以实际应为00 00 00 1a,转化为十进制是26,表示该ziplist总共有26个字节
- zltail,4个字节,00 00 00 0c转化为十进制是12,表示该ziplist中最后一个数据项entry所在的位置是第12个字节
- zlen,2个字节,00 02转化为十进制是2,表示该ziplist中有2个数据项entry,如果此值为ff ff时,则说明两个字节不够记录数据项数量了,需遍历才能得知
- entry1,2个字节,00转化为十进制是0,