在上一篇文章《Redis列表实现原理之ziplist结构》,我们分析了ziplist结构如何使用一块完整的内存存储列表数据。

同时也提出了一个问题:如果链表很长,ziplist中每次插入或删除节点时都需要进行大量的内存拷贝,这个性能是无法接受的。

本文分析quicklist结构如何解决这个问题,并实现Redis的列表类型。

quicklist的设计思想很简单,将一个长ziplist拆分为多个短ziplist,避免插入或删除元素时导致大量的内存拷贝。

ziplist存储数据的形式更类似于数组,而quicklist是真正意义上的链表结构,它由quicklistNode节点链接而成,在quicklistNode中使用ziplist存储数据。

提示:本文以下代码如无特殊说明,均位于quicklist.h/quicklist.c中。

本文以下说的“节点”,如无特殊说明,都指quicklistNode节点,而不是ziplist中的节点。

定义

quicklistNode的定义如下:

typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl;
unsigned int sz;
unsigned int count : 16;
unsigned int encoding : 2;
unsigned int container : 2;
unsigned int recompress : 1;
unsigned int attempted_compress : 1;
unsigned int extra : 10;
} quicklistNode;
  • prev、next:指向前驱节点,后驱节点。
  • zl:ziplist,负责存储数据。
  • sz:ziplist占用的字节数。
  • count:ziplist的元素数量。
  • encoding:2代表节点已压缩,1代表没有压缩。
  • container:目前固定为2,代表使用ziplist存储数据。
  • recompress:1代表暂时解压(用于读取数据等),后续需要时再将其压缩。
  • extra:预留属性,暂未使用。

当链表很长时,中间节点数据访问频率较低。这时Redis会将中间节点数据进行压缩,进一步节省内存空间。Redis采用是无损压缩算法—LZF算法。

压缩后的节点定义如下:

typedef struct quicklistLZF {
unsigned int sz;
char compressed[];
} quicklistLZF;
  • sz:压缩后的ziplist大小。
  • compressed:存放压缩后的ziplist字节数组。

quicklist的定义如下:

typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count;
unsigned long len;
int fill : QL_FILL_BITS;
unsigned int compress : QL_COMP_BITS;
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
  • head、tail:指向头节点、尾节点。
  • count:所有节点的ziplist的元素数量总和。
  • len:节点数量。
  • fill:16bit,用于判断节点ziplist是否已满。
  • compress:16bit,存放节点压缩配置。

quicklist的结构如图2-5所示。

操作分析

插入元素到quicklist头部:

int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
quicklistNode *orig_head = quicklist->head;
// [1]
if (likely(
_quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
// [2]
quicklist->head->zl =
ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
// [3]
quicklistNodeUpdateSz(quicklist->head);
} else {
// [4]
quicklistNode *node = quicklistCreateNode();
node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD); quicklistNodeUpdateSz(node);
_quicklistInsertNodeBefore(quicklist, quicklist->head, node);
}
quicklist->count++;
quicklist->head->count++;
return (orig_head != quicklist->head);
}

参数说明:

  • value、sz:插入元素的内容与大小。

【1】判断head节点ziplist是否已满,_quicklistNodeAllowInsert函数中根据quicklist.fill属性判断节点是否已满。

【2】head节点未满,直接调用ziplistPush函数,插入元素到ziplist中。

【3】更新quicklistNode.sz属性。

【4】head节点已满,创建一个新节点,将元素插入新节点的ziplist中,再将该节点头插入quicklist中。

也可以在quicklist的指定位置插入元素:

REDIS_STATIC void _quicklistInsert(quicklist *quicklist, quicklistEntry *entry,
void *value, const size_t sz, int after) {
int full = 0, at_tail = 0, at_head = 0, full_next = 0, full_prev = 0;
int fill = quicklist->fill;
quicklistNode *node = entry->node;
quicklistNode *new_node = NULL;
...
// [1]
if (!_quicklistNodeAllowInsert(node, fill, sz)) {
full = 1;
} if (after && (entry->offset == node->count)) {
at_tail = 1;
if (!_quicklistNodeAllowInsert(node->next, fill, sz)) {
full_next = 1;
}
} if (!after && (entry->offset == 0)) {
at_head = 1;
if (!_quicklistNodeAllowInsert(node->prev, fill, sz)) {
full_prev = 1;
}
}
// [2]
...
}

参数说明:

  • entry:quicklistEntry结构,quicklistEntry.node指定元素插入的quicklistNode节点,quicklistEntry.offset指定插入ziplist的索引位置。
  • after:是否在quicklistEntry.offset之后插入。

【1】根据参数设置以下标志。

  • full:待插入节点ziplist是否已满。
  • at_tail:是否ziplist尾插。
  • at_head:是否ziplist头插。
  • full_next:后驱节点是否已满。
  • full_prev:前驱节点是否已满。

提示:头插指插入链表头部,尾插指插入链表尾部。

【2】根据上面的标志进行处理,代码较烦琐,这里不再列出。

这里的执行逻辑如表2-2所示。

条件 条件说明 处理方式
!full && after 待插入节点未满,ziplist尾插 再次检查ziplist插入位置是否存在后驱元素,如果不存在则调用ziplistPush函数插入元素(更快),否则调用ziplistInsert插入元素
!full && !after 待插入节点未满,非ziplist尾插 调用ziplistInsert函数插入元素
full && at_tail && node -> next && !full_next && after 待插入节点已满,尾插,后驱节点未满 将元素插入后驱节点ziplist中
full && at_head && node -> prev && !full_prev && !after 待插入节点已满,ziplist头插,前驱节点未满 将元素插入前驱节点ziplist中
full && ((at_tail && node -> next && full_next && after) ||(at_head && node->prev && full_prev && !after)) 待插入节点已满,尾插且后驱节点已满,或者头插且前驱节点已满 构建一个新节点,将元素插入新节点,并根据after参数将新节点插入quicklist中
full 待插入节点已满,并且在节点ziplist中间插入 将插入节点的数据拆分到两个节点中,再插入拆分后的新节点中

我们只看最后一种场景的实现:

    // [1]
quicklistDecompressNodeForUse(node);
// [2]
new_node = _quicklistSplitNode(node, entry->offset, after);
new_node->zl = ziplistPush(new_node->zl, value, sz,
after ? ZIPLIST_HEAD : ZIPLIST_TAIL);
new_node->count++;
quicklistNodeUpdateSz(new_node);
// [3]
__quicklistInsertNode(quicklist, node, new_node, after);
// [4]
_quicklistMergeNodes(quicklist, node);

【1】如果节点已压缩,则解压节点。

【2】从插入节点中拆分出一个新节点,并将元素插入新节点中。

【3】将新节点插入quicklist中。

【4】尝试合并节点。_quicklistMergeNodes尝试执行以下操作:

  • 将node->prev->prev合并到node->prev。
  • 将node->next合并到node->next->next。
  • 将node->prev合并到node。
  • 将node合并到node->next。

    合并条件:如果合并后节点大小仍满足quicklist.fill参数要求,则合并节点。

    这个场景处理与B+树的节点分裂合并有点相似。

quicklist常用的函数如表2-3所示。

函数 作用
quicklistCreate、quicklistNew 创建一个空的quicklist
quicklistPushHead,quicklistPushTail 在quicklist头部、尾部插入元素
quicklistIndex 查找给定索引的quicklistEntry节点
quicklistDelEntry 删除给定的元素

配置说明

  • list-max-ziplist-size:配置server.list_max_ziplist_size属性,该值会赋值给quicklist.fill。取正值,表示quicklist节点的ziplist最多可以存放多少个元素。例如,配置为5,表示每个quicklist节点的ziplist最多包含5个元素。取负值,表示quicklist节点的ziplist最多占用字节数。这时,它只能取-1到-5这五个值(默认值为-2),每个值的含义如下:

    -5:每个quicklist节点上的ziplist大小不能超过64 KB。

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

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

    -2:每个quicklist节点上的ziplist大小不能超过8 KB。

    -1:每个quicklist节点上的ziplist大小不能超过4 KB。
  • list-compress-depth:配置server.list_compress_depth属性,该值会赋值给quicklist.compress。

    0:表示节点都不压缩,Redis的默认配置。

    1:表示quicklist两端各有1个节点不压缩,中间的节点压缩。

    2:表示quicklist两端各有2个节点不压缩,中间的节点压缩。

    3:表示quicklist两端各有3个节点不压缩,中间的节点压缩。

    以此类推。

编码

ziplist由于结构紧凑,能高效使用内存,所以在Redis中被广泛使用,可用于保存用户列表、散列、有序集合等数据。

列表类型只有一种编码格式OBJ_ENCODING_QUICKLIST,使用quicklist存储数据(redisObject.ptr指向quicklist结构)。列表类型的实现代码在t_list.c中,读者可以查看源码了解实现更多细节。

总结

  • ziplist是一种结构紧凑的数据结构,使用一块完整内存存储链表的所有数据。
  • ziplist内的元素支持不同的编码格式,以最大限度地节省内存。
  • quicklist通过切分ziplist来提高插入、删除元素等操作的性能。
  • 链表的编码格式只有OBJ_ENCODING_QUICKLIST。

本文内容摘自作者新书《Redis核心原理与实践》,这本书深入地分析了Redis常用特性的内部机制与实现方式,大部分内容源自对Redis源码的分析,并从中总结出设计思路、实现原理。通过阅读本书,读者可以快速、轻松地了解Redis的内部运行机制。

经过该书编辑同意,我会继续在个人技术公众号(binecy)发布书中部分章节内容,作为书的预览内容,欢迎大家查阅,谢谢。

Redis核心原理与实践--列表实现原理之quicklist结构的更多相关文章

  1. Redis核心原理与实践--列表实现原理之ziplist

    列表类型可以存储一组按插入顺序排序的字符串,它非常灵活,支持在两端插入.弹出数据,可以充当栈和队列的角色. > LPUSH fruit apple (integer) 1 > RPUSH ...

  2. Redis核心原理与实践--字符串实现原理

    Redis是一个键值对数据库(key-value DB),下面是一个简单的Redis的命令: > SET msg "hello wolrd" 该命令将键"msg&q ...

  3. Redis核心原理与实践--散列类型与字典结构实现原理

    Redis散列类型可以存储一组无序的键值对,它特别适用于存储一个对象数据. > HSET fruit name apple price 7.6 origin china 3 > HGET ...

  4. 分布式开放消息系统(RocketMQ)的原理与实践(转)

    转自:http://www.jianshu.com/p/453c6e7ff81c 分布式消息系统作为实现分布式系统可扩展.可伸缩性的关键组件,需要具有高吞吐量.高可用等特点.而谈到消息系统的设计,就回 ...

  5. 分布式消息中间件rocketmq的原理与实践

    RocketMQ作为阿里开源的一款高性能.高吞吐量的消息中间件,它是怎样来解决这两个问题的?RocketMQ 有哪些关键特性?其实现原理是怎样的? 关键特性以及其实现原理 一.顺序消息 消息有序指的是 ...

  6. 20165223《网络对抗技术》Exp3 免杀原理与实践

    目录 -- 免杀原理与实践 免杀原理与实践 本次实验任务 基础知识问答 免杀扫描引擎 实验内容 正确使用msf编码器,msfvenom生成jar等文件,veil-evasion,加壳工具,使用shel ...

  7. 2018-2019-2 网络对抗技术 20165232 Exp3 免杀原理与实践

    2018-2019-2 网络对抗技术 20165232 Exp3 免杀原理与实践 免杀原理及基础问题回答 一.免杀原理 一般是对恶意软件做处理,让它不被杀毒软件所检测.也是渗透测试中需要使用到的技术. ...

  8. 2018-2019-2 网络对抗技术 20165232 Exp2 后门原理与实践

    2018-2019-2 网络对抗技术 20165232 Exp2 后门原理与实践 1. 后门原理与实践实验说明及预备知识 一.实验说明 任务一:使用netcat获取主机操作Shell,cron启动 ( ...

  9. 2018-2019-2 网络对抗技术 20165311 Exp3 免杀原理与实践

    2018-2019-2 网络对抗技术 20165311 Exp3 免杀原理与实践 免杀原理及基础问题回答 实验内容 任务一:正确使用msf编码器,msfvenom生成如jar之类的其他文件,veil- ...

随机推荐

  1. NTP 集群简略部署指南

    NTP 集群简略部署指南 by 无若 1. NTP 简介 网络时间协议(英语:Network Time Protocol,简称NTP)是在数据网络潜伏时间可变的计算机系统之间通过分组交换进行时钟同步的 ...

  2. MySQL-15-主从复制

    企业高可用性标准 1 全年无故障率(非计划内故障停机) 99.9% ----> 0.001*365*24*60=525.6 min 99.99% ----> 0.0001*365*24*6 ...

  3. BUUCTF刷题系列(1)5.25日记

    前面的题目都不太难,就直接从SQL注入开始了. 这个样子的话,明显就是注入,我们先拿出SQL语句:http://fb415201-6634-4fc3-a6bc-a67b84ea1ed2.node3.b ...

  4. Django ORM多表查询

    基于双下划线查询 根据存的时候,字段的数据格式衍生的查询方法 1.年龄大于35岁 res = models.AuthorDetails.objects.filter(age__lt=80) print ...

  5. Centos7-编译安装zlib

    1.解压并进入zlib目录tar xf zlib-1.2.11.tar.gz cd zlib-1.2.11 2.查看编辑参数[root@manage zlib-1.2.11]#./configure ...

  6. NOIP 模拟 $23\; \rm 联$

    题解 \(by\;zj\varphi\) 区间上的问题,一般都用线段树来解决(但是这题也可以用 \(\rm ODT\)) 对于每段段区间设置三个参数,分别表示这个区间是否只有 \(1\) 或 \(0\ ...

  7. JavaWeb学习笔记(五)

    本文内容 1. JSP: 1. 指令 2. 注释 3. 内置对象 2. MVC开发模式 3. EL表达式 4. JSTL标签 5. 三层架构 JSP: 1. 指令 * 作用:用于配置JSP页面,导入资 ...

  8. VirtualBox-虚拟硬盘扩容-win7

    问题: 我在VirtualBox下搭建的win7系统只设置了一个C盘,当初只给硬盘分配32G,如今深受生活的毒打,发现只剩5G可用,装个PS都费劲. 我要扩容,扩容! 当前环境: VirtualBox ...

  9. npm常用命令及其node相关工具汇总

    它是一个事件驱动异步I/O单进程的服务端JS环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好. 浏览器是JS的前端运行环境. Node.js是JS的后端运行环 ...

  10. 你真的了解JS里的"new"吗?

    我们常常喜欢用new关键字去创建一些对象如new Vue(),但是这个关键字的背后究竟做了什么其实没太多人去关注. 想象我们是苹果公司,要生产30部iPod,规定: 每台iPod都会有自己的ID 每台 ...