青春因磨砺而精彩,人生因奋斗而升华
ziplist简介
ziplist: 是一个经过特殊编码的双向链表,设计的目标就是为了提高存储效率。可以用来存储整数或字符串,其中整数是按照真正的二进制进行编码的,而不是编码成字符串序列。能以O(1)的时间复杂度在表的两端提供push和pop操作;
优点:
ziplist充分体现了redis对于存储效率的追求,一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。ziplist却是将表中每一项都存放在前后连续的地址空间内,一个ziplist整体占用一大块内存
数据结构
内部宏观结构布局
<zlbytes> <zltail> <zllen> <entry> ... <entry> <zlend>
字段解析:
-
: 4byte,表示ziplist占用的字节总数;在对压缩列表进行内存重分配,或计算zlend的位置时使用
-
: 4byte,ziplist最后一项entry在ziplist中偏移的字节数;通过这个偏移量,可以很方便找到最后一项 (zlbytes+zltail=最后一个entry) ,且不需要遍历;从而可以很快的执行push或pop操作;
-
: 2byte, 表示ziplist中数据项entry的个数;因为只有2byte,当值超过2^16 - 2时,此值设置为2^16 - 1, 此时的值不再表示数据项个数,想知道ziplist中数据项总数,必须对其进行遍历;
-
: 真正存储数据的数据项,长度不定。
-
: 1byte,是一个结束标记;标记压缩列表的末端;
entry数据项结构
<prevlen> <encoding> <entry-data>
节点解析:
-
prevlen:存储上一个entry的长度,以字节为单位,能够从后向前遍历列表。只需要偏移prevlen字节就能找到前一项;
-
encoding:entry的编码。
-
entry-data:存储entry表示的数据
prevlen解析
-
当前一个 entry 长度 小于254 的时候,prevlen只使用 1byte, 这个字节的值就是前一个entry占用的字节数
<prevlen from 0 to 253>
-
当前一个entry 长度 >=254,那么prevlen用 5byte 表示,第一个字节设置为254(0xFE),仅作为一个标记,后面4个字节组成一个整型值,用来真正存储前一个entry的占用字节数;
0xFE <4 bytes unsigned little endian prevlen>
由于prevlen记录前一个数据项(entry)的长度,所以可以根据当前节点的起始地址计算出前一个节点的起始地址(prev = 地址指针 - prevlen属性值);压缩列表从表尾向表头遍历操作就是使用这一原理实现的;只要拥有某一节点的起始指针就可以一直向表头遍历;
encoding
当存储的是string时:
-
|00pppppp| - 1byte: 第1个字节最高2bit是00;字段占用1字节;剩余的6bit用来表示长度值,最高可表示 63;
-
|01pppppp|qqqqqqqq| - 2byte: 第1byte最高2bit是01, 占用2byte,剩余14bit来表示长度值;
-
|10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt - 5byte: 第1byte最高2bit是10,字段占5byte,总共使用32bit表示长度值(第1字节的后6bit舍弃不用);
当存储整数时:
-
|11000000| - 3byte: 占用1byte,值为: 0xC0; 占用2byte,存储int16类型
-
|11010000| - 5byte: 占用1byte,值为: 0xD0; 占用4byte,存储int32类型
-
|11100000| - 9byte: 占用1byte,值为: 0xE0; 占用8byte,存储int64类型
-
|11110000| - 4byte: 占用1byte,值为: 0xF0; 占用3byte,存储int24类型
-
|11111110| - 2byte: 占用1byte,值为: 0xFE; 占用1byte,存储int8类型
-
|1111xxxx| - (xxxx的值在0001 - 1101之间): 这是一种特殊情况,xxxx从1到13一共13个值,就用这13个值来表示真正的数据(不是数据长度);即这种情况下,后面不需要一个单独的字段来表示真正的数据,而是和合二为一。另外由于xxxx只能去0001 - 1101这13个值(其他可能的值和其它情况冲突,eg: 0000 和 1110分别同其前面情况出现冲突),而小数值应该从0开始,因此这13个值分别表示 0 - 12,即xxxx的值减去1才是它所要表示的那个整数值;
zlentry压缩列表结构:
typedef struct zlentry {
// 前一节点长度信息的长度;因为压缩列表可以能需要倒叙遍历;所以可定位上一个entry
unsigned int prevrawlensize;
// 前一节点长度;
unsigned int prevrawlen;
// 当前节点长度信息长度
unsigned int lensize;
// 当前节点长度
unsigned int len;
// 当前节点头部信息长度
unsigned int headersize;
// 当前节点数据编码;
unsigned char encoding;
// 指向开头指针
unsigned char *p;
} zlentry;
ziplist具体结构
-
ziplist包含27byte,本例以16进制表示;
-
zlbytes用4byte,1B=27byte;
-
然后用4byte表示zltail,0x16=22,表示最后一项数据项在字节数组的第22位置
-
再用2byte,值为0x03=3,表示ziplist里一共存有3项数据项(entry)。
-
第一个entry: 用6byte保存数据项;其中prevlen=0;因为它前面没有数据项;encoding=4,表示后面4byte按照字符串存储,数据的值为”name”
-
第二个entry: 用 5byte 保存数据项;其中prevlen=06,即前一项的长度6byte, encoding表示当前entry-data为3byte, 值为:why;
-
第三个entry: 用5byte保存数据;encoding=FE; 所以存储的为1byte的整数,值为:0x14=20;
-
最后1byte, 表示zlend,固定值0xFF;
思考:
连锁更新:
添加新节点或者删除节点,都有可能发生连锁更新的想象;
-
当对ziplist进行插入操作时,由于prevlen记录了前一个节点的长度,假设ziplist现有数据项的长度都 <254字节,那么prevlen只需1byte;如果我们插入的数据项长度 >= 254, 由于新数据项后面节点的prevlen仅1byte,无法保存新数据项长度,所以会对压缩列表执行空间重分配操作,并将e1的prevlen扩容至5byte, 但是,由于e1的原长度<254, 扩容之后,e1后面的数据项又无法保存e1的长度,为了让其能够记录下e1的长度,程序需要再次执行空间重分配操作,并将e2的prevlen扩容到5byte, 以此类推,从而发生连锁更新现象;
-
当对ziplist进行删除操作,如果ziplist中存在一个big节点,且其后面的节点e1-en的长度<254时, 如果删除big节点后e1数据项时,e2的prevlen为1byte, 不足以记录big的长度,程序就会扩展e1的空间,以此类推就会发生连锁更新
为什么没有255
因为255已经定义了ziplist的的值。在ziplist的很多操作的实现中,都会根据数据项的第1个字节(的第1字节)是不能够取255的,否则就冲突了
为什么元素较少时使用ziplist?
ziplist本身是一块连续的内存块,从底层的磁盘读写来说,顺序I/O的效率肯定高于随机I/O。但是,由于ziplist是连续内存,如果元素数量太多,意味着当创建和扩展的时候需要操作更多的内存,所以ziplist针对元素少的时候才能提升效率