Redis源码整理笔记:ziplist与个人理解

       ziplist 是一种特殊编码的双向列表,特殊的设计有效提升了内存操作效率;ziplist的结点可以无序;也可以有序;ziplist允许同时存放字符串和整型类型,并且整型数被编码成真实的整型数而不是字符串序列,列表可以在头尾进行push和pop操作的时间复杂度都在常量范围O(1), 但是每次操作都涉及内存重分配,也增加了操作的复杂性。

ziplist 的组成结构

在这里插入图片描述

zlbytes 4字节,记录压缩列表占用的内存字节数;
zltail     4字节,记录最后一个entry在压缩列表中的位置,方便在列表尾部的push/pop操作和逆向遍历链表
zllen     2字节,记录压缩列表中数据项entry的个数
entry    每个entry数据节点包含2部分信息,
     (a) 上一个节点的长度,可以从任意节点开始从后往前遍历整个列表
     (b) 字符串的编码方式
zlend    结束标识,用单字节表示,值是11111111 即255

zlentry 的布局

每个节点由三部分组成:
Redis源码整理笔记:ziplist与个人理解_第1张图片
previous_entry_length:记录前一个结点的长度,previous_entry_length属性长度可以是1或5字节

  • 如果前一个数据项的长度小于254,previous_entry_length属性长度为1字节

  • 如果大于等于254,previous_entry_length属性的长度为5字节,第一个字节为0xFE(254),,后面的4个字节保存的是前一个数据项的长度

  • 不用255作为分界,因为zlend是255,用于判断ziplist是否到达尾部位置
    Redis源码整理笔记:ziplist与个人理解_第2张图片
    encoding:当前节点的内存编码方式,通过第一个字节的前两位来区分整数和字符串

  • 字符串编码以00/01/10 开头

编码 编码长度 context属性保存的内容
00bbbbbb 1byte 保存长度小于等于63(2^6 - 1)字节的字节数组(以00开头)
01bbbbbb xxxxxxxx 2byte 保存长度小于等于16383(2^14 - 1)字节的字节数组(以01开头)
10bbbbbb qqqqqqqq rrrrrrrr ssssssss tttttttt 5byte 保存长度小于等于4294 967 295(2^32 - 1)字节的字节数组(以10开头)
  • 整数编码以11开头
编码 编码长度 context属性保存的内容
11000000 1byte int16_t
11010000 1byte int32_t
11100000 1byte int64_t
11110000 1byte 24位有符号整数
11111110 1byte 8位有符号整数
1111xxxx 1byte xxxx将是介于0001到1101之间

需要注意的是:1111xxxx,表示 0-12 之间的值,使用这一编码无须content属性

content:当前节点实际保存的数据,可以是字符串或整数,由encoding属性决定

// 单个节点的结构体定义
typedef struct zlentry {
    unsigned int prevrawlensize; // 编码prevrawlen所需的字节大小		
    unsigned int prevrawlen;     // 前一个数据项的长度 		
    unsigned int lensize;        // 编码len所需的字节大小
    // 每个节点可以存储字符串或整数,存储字符串时,len为字符串长度,存储整数时,len是存储整数所需要的字节数
    unsigned int len;            // 当前节点值的长度 							
    unsigned int headersize;     // 当前节点的header大小 = prevrawlensize + lensize. 
    unsigned char encoding;      // 当前节点存储使用的编码类型							
    unsigned char *p;            // 指向当前节点的指针
} zlentry;

(1) prevrawlensize和prevrawlen信息存储在 previous_entry_length 部分
(2) lensize、len、encoding信息存储在encoding部分;
ziplist 插入新结点的过程
  1. 获取前一个数据项的长度即previous_entry_length。如果是插入的末尾,可以根据offset获取最后数据项的长度,如果不是,可根据下一个数据项的previous_entry_length获取

  2. 计算当前数据项占用的总字节数reqlen,主要包括三部分:previous_entry_length的编码长度+ encoding的编码长度 + curlen(curlen是当前数据项的大小)

  3. 判断下一个数据项的previous_entry_length编码长度是否需要调整

  4. 为新数据项申请内存空间,ziplist是一段连续的内存空间,申请的空间大小是totalsize +reqlen+nextdiff,totalsize 是当前ziplist的空间大小

  5. 确定插入位置,并后移下一个数据项及其之后的数据项,可能会引起连锁反应

  6. 将新数据项的数据复制到插入位置

ziplist元素后移
  1. nextdiff = 4时

    p(插入位置)处下一个元素需要扩充4个字节,移动时需要留出reqlen+4个字节的空间,用于扩充下一个元素的previous_entry_length编码

  2. nextdiff = 0 时

    此时无论forcelarge为0还是1,移动后空出reqlen个字节,都不需要再多留出额外空间

  3. nextdiff = -4 时

    (a) 如果reqlen>=4 ,下个数据项的previous_entry_length编码缩减了4个字节,其后的数据项向后移动时会少移动4个字节

    (b) 如果reqlen<4 时,插入新数据项会前移,源码中当满足nextdiff == -4 && reqlen < 4条件时nextdiff=0;forcelarge=1目的在于防止前移。

连锁更新

       存在这样一种情况,有多个连续的而且长度在【250,253】范围内的节点e1到eN,由于长度范围在【250,253】之间,所以每个节点的previous_entry_length字段只需要一个字节,其中e1的previous_entry_length字段需要1个字节;
Redis源码整理笔记:ziplist与个人理解_第3张图片
       e1的previous_entry_length字段表示new节点的长度(>=254),插入新节点后就由1字节变为5字节,即e1的长度增加4字节,e1的长度由在【250,253】范围内变为在【254,257】范围内,紧跟着的e2节点的previous_entry_length字段也由1字节变为5字节,依次类推直到eN,所有节点的previous_entry_length字段均有1字节变为5字节,这种现象就是连锁更新。删除操作也会引发连锁更新,原因是结点长度在【254,257】范围内变为在【250,253】范围内,previous_entry_length编码由5个字节变为1个字节
       连锁更新在最坏的情况下的时间复杂度为O(N^2),因为需要对压缩列表执行N次空间重分配和内存移动操作,每次空间重分配的最坏复杂度为O(N)。但是需要恰好连续多个长度在【250,253】范围内的节点才会触发连锁更新,而且即使触发连锁更新,如果被更新的节点数量不多也不会对性能造成影响。

连锁更新处理过程:

  1. 如果previous_entry_length由1字节变为4字节,则申请内存,后移数据
  2. 如果需要缩减previous_entry_length,会强制previous_entry_length置为4字节,方式数据迁移,同时数据项长度未发生变化,连锁更新结束处理过程
  3. 如果previous_entry_length长度没有发生变化,结束连锁更新过程
ziplist 和adlist 的区别
  1. 前者是压缩列表,后者是双向列表;压缩双链表以连续的内存空间 来表示双链表,压缩双链表节省前驱和后驱指针的空间(8b),这在小的list 上,压缩效率 是非常明显的,因为一个普通的双链表中,前驱后驱指针在 64 位机器上需分别占用 8B;
  2. 普通的双向链表,链表的每一个节点都会占用一块独立内存,各项之间使用指针连接起来,指针会占用额外的内存空间,而且会产生大量的内存碎片,ziplist则是放在连续的地址空间中,实际上是一个表(list),而不是链表
  3. ziplist 对于值的存储也采用了变长的编码方式,就是对于比较大的数就多用一些字节存储,小的数就少用一些字节

你可能感兴趣的:(Redis)