Redis 数据结构底层实现


Redis 数据结构底层实现

String

  • 如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将 void* 转换成 long ),并将字符串对象的编码设置为 int
  • 如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于 39 字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为 raw
  • 如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于 39 字节,那么字符串对象将使用 embstr 编码的方式来保存这个字符串值

List

  • 列表对象的编码可以是 ziplist 或者 linkedlist
  • 列表对象保存的所有字符串元素的长度都小于 64 字节并且保存的元素数量小于 512 个,使用 ziplist 编码;否则使用 linkedlist

Hash

  • 哈希对象的编码可以是 ziplist 或者 hashtable
  • 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节并且保存的键值对数量小于 512 个,使用ziplist 编码;否则使用hashtable

Set

  • 集合对象的编码可以是 intset 或者 hashtable
  • 集合对象保存的所有元素都是整数值并且保存的元素数量不超过 512 个,使用intset 编码;否则使用hashtable

Sorted Set

  • 有序集合的编码可以是 ziplist 或者 skiplist
  • 有序集合保存的元素数量小于 128 个并且保存的所有元素成员的长度都小于 64 字节。使用 ziplist 编码;否则使用skiplist

简单动态字符串(SDS)

Redis 自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型, 并将 SDS 用作 Redis 的默认字符串表示;
key 和 value 底层都是用 SDS 来实现的

SDS 的结构:

struct sdshdr {
    // 记录 buf 数组中已使用字节的数量
    // 等于 SDS 所保存字符串的长度
    int len;
    // 记录 buf 数组中未使用字节的数量
    int free;
    // 字节数组,用于保存字符串,最后一个字节则保存了空字符 '\0' 兼容C语言
    char buf[];
};

为什么不直接使用C语言的字符串呢?SDS对比C语言的字符串有如下优势:

  • SDS 获取字符串长度时间复杂度O(1)

因为 SDS 通过 len 字段来存储长度,使用时直接读取就可以;C语言要想获取字符串长度需要遍历整个字符串,时间复杂度O(N)

  • SDS 能杜绝缓冲区的溢出

因为当 SDS API 要对 SDS 进行修改时,会先检查 SDS 的空间是否足够,如果不够的话 SDS 会自动扩容,所以不会造成缓冲区溢出。而C语言及其容易造成缓冲区溢出问题;
比如用strcat(),在用这个函数之前必须要先给目标变量分配足够的空间,否则就会溢出

  • SDS 能减少修改字符串时带来的内存重分配次数

空间预分配:当SDS 扩容时不只是会增加需要的空间大小,还会额外的分配一些未使用的空间。分配的规则是:如果分配后SDS的长度小于 1MB,那么会分配所需双倍大小的空间,简单说就是,SDS 动态分配后是 16KB,那么就会多分配 16KB 的未使用空间;
如果大于 1MB,那么就分配申请的空间大小。

惰性空间释放:用于优化 SDS 的字符串缩短操作:当 SDSAPI 需要缩短 SDS 保存的字符串时,并不会立即内存重分配来回收多出来的字节,而是用 free 来记录未使用空间。

C字符串的每次变更(曾长或缩短)都会对数组作内存重分配。同样,如果是缩短,没有处理好多余的空间,也会造成内存泄漏

链表

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。链表在 Redis 中的应用非常广泛,
比如 List 的底层实现之一链表,当一个 List 包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis 就会使用链表作为 List 的底层实现。
除了用作 List 的底层实现之外,发布与订阅、慢查询、监视器等动能也用到了链表, Redis 服务器本身还使用链表来保存多个客户端的状态信息,以及使用链表来构建客户端输出缓冲区

结构:

typedef  struct listNode{
   //前置节点
   struct listNode *prev;
   //后置节点
   struct listNode *next;
   //节点的值
   void *value;  
}listNode

多个 listNode 可以通过 prevnext 指针组成双端链表, 如下图所示:

list结构:

typedef struct list {
    // 表头节点
    listNode *head;
    // 表尾节点
    listNode *tail;
    // 链表所包含的节点数量
    unsigned long len;
    // 节点值复制函数,用于复制链表节点所保存的值
    void *(*dup)(void *ptr);
    // 节点值释放函数,用于释放链表节点所保存的值
    void (*free)(void *ptr);
    // 节点值对比函数,用于对比链表节点所保存的值和另一个输入值是否相等
    int (*match)(void *ptr, void *key);
} list;

由一个 list 结构和三个 listNode 结构组成的链表:

list特性总结:

  • 双端

链表节点带有 prevnext 指针,获取某个节点的前置节点和后置节点的复杂度都是 O(1)

  • 无环

表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL ,对链表的访问以 NULL 为终点

  • 带表头指针和表尾指针

通过 list 结构的 head 指针和 tail 指针,程序获取链表的表头节点和表尾节点的复杂度为 O(1)

  • 带链表长度计数器

程序使用 list 结构的 len 属性来对 list 持有的链表节点进行计数,程序获取链表中节点数量的复杂度为 O(1)

  • 多态

链表节点使用 void* 指针来保存节点值,并且可以通过 list 结构的 dupfreematch 三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值

字典

Redis 数据库底层就是用字典实现的,对数据库的增、删、改、查操作都是构建在对字典的操作之上;字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis 就会使用字典作为哈希键的底层实现

哈希表

结构:

typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;

一个大小为 4 的空哈希表:

哈希表节点

结构:

typedef struct dictEntry {
    // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下个哈希表节点,形成链表,用于解决hash冲突(链地址法)
    struct dictEntry *next;  // 单链表结构
} dictEntry;

举个例子, 下图就展示了如何通过 next 指针, 将两个索引值相同的键 k1 和 k0 连接在一起:

字典

结构:

typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;

type 属性和 privdata 属性是针对不同类型的键值对,为创建多态字典而设置的:

type 属性是一个指向 dictType 结构的指针,每个 dictType 结构保存了一簇用于操作特定类型键值对的函数,Redis 会为用途不同的字典设置不同的类型特定函数; 而 privdata
属性则保存了需要传给那些类型特定函数的可选参数

ht 属性是一个包含两个项的数组,数组中的每个项都是一个 dictht 哈希表,一般情况下,字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用

除了 ht[1] 之外,另一个和 rehash 有关的属性就是 rehashidx ,它记录了 rehash 目前的进度, 如果目前没有在进行 rehash , 那么它的值为 -1

下面展示了一个普通状态下(没有进行 rehash)的字典:

哈希算法

当将一个新的键值对插入到字典中,需要计算索引值,Redis 计算索引值的方法是:

# 使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);

# 使用哈希表的 sizemask 属性和哈希值,计算出索引值
# 根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;

rehash

随着不断的操作,hash表中的键值对可能会增多或减少,为了让哈希表的负载因子保持在一个范围内,需要对 hash表进行扩容或收缩,收缩和扩容的过程就叫 rehash

rehash过程:

  1. 为字典的 ht[1] 哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及 ht[0] 当前包含的键值对数量(也即是 ht[0].used 属性的值):

    • 如果执行的是扩展操作,那么 ht[1] 的大小为第一个大于等于 ht[0].used * 22^n (2 的 n 次方幂)
    • 如果执行的是收缩操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n
  2. 将保存在 ht[0] 中的所有键值对 rehashht[1] 上面;·rehash· 指的是重新计算键的哈希值和索引值,然后将键值对放置到 ht[1] 哈希表的指定位置上

  3. ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] ,将 ht[1] 设置为 ht[0] ,并在 ht[1]
    新创建一个空白哈希表,为下一次 rehash 做准备

当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:

  • 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1
  • 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5

哈希表的负载因子可以通过公式:

# 负载因子 = 哈希表已保存节点数量 / 哈希表大小  
load_factor = ht[0].used / ht[0].size

另一方面,当哈希表的负载因子小于 0.1 时,程序自动开始对哈希表执行收缩操作

  • 渐进式 rehash

rehash 时会将 ht[0] 所有的键值对迁移到 ht[1] 中,但这个动作不是一次性的,而是分多次、渐进式地完成。
这样的原因是:当数据量大的时候一次性迁移会造成服务器在一段时间内停止服务(类似一些语言执行GC时的STW)。为了避免发生这样的事就出现了 渐进式rehash

详细步骤:

  1. ht[1] 分配空间,让字典同时持有 ht[0]ht[1] 两个哈希表
  2. 在字典中维持一个索引计数器变量 rehashidx ,并将它的值设置为 0 ,表示 rehash 工作正式开始
  3. rehash 进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehashht[1]
    ,当 rehash 工作完成之后,程序将 rehashidx 属性的值增一
  4. 随着字典操作的不断执行,最终在某个时间点上,ht[0] 的所有键值对都会被 rehashht[1] ,这时程序将 rehashidx 属性的值设为 -1 ,表示 rehash 操作已完成

rehash 期间,字典的删改查操作都是同时作用在 ht[0] 以及 ht[1] 上的。 如查找一个键,会现在 ht[0] 查找,找不到就去 ht[1] 查找,注意的是增加操作,新增的键值对只会保存到 ht[1]
上,不会保存到 ht[0] 上,这一措施保证了 ht[0] 的键值只减不增,随着 rehash 操作 ht[0] 最终会变成空表

跳跃表

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。 跳跃表支持平均 O(log N) 最坏 O(N) 复杂度的节点查找,还可以通过顺序性操作来批量处理节点

在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要来得更为简单,所以有不少程序都使用跳跃表来代替平衡树

skiplistAVL对比:

  1. 从算法实现难度上来比较,skiplist比平衡树要简单得多。
  2. 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
  3. 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当。
  4. 在做范围查找的时候,平衡树比skiplist操作要复杂。
  5. skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的

Redis 使用跳跃表作为有序集合键的底层实现之一: 如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis 就会使用跳跃表来作为有序集合键的底层实现

跳跃表节点结构:

typedef struct zskiplistNode {
    // 后退指针
    struct zskiplistNode *backward;
    // 分值
    double score;
    // 成员对象
    robj *obj;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
} zskiplistNode;

跳跃表结构:

typedef struct zskiplist {
    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;
    // 表中节点的数量,也即是跳跃表目前包含节点的数量(表头节点不计算在内)
    unsigned long length;
    // 表中层数最大的节点的层数,层数最大的那个节点的层数(表头节点的层数不计算在内)
    int level;
} zskiplist;

示例:

位于 zskiplist 结构右方的是四个 zskiplistNode 结构,该结构包含以下属性:

层(level

节点中用 L1 、L2 、L3 等字样标记节点的各个层,L1 代表第一层,L2 代表第二层,以此类推。每个层都带有两个属性:前进指针和跨度。
前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。在上面的图片中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行

跳跃表节点的 level 数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快

每次创建一个新跳跃表节点的时候,程序都根据幂次定律 (power law,越大的数出现的概率越小) 随机生成一个介于 1 和 32 之间的值作为 level 数组的大小,这个大小就是层的“高度”

跨度

层的跨度(level[i].span 属性)用于记录两个节点之间的距离,两个节点之间的跨度越大,它们相距得就越远,指向 NULL 的所有前进指针的跨度都为 0 ,因为它们没有连向任何节点

初看上去,很容易以为跨度和遍历操作有关,但实际上并不是这样,遍历操作只使用前进指针就可以完成了,跨度实际上是用来计算排位(rank)的,在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位

后退(backward)指针

节点中用 BW 字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用 节点的后退指针(backward 属性)用于从表尾向表头方向访问节点,跟可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点

分值(score

各个节点中的 1.0 、 2.0 和 3.0 是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列

节点的分值(score 属性)是一个 double 类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序

成员对象(obj

各个节点中的 o1 、o2 和 o3 是节点所保存的成员对象

节点的成员对象(obj 属性)是一个指针,它指向一个字符串对象,而字符串对象则保存着一个 SDS

在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的
分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的方向),而成员对象较大的节点则会排在后面(靠近表尾的方向)

整数集合

整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现
它可以保存类型为 int16_tint32_t 或者 int64_t 的整数值,并且保证集合中不会出现重复元素

结构:

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

虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组,但实际上 contents 数组并不保存任何 int8_t 类型的值,
contents 数组的真正类型取决于 encoding 属性的值:如果 encoding 属性的值为 INTSET_ENC_INT16 ,那么 contents 就是一个 int16_t
类型的数组,数组里的每个项都是一个 int16_t 类型的整数值 (最小值为 -32,768 ,最大值为 32,767 )

每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级(upgrade),然后才能将新元素添加到整数集合里面

升级整数集合并添加新元素共分为三步进行:

  1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
  2. 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变
  3. 将新元素添加到底层数组里面

整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态

压缩列表

当一个列表键、哈希键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么 Redis 就会使用压缩列表来做列表键的底层实现

redis> RPUSH lst 1 3 5 10086 "hello" "world"
(integer) 6

redis> OBJECT ENCODING lst
"ziplist"
redis> HMSET profile "name" "Jack" "age" 28 "job" "Programmer"
OK

redis> OBJECT ENCODING profile
"ziplist"

压缩列表的构成

压缩列表是 Redis 为了节约内存而开发的,由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构

一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值

ziplist没有明确定义结构体,这里只作大概的演示:

typedef struct entry {
     /*前一个元素长度需要空间和前一个元素长度*/
    unsigned int prevlengh;
     /*元素内容编码*/
    unsigned char encoding;
     /*元素实际内容*/
    unsigned char *data;
}zlentry;

entry结构体里面有三个重要的字段:

  1. previous_entry_length这个字段记录了ziplist中前一个节点的长度,什么意思?就是说通过该属性可以进行指针运算达到表尾向表头遍历,这个字段还有一个大问题下面会讲。
  2. encoding记录了数据类型(int16? string?)和长度。
  3. data/content记录数据

上面有说到,previous_entry_length这个字段存放上个节点的长度,那默认长度给分配多少呢?Redis是这样分的,如果前节点长度小于254,就分配1字节,大于的话分配5字节,那问题就来了;
如果前一个节点的长度刚开始小于254字节,后来大于254,那不就存放不下了吗? 这就涉及到previous_entry_length
的更新,但是改一个肯定不行阿,后面的节点内存信息都需要改。所以就需要重新分配内存,然后连锁更新包括该受影响节点后面的所有节点; 除了增加新节点会引发连锁更新、删除节点也会触发

typedef struct ziplist{
     /*ziplist分配的内存大小*/
     uint32_t zlbytes;
     /*达到尾部的偏移量*/
     uint32_t zltail;
     /*存储元素实体个数*/
     uint16_t zllen;
     /*存储内容实体元素*/
     unsigned char* entry[];
     /*尾部标识*/
     unsigned char zlend;
}ziplist;

压缩列表的各个组成部分:

属性类型长度用途
zlbytesuint32_t4 字节记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分配,或者计算 zlend 的位置时使用
zltailuint32_t4 字节记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址
zllenuint16_t2 字节记录了压缩列表包含的节点数量,当这个属性的值小于 UINT16_MAX (65535)时,这个属性的值就是压缩列表包含节点的数量;当这个值等于 UINT16_MAX 时,节点的真实数量需要遍历整个压缩列表才能计算得出
entryX列表节点不定压缩列表包含的各个节点,节点的长度由节点保存的内容决定
zlenduint8_t1 字节特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端

quicklist

一个由ziplist组成的双向链表。但是一个quicklist可以有多个quicklist节点,它很像B树的存储方式。是在redis3.2版本中新加的数据结构,用在列表的底层实现

表头结构:

typedef struct quicklist {
    //指向头部(最左边)quicklist节点的指针
    quicklistNode *head;
 
    //指向尾部(最右边)quicklist节点的指针
    quicklistNode *tail;
 
    //ziplist中的entry节点计数器
    unsigned long count;        /* total count of all entries in all ziplists */
 
    //quicklist的quicklistNode节点计数器
    unsigned int len;           /* number of quicklistNodes */
 
    //保存ziplist的大小,配置文件设定,占16bits
    int fill : 16;              /* fill factor for individual nodes */
 
    //保存压缩程度值,配置文件设定,占16bits,0表示不压缩
    unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;

节点结构:

typedef struct quicklistNode {
    struct quicklistNode *prev;     //前驱节点指针
    struct quicklistNode *next;     //后继节点指针
 
    //不设置压缩数据参数recompress时指向一个ziplist结构
    //设置压缩数据参数recompress指向quicklistLZF结构
    unsigned char *zl;
 
    //压缩列表ziplist的总长度
    unsigned int sz;                  /* ziplist size in bytes */
 
    //ziplist中包的节点数,占16 bits长度
    unsigned int count : 16;          /* count of items in ziplist */
 
    //表示是否采用了LZF压缩算法压缩quicklist节点,1表示压缩过,2表示没压缩,占2 bits长度
    unsigned int encoding : 2;        /* RAW==1 or LZF==2 */
 
    //表示一个quicklistNode节点是否采用ziplist结构保存数据,2表示压缩了,1表示没压缩,默认是2,占2bits长度
    unsigned int container : 2;       /* NONE==1 or ZIPLIST==2 */
 
    //标记quicklist节点的ziplist之前是否被解压缩过,占1bit长度
    //如果recompress为1,则等待被再次压缩
    unsigned int recompress : 1; /* was this node previous compressed? */
 
    //测试时使用
    unsigned int attempted_compress : 1; /* node can't compress; too small */
 
    //额外扩展位,占10bits长度
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

Redis相关配置

list-max-ziplist-size -2
list-compress-depth 0
  • list-max-ziplist-size参数

当取正值的时候,表示按照数据项个数来限定每个quicklist节点上的ziplist长度。比如,当这个参数配置成5的时候,表示每个quicklist节点的ziplist最多包含5个数据项。

当取负值的时候,表示按照占用字节数来限定每个quicklist节点上的ziplist长度。这时,它只能取-1-5这五个值,每个值含义如下:

-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)

-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。

-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。

-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(默认值)

  • list-compress-depth参数

这个参数表示一个quicklist两端不被压缩的节点个数。 注:这里的节点个数是指quicklist双向链表的节点个数,而不是指ziplist里面的数据项个数。实际上,一个quicklist节点上的ziplist
,如果被压缩,就是整体被压缩的

取值含义如下:
0: 是个特殊值,表示都不压缩。这是Redis的默认值。
1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
3: 表示quicklist两端各有3个节点不压缩,中间的节点压缩。 依此类推

Redis对于quicklist内部节点的压缩算法,采用的LZF(一种无损压缩算法)


本文不允许转载。
  目录