07 关于 quicklist
Posted 蓝风9
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了07 关于 quicklist相关的知识,希望对你有一定的参考价值。
前言
关于 redis 的数据结构 quicklist
相关介绍主要围绕着如下测试用例, 来看看 quicklist 的存储, 以及 相关的 api
本文的 quicklist 相关代码 拷贝自 redis-6.2.0
代码来自于 https://redis.io/
测试用例
//
// Created by Jerry.X.He on 2021/3/4.
//
#include<iostream>
#include "../libs/sds.h"
#include "../libs/ziplist.h"
#include "../libs/quicklist.h"
using namespace std;
// Test01QuicklistUsage.cpp
int main(int argc, char **argv) {
sds head = sdsnew("head");
sds tail = sdsnew("tail");
sds insertAfter = sdsnew("insertAfter");
sds ziplist = sdsnew("ziplist");
sds ziplist2 = sdsnew("ziplist2");
// quicklistCreate
quicklist *list = quicklistCreate();
for (int i = 0; i < 1; i++) {
quicklistPushHead(list, sdsfromlonglong(i), sdslen(sdsfromlonglong(i)));
}
// quicklistPushHead
quicklistPushHead(list, (void *) head, sdslen(head));
quicklistPushTail(list, (void *) tail, sdslen(tail));
// quicklistCount
int len = quicklistCount(list);
// quicklistIndex
quicklistEntry firstEntry;
int idxResult = quicklistIndex(list, 1, &firstEntry);
// quicklistInsertAfter
quicklistInsertAfter(list, &firstEntry, (void *) insertAfter, sdslen(insertAfter));
// quicklistAppendZiplist
unsigned char *zl = ziplistNew();
ziplistPush(zl, (unsigned char *) ziplist, sdslen(ziplist), ZIPLIST_HEAD);
ziplistPush(zl, (unsigned char *) ziplist2, sdslen(ziplist2), ZIPLIST_HEAD);
quicklistAppendZiplist(list, zl);
// quicklistGetIterator & quicklistNext
quicklistIter *iter = quicklistGetIterator(list, AL_START_HEAD);
quicklistEntry entry;
while (quicklistNext(iter, &entry)) {
if (entry.value)
cout << entry.value << endl;
else
cout << entry.longval << endl;
}
// quicklistDelEntry
quicklistIter *delIter = quicklistGetIterator(list, AL_START_HEAD);
quicklistDelEntry(delIter, &firstEntry);
// quicklistDelRange
int delResult = quicklistDelRange(list, 0, 3);
// quicklistCount
int newLen = quicklistCount(list);
int x = 0;
}
数据结构
quicklist 本身是一个双向链表, 然后链表中的每一个节点的数据结构为 quicklistNode
可以看到 quicklist 里面主要是存储了 头结点尾结点, 元素的数量, quicklistNode 的数量, 以及一些其他用于业务配置的选项
quicklistNode 为数据节点, 主要存储了 prev, next, 当前ziplist, 当前ziplist元素数量, 当前ziplist占用空间, 以及一些其他用于业务配置的选项
这整个数据结构就是 双向链表 + ziplist 的一个整合(能够限制ziplist的长度, 以避免ziplist元素过多的时候存在的弊端), 取其精华去其糟粕, 类似于 dict 是 数组 + 链表的整合
quicklistCreate
创建了一个 新的 quicklist, 并初始化
quicklistPushHead/quicklistPushTail
判断头结点 是否可以插入新元素, 如果可以直接把数据放到 quicklist->head 里面
否则新建一个 quciklistNode, 作为头结点, 并添加元素到 quicklist->head
更新 quicklist->count, quciklistNode->count
判断当前 quicklistNode 是否还可以插入 sz 字节的数据
估算新增当前节点的 prevLen + len 的长度, 为什么这么计算可以参见 关于 ziplist
计算当前 quicklistNode 的 ziplist 加上新的 元素 之后的字节数, 判断是否允许插入
这里 fill 为 -2, 需要确保 ziplist 的大小在 8192 以下
下面的 sizeMeetsSafetyLimit 也是对于 ziplist 的字节数的限定, 这里的默认的上线 SIZE_SAFETY_LIMIT 也是 8192
如果 fill 大于0, 意义为约束 ziplist 里面的元素的数量, 如果在约束范围内, 允许插入
如果是 ziplist 的大小 或者 数量不满足约束, 不允许再往当前节点插入数据
备注 : 请注意这里的判断顺序, 对于 size 的判断是优先于数量的判断的
我们来 inspect 一下一个简单的 quicklist
到断点的位置, 我们看一下下面的 list, 可以看到 是有两个元素, 一个quicklistNode, fill 默认为 -2(限定 ziplist 大小最大为 8192)
我们再来看 head(tail是同一个节点), 因为只有一个节点因此 prev, next 为 null, zl 指向存储数据的 ziplist, sz 为 ziplist 的字节数, 19字节, count 为ziplist的元素的数量 2个
再下面 我们 inspect 一下 list->head->zl 的数据
我们来 inspect 一下 list->head->zl
可以发现 zlBytes 为 19, zltail 为 16, zllen 为 2
第一个 entry 的 prevLen 为 0, len 为 4, 接下来四个字节为数据 "first"
第二个 entry 的 prevLen 为 6, len + 数据约束为 : 0b11110001 存储的是整数 0, 长度为 1字节
接下来 0x7ff441500062 为 ziplist 的结束标记 0xff
(lldb) x 0x7ff441500050
0x7ff441500050: 13 00 00 00 10 00 00 00 02 00 00 04 68 65 61 64 ............head
0x7ff441500060: 06 f1 ff 41 f4 7f 00 00 e0 09 50 41 f4 7f 00 00 .��A�...�.PA�...
quicklistCount
统计元素的数量的方式为直接从 quicklist 的元数据里面直接获取
quicklistIndex
约束 idx > 0 为正向遍历, index < 0 为反向遍历, 以第一个元素为 基准
首先是 正向/反向 遍历 quicklistNode, 找到 idx 所在的 quicklistNode[节点上面记录了当前节点的元素数量]
然后再获取 idx, 相对于当前 quicklistNode 的索引, 最终通过 ziplistGet 获取 idx 所对应的 entry 的信息
整个过程中会设置 entry 的 quicklist, node, offset, zi[当前entry], 以及 entry 存储的数据
quicklistInsert
在给定的 entry 之前或者之后增加 给定的元素
如果 entry 对应的 node 不存在, 则视为 quicklist 为空, 新建节点 并添加到 quicklist
判断当前节点是否还可以添加给定的元素, 标记为 full
如果是在 entry 之后添加元素, 并且 entry 是当前 node 的最后一个节点, 标记 at_tail, 判断下一个节点是否还可以添加元素, 标记为 full_next
如果是在 entry 之前添加元素, 并且 entry 是当前 node 的第一个节点, 标记 at_head, 判断前一个节点是否还可以添加元素, 标记为 full_prev
如果当前节点还可以添加元素 "if(!full)", 则添加给定的元素到 entry 之前/之后
如果当前节点已经满了, 并且下一个节点可添加元素, 并且是需要将元素添加到 entry 之后, 则将新元素添加到下一个节点的头部
如果当前节点已经满了, 并且上一个节点可添加元素, 并且是需要将元素添加到 entry 之前, 则将新元素添加到上一个节点的尾部
如果当前节点已经满了, 并且 ((下一个节点已经满了, 并且是需要将元素添加到 entry 之后) 或者 (上一个节点已经满了, 并且是需要将元素添加到 entry 之前)), 则新建一个 quicklistNode, 并添加给定的元素
否则 spliit 当前节点为两部分, 待插入的业务数据是放在拆分之后后半部分的节点里面
更新 quicklist->count
quicklistAppendZiplist
新建一个 quicklistNode, 并根据传入的 ziplist 初始化
将新建的 quicklistNode 放到 quicklist->tail 之后, 更新 quicklist->tail
更新 quicklist->count
quicklistGetIterator
创建 quicklistIter, 并初始化 current 节点, offset, direction, quicklist, zi[当前 entry]
quicklistNext
如果 iter->zi 未初始化, 根据 iter->current, iter->offset 初始化 iter-> zi[第一次迭代的时候]
否则 迭代 iter->zi 为下一个 entry[next/prev], 更新 iter->offset
如果 iter->zi[entry 存在], 则获取这个 entry 的数据
否则 是遍历完了当前 ziplist 的元素, 根据 direction 切换到下一个[next/prev] ziplist
quicklistDelEntry
删除 quicklist 下面的 给定的 entry 对应的的元素
从当前 quicklistNode 中删除 entry 对应的元素, 如果删除之后 quicklistNode 为空, 则删除当前 quicklistNode, 更新节点元素的数量, 更新 quicklist->count
如果当前元素的删除, 导致了quicklistNode的删除, 则切换 iter 的 current, offset
quicklistDelRange
先计算需要删除的元素的数量是否合理, 如果不合理 补偿更新待删除元素的数量
接着迭代 quicklistNode 删除元素
这里为了效率拆分了几种情况
1. offset 为 0, 待删除的元素大于当前 quicklistNode 的元素, 直接整个 entry 删掉
2. offset > 0, 待删除的元素大于当前 quicklistNode 的元素, 删除当前 entry 之后的所有元素
3. offset < 0[定位方式不同而已], 删除当前 entry 之后的所有元素
4. offset > 0, 待删除的元素小于当前 quicklistNode 的元素, 删除待删除元素个元素
对于 range 跨越多个 quicklistNode 的情况, 一般就是第一个节点需要删除 entry 之后的所有元素, 中间的节点需要整除整个节点, 最后一个节点删除 前面的一部分元素[数量取决于实际的情况]
对于整个 quicklistNode 删除的情况, 从双向链表中删除 quicklistNode, 并更新 quicklist 的元数据
对于部分删除 quicklistNode 的情况, 更新当前 quicklistNode 的 count, 更新 quicklist->count
完
以上是关于07 关于 quicklist的主要内容,如果未能解决你的问题,请参考以下文章
关于片段生命周期,何时调用片段的 onActivityResult?
Redis核心原理与实践--列表实现原理之quicklist结构