Redis 设计与实现 7:五大数据类型之列表

列表对象有 3 种编码:ziplistlinkedlistquicklist

  • ziplistlinkedlist 是 3.2 版本之前的编码。
  • quicklist 是 3.2 版本新增的编码,ziplistlinkedlist 在 3.2 版本及后续版本将不再是列表对象的编码。

编码定义如下(server.h):

#define OBJ_ENCODING_LINKEDLIST 4
#define OBJ_ENCODING_ZIPLIST 5
#define OBJ_ENCODING_QUICKLIST 9

虽然 ziplistlinkedlist 不再被列表对象作为编码,但是我们还是有必要了解的。因为 quicklist 也是基于 ziplistlinkedlist 改良的。


ziplist

压缩列表 ziplist 在之前的文章 Redis 设计与实现 5:压缩列表 ziplist 有介绍过,结构如下:

ziplist 的结构

我们使用命令操作列表的元素的时候,实际上就是在操作 entry 的数据。下面我们来举个栗子:

redis> RPUSH list_key 1 "ab" "d"

如果 list_keyziplist 编码,那么结构如下图:

list ziplist 编码实例结构


linkedlist

链表 linkedlist 的数据结构如下(adlist.h),跟普通的链表差不多:

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

链表节点的结构也很简单:

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

结构示意图如下:

list ziplist 编码结构图

数据将存储在 listNode 的 value 中,数据是一个字符串对象,用 redisObject 包裹着 sds
例如可能是 embstr 编码的 sds :
string embstr 编码示意图


下面我们来举个栗子:

redis> RPUSH list_key 1 "ab" "d"

假如 list_key 的编码是 linkedlist,那么结构如下图:

list linkedlist 编码示例结构图


quicklist

快速列表 quicklist3.2 版本新添加的编码类型,结合了 ziplistlinkedlist 的一种编码。
同时在 3.2 版本中,列表也废弃了 ziplistlinkedlist

通过上面的介绍,我们可以看出。双向链表的内存开销很大,每个节点的地址不连续,容易产生内存碎片,quicklist 利用 ziplist减少节点数量,但 ziplist 插入和删除数都很麻烦,复杂度高,为避免长度较长的 ziplist修改时带来的内存拷贝开销,通过配置项配置合理的 ziplist长度。

quicklist 的结构如下:

list quicklist 编码结构图

从上图可以看出,quicklistlinkedlist 最大的不同就是,quicklist 的值指向的是 ziplistziplist 可比之前的 redisObject 节省了非常多的内存!
从另一个角度看,他就是把一个长的 ziplist 切割成多个小的 ziplist


代码实现在 quicklist.h:

typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    // 所有 ziplist 中所有的节点数
    unsigned long count;
    // quicklistNode 的数量
    unsigned long len;
    // 限定 ziplist 的最大大小,可通过配置文件配置
    int fill : QL_FILL_BITS;
    // 压缩程度,0 表示不压缩,可通过配置文件配置
    unsigned int compress : QL_COMP_BITS;
    // ...
} quicklist;

配置一:fill (控制 ziplist 大小)

太长的 ziplist 增删的复杂度高,所以 quicklistfill 参数来控制 ziplist 的大小,它是通过配置文件的list-max-ziplist-size配置。

  • 当数字为正数,表示:每个节点的 ziplist 最多包含的 entry 个数。
  • 当数字为负数:
    • -1:每个节点的 ziplist 字节大小不能超过4kb
    • -2:每个节点的 ziplist 字节大小不能超过8kb (redis默认值)
    • -3:每个节点的 ziplist 字节大小不能超过16kb
    • -4:每个节点的 ziplist 字节大小不能超过32kb
    • -5:每个节点的 ziplist 字节大小不能超过64kb

配置二:compress (控制压缩程度)

因为链表的特性,一般首尾两端操作较频繁,中部操作相对较少,所以 redis 提供压缩深度配置:list-compress-depth,也就是属性 compress

  • 0:表示都不压缩。这是Redis的默认值。
  • 1:表示 quicklist 两端各有1个节点不压缩,中间的节点压缩。
  • 2:表示 quicklist 两端各有2个节点不压缩,中间的节点压缩。
  • 3:表示 quicklist 两端各有3个节点不压缩,中间的节点压缩。

quicklist 节点

typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    // 不设置压缩数据参数 recompress 时指向一个 ziplist 结构
    // 设置压缩数据参数recompress 时指向 quicklistLZF 结构
    unsigned char *zl;
    // ziplist 的字节数
    unsigned int sz;
    // ziplist 中包含的节点数量
    unsigned int count : 16;
    // 编码。1 表示压缩过,2 表示没压缩
    unsigned int encoding : 2;
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    // 标记 quicklist 节点的 ziplist 之前是否被解压缩过
    // 如果recompress 为 1,则等待被再次压缩
    unsigned int recompress : 1;
    // ...
} quicklistNode;

压缩过的 ziplist 结构

typedef struct quicklistLZF {
    // 表示被 LZF 算法压缩后的 ziplist 的大小
    unsigned int sz;
    // 压缩后的 ziplist 的数组,柔性数组
    char compressed[];
} quicklistLZF;

quick 的常用操作

1. 插入

(1) quicklist 可以在头部或者尾部插入数据:quicklist.c/quicklistPushHeadquicklist.c/quicklistPushTail,我们就挑一个从头部插入的代码来看看吧(插入尾部的代码也是差不多的)(代码格式略微调整了一下):

int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *orig_head = quicklist->head;
    // 判断头结点上的 ziplist 大小是否没超过限制
    if (likely(_quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
        // 没超过限制,就插入到 ziplist 中。ziplistPush 是 ziplist.c 的方法
        quicklist->head->zl = ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
        quicklistNodeUpdateSz(quicklist->head);
    } else {
        // ziplist 超过大小限制,则创新创建一个新的 quicklistNode
        quicklistNode *node = quicklistCreateNode();
        // 再创建新的 ziplist,然后把 ziplist 放到节点中
        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
        quicklistNodeUpdateSz(node);
        // 新的 quicklistNode 插入原来的头结点上,成为新的头结点
        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
    }
    quicklist->count++;
    quicklist->head->count++;
    return (orig_head != quicklist->head);
}

(2) quicklist 也可以从任意指定的位置插入:quicklist.c/_quicklistInsert,实现相对来说比较复杂,我们就用文字说明(代码太长,感兴趣的读者自己去读吧):

  • 当前节点是 NULL:创建一个新的节点,插入就好。
  • 当前节点的 ziplist 大小没有超过限制时:直接插入到 ziplist 就好。
  • 当前节点的 ziplist 大小超过限制时:
    • 如果插入的位置是 ziplist两端
      • 如果相邻的节点的 ziplist 大小没有超过限制,那么就插入到相邻节点ziplist 中。
      • 如果相邻的节点的 ziplist 大小也超过限制,这时需要创建一个新的节点插入。
    • 如果插入的位置是 ziplist中间
      则需要把当前 ziplist 从插入位置 分裂 (_quicklistSplitNode) 为两个节点,然后把数据插入第二个节点上。

2. 查找

quicklist 支持通过 index 查找元素:quicklist.c/quicklistIndex
查找的本质就是遍历,先查看quicklistNode 的长度判断 index 是否在这个节点中,如果不是则跳到下个节点。
当定位到节点之后,对节点里面的 ziplist 进行遍历查找 (ziplistIndex)。

3 删除

(1) 指定值的删除,quicklist.c/quicklistDelEntry
这个指定的值的信息 quicklistEntry 的结构如下:

typedef struct quicklistEntry {
    // 指向当前 quicklist 的指针
    const quicklist *quicklist;
    // 指向当前 quicklistNode 节点的指针
    quicklistNode *node;
    // 指向当前 ziplist 的指针
    unsigned char *zi;
    // 指向当前 ziplist 的字符串 vlaue 成员
    unsigned char *value;
    // 当前 ziplist 的整数 value 成员
    long long longval;
    // 当前 ziplist 的字节数大小
    unsigned int sz;
    // 在 ziplist 的偏移量
    int offset;
} quicklistEntry;

具体的删除代码如下(做了一些删减):

void quicklistDelEntry(quicklistIter *iter, quicklistEntry *entry) {
    quicklistNode *prev = entry->node->prev;
    quicklistNode *next = entry->node->next;
    // 通过 quicklistEntry 可以定位到 ziplist 中的元素位置,然后进行删除
    // quicklist -> quicklistNode -> ziplist -> ziplistEntry
    int deleted_node = quicklistDelIndex((quicklist *)entry->quicklist, entry->node, &entry->zi);
    // 下面是迭代器的参数调整,此处忽略...
}

(2) 区间元素 index 删除: quicklist.c/quicklistDelRange(代码太长了,就不晾出来了)
先通过遍历找元素,会判断是否可以删除整个节点 entry.offset == 0 && extent >= node->count,可以的话不用遍历里面的ziplist直接删除整个节点。
否则计算出当前节点ziplist 要删除的范围,通过 ziplistDeleteRange 函数删除。


重点回顾

  • 列表对象有 3 种编码:ziplistlinkedlistquicklist
  • quicklist3.2 后新增的用于替代 ziplistlinkedlist 的编码。
  • ziplist 节省内存,但是太长的话性能低下。linkedlist 占用内存太多。
  • quicklist 可以看成由多个 ziplist 组成的 linkedlist,性能高,节省内存。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容