Redis源码剖析--快速列表quicklist

2023-05-16

在RedisObject这一篇博客中,有介绍到list结构的底层编码类型有OBJ_ENCODING_QUICKLIST,当时就发现这个底层数据结构被我遗漏了。昨天花了点时间补了补这个知识,看完发现这货就跟STL中的deque的思想一样,顿时觉得又是一个实现超级繁琐但很实用的数据结构。今天就带大家一起来看看这个“二合一”的数据结构。

quicklist是Redis在3.2版本加入的新数据结构,其是list列表的底层数据结构。

quicklist简介

为什么说quicklist是“二合一”呢?如果你看过STL中的deque的实现,就会知道deque是由一个map中控器和一个数组组成的数据结构,它既具有链表头尾插入便捷的优点,又有数组连续内存存储,支持下标访问的优点。Redis中是采用sdlist和ziplist来实现quicklist的,其中sdlist充当map中控器的作用,ziplist充当占用连续内存空间数组的作用。quicklist本身是一个双向无环链表,它的每一个节点都是一个ziplist。为什么这么设计呢?

  • 双向链表在插入节点上复杂度很低,但它的内存开销很大,每个节点的地址不连续,容易产生内存碎片。
  • ziplist是存储在一段连续的内存上,存储效率高,但是它不利于修改操作,插入和删除数都很麻烦,复杂度高,而且其需要频繁的申请释放内存,特别是ziplist中数据较多的情况下,搬移内存数据太费时!

Redis综合了双向链表和ziplist的优点,设计了quicklist这个数据结构,使它作为list键的底层实现。接下来,就要考虑每一个ziplist中存放的元素个数。

  • 如果每一个ziplist中的元素个数过少,内存碎片就会增多。可以按照极端情况双向链表来考虑。
  • 如果每一个ziplist中的元素个数过多,那么ziplist分配大块连续内存空间的难度就增大,同样会影响效率。

Redis的配置文件中,给出了每个ziplist中的元素个数设定,考虑使用场景需求,我们可以选择不同的元素个数。该参数设置格式如下:

list-max-ziplist-size -2

后面的数字可正可负,正、负代表不同函数,其中,如果参数为正,表示按照数据项个数来限定每个节点中的元素个数,比如3代表每个节点中存放的元素个数不能超过3;反之,如果参数为负,表示按照字节数来限定每个节点中的元素个数,它只能取-1~-5这五个数,其含义如下:

  • -1 每个节点的ziplist字节大小不能超过4kb
  • -2 每个节点的ziplist字节大小不能超过8kb
  • -3 每个节点的ziplist字节大小不能超过16kb
  • -4 每个节点的ziplist字节大小不能超过32kb
  • -5 每个节点的ziplist字节大小不能超过64kb

另外,在quicklist的源码中提到了一个LZF的压缩算法,该算法用于对quicklist的节点进行压缩操作。list的设计目的是能够存放很长的数据列表,当列表很长时,必然会占用很高的内存空间,且list中最容易访问的是两端的数据,中间的数据访问率较低,于是就可以从这个出发点来进一步节省内存用于其他操作。Redis提供了一下的配置参数,用于表示中间节点是否压缩。

list-compress-depth 0

参数list-compress-depth的取值和含义对应如下:

  • 0 特殊值,表示不压缩
  • 1 表示quicklist两端各有一个节点不压缩,中间的节点压缩
  • 2 表示quicklist两端各有两个节点不压缩,中间的节点压缩
  • 3 表示quicklist两端各有三个节点不压缩,中间的节点压缩
  • 以此类推。

quicklist的数据结构

quicklist的数据结构定义在quicklist.c文件中。

typedef struct quicklist {
    quicklistNode *head;        // 指向quicklist的头部
    quicklistNode *tail;        // 指向quicklist的尾部
    unsigned long count;        // 列表中所有数据项的个数总和
    unsigned int len;           // quicklist节点的个数,即ziplist的个数
    int fill : 16;              // ziplist大小限定,由list-max-ziplist-size给定
    unsigned int compress : 16; // 节点压缩深度设置,由list-compress-depth给定
} quicklist;

每个quicklist结构占用32个字节的空间,下面来看看quicklist节点的数据结构。

typedef struct quicklistNode {
    struct quicklistNode *prev;  // 指向上一个ziplist节点
    struct quicklistNode *next;  // 指向下一个ziplist节点
    unsigned char *zl;           // 数据指针,如果没有被压缩,就指向ziplist结构,反之指向quicklistLZF结构 
    unsigned int sz;             // 表示指向ziplist结构的总长度(内存占用长度)
    unsigned int count : 16;     // 表示ziplist中的数据项个数
    unsigned int encoding : 2;   // 编码方式,1--ziplist,2--quicklistLZF
    unsigned int container : 2;  // 预留字段,存放数据的方式,1--NONE,2--ziplist
    unsigned int recompress : 1; // 解压标记,当查看一个被压缩的数据时,需要暂时解压,标记此参数为1,之后再重新进行压缩
    unsigned int attempted_compress : 1; // 测试相关
    unsigned int extra : 10; // 扩展字段,暂时没用
} quicklistNode;

每个quicklistnode也占用32个字节,上面介绍了,每个节点的数据存放格式有ziplist和quicklistLZF,后者是一种采用LZF压缩算法压缩的数据结构,其定义如下:

typedef struct quicklistLZF {
    unsigned int sz; // LZF压缩后占用的字节数
    char compressed[]; // 柔性数组,指向数据部分
} quicklistLZF;

分析到这里,quicklist的大体结构以及呈现在我们面前了,请看下图,是不是豁然开朗:

quicklist

另外,quicklist还提供了迭代器结构以及指向ziplist中的节点结构

// quicklist的迭代器结构
typedef struct quicklistIter {
    const quicklist *quicklist;  // 指向所在quicklist的指针
    quicklistNode *current;  // 指向当前节点的指针
    unsigned char *zi;  // 指向当前节点的ziplist
    long offset; // 当前ziplist中的偏移地址
    int direction; // 迭代器的方向
} quicklistIter;
// 表示quicklist节点中ziplist里的一个节点结构
typedef struct quicklistEntry {
    const quicklist *quicklist;  // 指向所在quicklist的指针
    quicklistNode *node;  // 指向当前节点的指针
    unsigned char *zi;  // 指向当前节点的ziplist
    unsigned char *value;  // 当前指向的ziplist中的节点的字符串值
    long long longval;  // 当前指向的ziplist中的节点的整型值
    unsigned int sz;  // 当前指向的ziplist中的节点的字节大小
    int offset;  // 当前指向的ziplist中的节点相对于ziplist的偏移量
} quicklistEntry;

quicklist基本接口

Redis为每一个底层数据结构都提供了丰富的接口,以供上层数据结构调用,quicklist也不例外。为了篇幅不会过长,本博客仅仅剖析一下部分关键的接口的实现。

创建quicklist及其节点

创建一个quicklist需要为其设定各种参数,其由quicklistCreate函数实现。

quicklist *quicklistCreate(void) {
    struct quicklist *quicklist;  // 声明指针

    quicklist = zmalloc(sizeof(*quicklist));  // 分配内存
    quicklist->head = quicklist->tail = NULL;   // 设定头尾指针
    quicklist->len = 0;  // 设定长度
    quicklist->count = 0;  // 设定数据项总和
    quicklist->compress = 0;  // 设定压缩深度
    quicklist->fill = -2;  // 设定ziplist大小限定
    return quicklist;
}

创建完quicklist,接下来就是创建一个quicklist节点。

REDIS_STATIC quicklistNode *quicklistCreateNode(void) {
    quicklistNode *node;
    node = zmalloc(sizeof(*node));  //  申请内存
    node->zl = NULL;  // 初始化指向ziplist的指针
    node->count = 0;  // 初始化数据项个数
    node->sz = 0;  // 初始化ziplist大小
    node->next = node->prev = NULL;  // 初始化prev和next指针
    node->encoding = QUICKLIST_NODE_ENCODING_RAW;  // 初始化节点编码方式
    node->container = QUICKLIST_NODE_CONTAINER_ZIPLIST;  // 初始化存放数据的方式
    node->recompress = 0;  // 初始化再压缩标记
    return node;
}

PUSH操作

quicklist最重要的操作就是首尾插入节点,此操作由quicklistPush函数实现。PUSH操作不管是头部还是尾部压入都包含两个步骤:

  • 如果插入节点中的ziplist大小没有超过限制(list-max-ziplist-size),那么直接调用ziplistPush函数压入
  • 如果插入节点中的ziplist大小超过了限制,则新建一个quicklist节点(自然会创建一个新的ziplist),新的数据项会压入到新的ziplist,新的quicklist节点插入到原有的quicklist上
// push操作,需要判断是头部插入还是尾部插入
void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
                   int where) {
    if (where == QUICKLIST_HEAD) {
        quicklistPushHead(quicklist, value, sz);
    } else if (where == QUICKLIST_TAIL) {
        quicklistPushTail(quicklist, value, sz);
    }
}
// 将新的数据项push到头部
int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *orig_head = quicklist->head;
    // likely()是linux提供给程序员的编译优化方法
    // 目的是将“分支转移”的信息提供给编译器,这样编译器可以对代码进行优化,以减少指令跳转带来的性能下降
    // 此处表示节点没有满发生的概率比较大,也就是数据项直接插入到当前节点的可能性大,
    // likely()属于编译器级别的优化
    if (likely(
            // 判断该头部节点是否允许插入,计算头部节点中的大小和fill参数设置的大小相比较
            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
        // 执行到此,说明允许插入,直接调用ziplistpush插入节点即可
        quicklist->head->zl =
            ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
        // 更新头部大小
        quicklistNodeUpdateSz(quicklist->head);
    } else {
        // 执行到此,说明头部节点已经满了,需要重新创建一个节点
        quicklistNode *node = quicklistCreateNode();
        // 将新节点压入新创建的ziplist中,并与新创建的quicklist节点关联起来
        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
        // 更新大小
        quicklistNodeUpdateSz(node);
        // 将新创建的quicklist节点关联到quicklist中
        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
    }
    // 更新total数据项个数
    quicklist->count++;
    // 更新头结点的数据项个数
    quicklist->head->count++;
    // 如果尾部quicklist节点指针没变,返回0;
    // 反之返回1
    return (orig_head != quicklist->head);
}
// 将新数据项push到尾部
int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *orig_tail = quicklist->tail;
    if (likely(
            // 判断该尾部节点是否允许插入,计算尾部节点中的大小和fill参数设置的大小相比较
            _quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
        // 执行到此,说明允许插入,直接调用ziplistpush插入节点即可
        quicklist->tail->zl =
            // 将新数据项push到ziplist的尾部
            ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);
        // 更新尾部节点大小
        quicklistNodeUpdateSz(quicklist->tail);
    } else {
        // 执行到此,说明尾部节点已经满了,需要重新创建一个节点
        quicklistNode *node = quicklistCreateNode();
        // 创建一个新的ziplist,并将新数据项插入,然后与新创建的quicklist节点关联起来
        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);
        // 更新该quicklist节点的大小
        quicklistNodeUpdateSz(node);
        // 将新创建的quicklist与quicklist关联起来
        _quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
    }
    // 更新quicklist的数据项个数
    quicklist->count++;
    // 更新尾部节点的数据项个数
    quicklist->tail->count++;
    // 如果尾部quicklist节点指针没变,返回0;
    // 反之返回1
    return (orig_tail != quicklist->tail);
}

POP操作

与PUSH操作对应的是POP操作,POP操作可以弹出首尾节点。

// 接口函数,执行POP操作
// 执行成功返回1,反之0
// 如果弹出节点是字符串值,data,sz存放弹出节点的字符串值
// 如果弹出节点是整型值,slong存放弹出节点的整型值
int quicklistPop(quicklist *quicklist, int where, unsigned char **data,
                 unsigned int *sz, long long *slong) {
    unsigned char *vstr;
    unsigned int vlen;
    long long vlong;
    // 没有数据项,直接返回
    if (quicklist->count == 0)
        return 0;
    // 调用底层实现函数
    // 传入的_quicklistSaver是一个函数指针,用于深拷贝节点的值,用于返回
    int ret = quicklistPopCustom(quicklist, where, &vstr, &vlen, &vlong,
                                 _quicklistSaver);
    // 给data,sz,slong赋值
    if (data)
        *data = vstr;
    if (slong)
        *slong = vlong;
    if (sz)
        *sz = vlen;
    return ret;
}
// pop操作的底层实现函数
// 执行成功返回1,反之0
// 如果弹出节点是字符串值,data,sz存放弹出节点的字符串值
// 如果弹出节点是整型值,slong存放弹出节点的整型值
int quicklistPopCustom(quicklist *quicklist, int where, unsigned char **data,
                       unsigned int *sz, long long *sval,
                       void *(*saver)(unsigned char *data, unsigned int sz)) {
    unsigned char *p;
    unsigned char *vstr;
    unsigned int vlen;
    long long vlong;
    // 判断弹出位置,首部或者尾部
    int pos = (where == QUICKLIST_HEAD) ? 0 : -1;
    // 没有数据
    if (quicklist->count == 0)
        return 0;
    // 
    if (data)
        *data = NULL;
    if (sz)
        *sz = 0;
    if (sval)
        *sval = -123456789;
    // 获取quicklist节点
    quicklistNode *node;
    if (where == QUICKLIST_HEAD && quicklist->head) {
        node = quicklist->head;
    } else if (where == QUICKLIST_TAIL && quicklist->tail) {
        node = quicklist->tail;
    } else {
        return 0;
    }
    // 获取ziplist中的节点
    p = ziplistIndex(node->zl, pos);
    // 获取该节点的值
    if (ziplistGet(p, &vstr, &vlen, &vlong)) {
        // 如果是字符串值
        if (vstr) {
            if (data)
                // _quicklistSaver函数用于深拷贝取出返回值
                *data = saver(vstr, vlen);
            if (sz)
                *sz = vlen;  // 字符串的长度
        } else {
            // 如果存放的是整型值
            if (data)
                *data = NULL;  // 字符串设为NULL
            if (sval)
                *sval = vlong;  // 弹出节点的整型值
        }
        // 删除该节点
        quicklistDelIndex(quicklist, node, &p);
        return 1;
    }
    return 0;
}
// 返回一个字符串副本,深拷贝
// 这里深拷贝的用意是避免二次释放
REDIS_STATIC void *_quicklistSaver(unsigned char *data, unsigned int sz) {
    unsigned char *vstr;
    if (data) {
        vstr = zmalloc(sz);
        memcpy(vstr, data, sz);
        return vstr;
    }
    return NULL;
}

其他接口函数

Redis关于quicklist还提供了很多接口函数,这里只罗列出接口,没有具体实现。

// 在quicklist尾部追加指针zl指向的ziplist
void quicklistAppendZiplist(quicklist *quicklist, unsigned char *zl);
// 将ziplist数据转换成quicklist
quicklist *quicklistCreateFromZiplist(int fill, int compress,
                                      unsigned char *zl);
// 在node节点后添加一个值valiue
void quicklistInsertAfter(quicklist *quicklist, quicklistEntry *node,
                          void *value, const size_t sz);
// 在node节点前面添加一个值value
void quicklistInsertBefore(quicklist *quicklist, quicklistEntry *node,
                           void *value, const size_t sz);
// 删除ziplist节点entry
void quicklistDelEntry(quicklistIter *iter, quicklistEntry *entry);
// 翻转quicklist
void quicklistRotate(quicklist *quicklist);
// 返回quicklist列表中所有数据项的个数总和
unsigned int quicklistCount(quicklist *ql);
// 比较两个quicklist结构数据
int quicklistCompare(unsigned char *p1, unsigned char *p2, int p2_len);
// 从节点node中取出LZF压缩编码后的数据
size_t quicklistGetLzf(const quicklistNode *node, void **data);

quicklist小结

quicklist将sdlist和ziplist两者的优点结合起来,在时间和空间上做了一个均衡,能较大程度上提高Redis的效率。压入和弹出操作的时间复杂度都很理想。在源码中,还给出了很多接口函数,有兴趣的读者可以去quicklist.c文件中查看,如果对博客中的讲述觉得有问题的,可以在下方留言,多多交流,互相学习!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Redis源码剖析--快速列表quicklist 的相关文章

随机推荐

  • redhat 6.5(rhel-server-6.5-x86_x64)+oracle11gr2 rac

    VMware 9 0 43 redhat 6 5 rhel server 6 5 x86 x64 43 oracle11gr2 rac 很久没有关注ORACLE了 xff0c 官方发行版已经更新到12c了 xff0c 考虑到目前大部分用户还
  • OracleRAC管理 之 集群状态&信息查看

    OracleRAC管理 之 集群状态 amp 信息 查看 by 王磊 菜小小 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61
  • fgetc,fgets,getline用法

    Linux Ubuntu 下用法 在Ubuntu下shell中 xff0c man fgets可以看到fgetc fgets等用法 xff0c man getline可以看到getline用法 span class hljs comment
  • 使用PyQt4制作一个音乐播放器(1)

    1 前言 最近用Python给老妈写了一个处理excel表格的小软件 xff0c 因为自己平时用Python一般都是用在数值计算领域 xff0c 所以一般使用命令行的方式交互即可 但是给老妈用发现用命令行交互方式使用并不是很方便 xff0c
  • ubuntu18.04/16.04 忘记开机密码,咋办?

    自己的 笔记本 装了双系统 xff0c 很久没有ubuntu系统了 xff0c 密码竟然给忘记了 于是乎 xff0c 在网上找答案 精髓 大概就是在句话 xff1a recovery nomodeset 并将之删除 xff0c 然后下移一行
  • C++ 实现阻塞队列

    文章出处 xff0c 来源自地址 xff1a C 43 43 实现阻塞队列 文章 详细代码 xff1a include lt queue gt include lt thread gt include lt mutex gt include
  • 浏览器请求nodejs搭建的web服务器上的html文件时引用不了jQuery.js文件解决办法

    构建web服务器代码 nodejs构建 span class token comment 引入核心模块http和fs span span class token keyword var span http span class token
  • AI 到底是怎么「想」的?

    本文作者 xff1a kurffzhou xff0c 腾讯 TEG 安全工程师 最近 xff0c Nature发表了一篇关于深度学习系统被欺骗的新闻文章 xff0c 该文指出了对抗样本存在的广泛性和深度学习的脆弱性 xff0c 以及几种可能
  • kafka3.1 docker-compose方式安装(二)

    ZooKeeper 是为 Kafka 提供协调服务的工具 xff0c 所以得启动一个ZooKeeper 容器 配置文件 docker compose yml span class token key atrule version span
  • 效能优化实践:C/C++单元测试万能插桩工具

    作者 xff1a mannywang xff0c 腾讯安全平台后台开发 研发效能是一个涉及面很广的话题 xff0c 它涵盖了软件交付的整个生命周期 xff0c 涉及产品 架构 开发 测试 运维 xff0c 每个环节都可能影响顺畅 高质量地持
  • Windows使用cmd刷入recovery.img

    Windows使用cmd刷入recovery img Windows键 43 R回车后进入cmd命令终端 进入fastboot 手机进入fastboot模式有2种方法 第一种进入方法是 xff0c 如果你的手机能用adb识别到 xff0c
  • Linux系统安装环境桌面

    最近工作加班还有在弄大数据 xff0c 有段时间没有记录了 xff0c 后续有空的话整理下大数据的东西分享 xff0c 今天记录下Linux安装环境桌面 之前弄了个阿里云服务器玩玩 xff0c CentOS 7的版本 xff0c CentO
  • 【Linux学习笔记】关于ubuntu开机菜单栏和任务栏不见了的有效解决方法

    一 问题描述 ubuntu开机只有桌面 xff0c 没有菜单栏和任务栏 xff0c 如下图 xff1a 二 问题解决 刚学习ubuntu xff0c 总有些像我这样不折腾就不舒服的人 xff0c 今天改了一下主题 xff0c 图标什么的 x
  • 【数据结构与算法】深入浅出递归和迭代的通用转换思想

    深入浅出递归和迭代的通用转换思想 一般来说 xff0c 能用迭代的地方就不要用递归 xff01 理论上讲 xff0c 所有的递归和迭代之间都能相互转换 xff01 刷题碰到 一天一道LeetCode 130 Surrounded Regio
  • 【unix网络编程第三版】阅读笔记(二):套接字编程简介

    unp第二章主要将了TCP和UDP的简介 xff0c 这些在 TCP IP详解 和 计算机网络 等书中有很多细致的讲解 xff0c 可以参考本人的这篇博客 计算机网络 第五版 阅读笔记之五 xff1a 运输层 xff0c 这篇博客就不再赘述
  • 带你深入理解STL之Deque容器

    在介绍STL的deque的容器之前 xff0c 我们先来总结一下vector和list的优缺点 vector在内存中是分配一段连续的内存空间进行存储 xff0c 其迭代器采用原生指针即可 xff0c 因此其支持随机访问和存储 xff0c 支
  • 带你深入理解STL之Set和Map

    在上一篇博客 带你深入理解STL之RBTree中 xff0c 讲到了STL中关于红黑树的实现 xff0c 理解起来比较复杂 xff0c 正所谓前人种树 xff0c 后人乘凉 xff0c RBTree把树都种好了 xff0c 接下来就该set
  • Redis源码剖析--字符串t_string

    前面一直在分析Redis的底层数据结构 xff0c Redis利用这些底层结构设计了它面向用户可见的五种数据结构 xff0c 字符串 哈希 xff0c 链表 xff0c 集合和有序集合 xff0c 然后用redisObject对这五种结构进
  • 制作启动盘安装Ubuntu 18.04.1

    1 使用UltraISO制作启动盘 首先 xff0c 下载Ubuntu镜像文件 链接 xff1a https pan baidu com s 1saIJuLzAR5ojc7 F3EQ Q 提取码 xff1a 1hc1 打开UItralISO
  • Redis源码剖析--快速列表quicklist

    在RedisObject这一篇博客中 xff0c 有介绍到list结构的底层编码类型有OBJ ENCODING QUICKLIST xff0c 当时就发现这个底层数据结构被我遗漏了 昨天花了点时间补了补这个知识 xff0c 看完发现这货就跟