一 序
书上这一节内容不是很多,3.0版本还是侧重讲了ziplist到linkedlist的实现。3.2已经用quiklist替代了。打算分两部分来整理。先看quicklist本身的结构特性,下一篇再看list的命令实现,
二 quicklist
先说下背景吧,就是为啥用quicklist:
1 考虑到链表的附加空间相对太高,prev 和 next 指针就要占去 16 个字节 (64bit 系统的指针是 8 个字节),另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管理效率。
2. 为啥不都永ziplist,之前ziplist在我们程序里面来看将会是一块连续的内存块。它使用内存偏移来保存next从而节约了next指针。这样造成了我们每一次的删除插入操作都会进行remalloc,从而分配一块新的内存块。很大的连续内存不方便申请.,还有ziplist有连锁更新的问题,短了没问题,长了就有风险。所以quicklist采用了对ziplist进行一次封装,使用小块的ziplist来既保证了少使用内存,也保证了性能。
可以理解是 zipList 和 linkedList 的混合体,它将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。看下结构代码,源码在quicklist.h
/* quicklist is a 32 byte struct (on 64-bit systems) describing a quicklist.
* 'count' is the number of total entries.
* 'len' is the number of quicklist nodes.
* 'compress' is: -1 if compression disabled, otherwise it's the number
* of quicklistNodes to leave uncompressed at ends of quicklist.
* 'fill' is the user-requested (or default) fill factor. */
typedef struct quicklist {
quicklistNode *head; //指向头部(最左边)quicklist节点的指针
quicklistNode *tail; //指向尾部(最右边)quicklist节点的指针
//ziplist中的entry节点计数器
unsigned long count; /* total count of all entries in all ziplists */
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;
在quicklist表头结构中,有两个成员是fill和compress,其中” : “是位域运算符,表示fill占int类型32位中的16位,compress也占16位。
fill和compress的配置文件是redis.conf。
fill成员对应的配置:list-max-ziplist-size -2 (就是8KB ,超出了这个字节数,就会新起一个 ziplist。)
当数字为负数,表示以下含义:
- -1 每个quicklistNode节点的ziplist字节大小不能超过4kb。(建议)
- -2 每个quicklistNode节点的ziplist字节大小不能超过8kb。(默认配置)
- -3 每个quicklistNode节点的ziplist字节大小不能超过16kb。(一般不建议)
- -4 每个quicklistNode节点的ziplist字节大小不能超过32kb。(不建议)
- -5 每个quicklistNode节点的ziplist字节大小不能超过64kb。(正常工作量不建议)
当数字为正数,表示:ziplist结构所最多包含的entry个数。最大值为 215215。
compress成员对应的配置:list-compress-depth 0
后面的数字有以下含义:
- 0 表示不压缩。(默认)
- 1 表示quicklist列表的两端各有1个节点不压缩,中间的节点压缩。
- 2 表示quicklist列表的两端各有2个节点不压缩,中间的节点压缩。
- 3 表示quicklist列表的两端各有3个节点不压缩,中间的节点压缩。
以此类推,最大为 216216。
为了支持快速的 push/pop 操作,quicklist 的首尾两个 ziplist 不压缩,此时深度就是 1。如果深度为 2,就表示 quicklist 的首尾第一个 ziplist 以及首尾第二个 ziplist 都不压缩。
再看下节点结构:
/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
* We use bit fields keep the quicklistNode at 32 bytes.
* count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
* encoding: 2 bits, RAW=1, LZF=2.
* container: 2 bits, NONE=1, ZIPLIST=2.
* recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
* attempted_compress: 1 bit, boolean, used for verifying during testing.
* extra: 12 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
struct quicklistNode *prev; //前驱节点指针
struct quicklistNode *next; //后继节点指针
unsigned char *zl; //指针,可指向zilist或者quicklist
//ziplist中包的节点数,占16 bits长度
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结构保存数据,1表示没压缩,2表示压缩了,默认是2,占2bits长度
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
//标记quicklist节点的ziplist之前是否被解压缩过,占1bit长度
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
当指定使用lzf压缩算法压缩ziplist的entry节点时,quicklistNode结构的zl成员指向quicklistLZF结构
/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
* 'sz' is byte length of 'compressed' field.
* 'compressed' is LZF data with total (compressed) length 'sz'
* NOTE: uncompressed length is stored in quicklistNode->sz.
* When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
typedef struct quicklistLZF {
unsigned int sz; /* LZF size in bytes*///表示被LZF算法压缩后的ziplist的大小
char compressed[]; //保存压缩后的ziplist的数组,柔性数组
} quicklistLZF;
三 quicklist的操作
3.1 创建quicklist
源码在quicklist.c
/* Create a new quicklist.
* Free with quicklistRelease(). */
quicklist *quicklistCreate(void) {
struct quicklist *quicklist;
quicklist = zmalloc(sizeof(*quicklist)); // 分配空间
quicklist->head = quicklist->tail = NULL; // 头尾指针为空
quicklist->len = 0; // 列表长度为0
quicklist->count = 0; // 数据项总和为0
quicklist->compress = 0; // 设定压缩深度
quicklist->fill = -2; // 设定ziplist大小
return quicklist;
}
quicklist中的节点创建
REDIS_STATIC quicklistNode *quicklistCreateNode(void) {
quicklistNode *node;
node = zmalloc(sizeof(*node)); // 分配空间
node->zl = NULL; // 初始化指向ziplist的指针
node->count = 0; // 数据项个数为0
node->sz = 0; // ziplist大小为0
node->next = node->prev = NULL; // 前后指针为空
node->encoding = QUICKLIST_NODE_ENCODING_RAW; // 节点编码方式
node->container = QUICKLIST_NODE_CONTAINER_ZIPLIST; // 数据存放方式
node->recompress = 0; // 初始化压缩标记
return node;
}
3.2 push
/* Wrapper to allow argument-based switching between HEAD/TAIL pop */
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);
}
}
/* Add new entry to head node of quicklist.
*
* Returns 0 if used existing head.
* Returns 1 if new head created. */
int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
quicklistNode *orig_head = quicklist->head;
if (likely(// 判断头部节点是否能够插入新元素
_quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
quicklist->head->zl =
// 如果能够插入,则执行ziplistPush插入新元素的当前节点的ziplist
ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
// 更新头部大小
quicklistNodeUpdateSz(quicklist->head);
} else {
// 否则的话,需要创建新的quicklist节点
quicklistNode *node = quicklistCreateNode();
// 将新节点压入新创建的ziplist中,并与新创建的quicklist节点关联起来
node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
// 更新头部信息
quicklistNodeUpdateSz(node);
// 将新创建的节点插入到quicklist中
_quicklistInsertNodeBefore(quicklist, quicklist->head, node);
}
quicklist->count++; // 更新quicklist的数据项个数
quicklist->head->count++; // 更新头结点的数据项个数
return (orig_head != quicklist->head); //返回判断头部节点是否新创建的
}
/* Add new entry to tail node of quicklist.
* 插入尾部类似,不在注释
* Returns 0 if used existing tail.
* Returns 1 if new tail created. */
int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
quicklistNode *orig_tail = quicklist->tail;
if (likely(
_quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
quicklist->tail->zl =
ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);
quicklistNodeUpdateSz(quicklist->tail);
} else {
quicklistNode *node = quicklistCreateNode();
node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);
quicklistNodeUpdateSz(node);
_quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
}
quicklist->count++;
quicklist->tail->count++;
return (orig_tail != quicklist->tail);
}
插入尾部节点与头部节点类似,不在一一注释,再看看内部调用函数。
/* Wrappers for node inserting around existing node. */
REDIS_STATIC void _quicklistInsertNodeBefore(quicklist *quicklist,
quicklistNode *old_node,
quicklistNode *new_node) {
__quicklistInsertNode(quicklist, old_node, new_node, 0);
}
REDIS_STATIC void _quicklistInsertNodeAfter(quicklist *quicklist,
quicklistNode *old_node,
quicklistNode *new_node) {
__quicklistInsertNode(quicklist, old_node, new_node, 1);
}
都调用了插入节点:
/* Insert 'new_node' after 'old_node' if 'after' is 1.
* Insert 'new_node' before 'old_node' if 'after' is 0.
* Note: 'new_node' is *always* uncompressed, so if we assign it to
* head or tail, we do not need to uncompress it. */
REDIS_STATIC void __quicklistInsertNode(quicklist *quicklist,
quicklistNode *old_node,
quicklistNode *new_node, int after) {
if (after) {//插入方式 后
new_node->prev = old_node; //设置节点关系,链表指针的常见操作
if (old_node) {//如果原来的节点存在
new_node->next = old_node->next;
if (old_node->next)
old_node->next->prev = new_node;
old_node->next = new_node;
}
if (quicklist->tail == old_node)//尾节点处理
quicklist->tail = new_node;
} else { //前插入
new_node->next = old_node; //新节点指向原来的
if (old_node) {//老节点存在。处理指向关系
new_node->prev = old_node->prev;
if (old_node->prev)
old_node->prev->next = new_node;
old_node->prev = new_node;
}
if (quicklist->head == old_node)//头结点处理
quicklist->head = new_node;
}
/* If this insert creates the only element so far, initialize head/tail. */
if (quicklist->len == 0) {//第一次加入 设置成头和尾
quicklist->head = quicklist->tail = new_node;
}
if (old_node)//设置压缩
quicklistCompress(quicklist, old_node);
quicklist->len++;
}
看主流程哈,一些细节没有一一展开。
相对于这种标准的双向链表来说,插入就变得熟悉。首先是确定插入头或者尾,插入的时候先判断该节点能否插入(这里就会牵扯到之前的配置,长度超没限制等等),具体的插入就是维护节点的双向关系。
查找
获取值最麻烦的地方在于需要解压ziplist
,目前Redis使用的是lzf
压缩算法(也可以说是个编码算法),要注意的是quicklist
中的获取值都是指获取真实的数据项的值,也就是存储在各个ziplist
中的数据项,而不是指quicklistNode
。
/* Populate 'entry' with the element at the specified zero-based index
* where 0 is the head, 1 is the element next to head
* and so on. Negative integers are used in order to count
* from the tail, -1 is the last element, -2 the penultimate
* and so on. If the index is out of range 0 is returned.
*
* Returns 1 if element found
* Returns 0 if element not found */
int quicklistIndex(const quicklist *quicklist, const long long idx,
quicklistEntry *entry) {
quicklistNode *n;
unsigned long long accum = 0;
unsigned long long index;
int forward = idx < 0 ? 0 : 1; /* < 0 -> reverse, 0+ -> forward */ // 小于0从后往前搜索
initEntry(entry); // 这里会对entry设置一些初始值
entry->quicklist = quicklist;
if (!forward) { // 从尾部开始遍历
index = (-idx) - 1;
n = quicklist->tail;
} else {
index = idx;
n = quicklist->head;
}
// 如果指定位置超出了链表本身长度,直接返回0
if (index >= quicklist->count)
return 0;
while (likely(n)) {//厉害啊,还有这种方法,当条件为真的可能性很大时使用该写法可以提高执行效率,
// 这个循环只能算出想要的节点在哪个ziplist中,后续再从ziplist取出真正节点
if ((accum + n->count) > index) {
break;
} else {
D("Skipping over (%p) %u at accum %lld", (void *)n, n->count,
accum);//看这个像是debug那种们便于分析的
// 每个快速列表的节点都记录了它附带的ziplist中的节点个数
accum += n->count;
n = forward ? n->next : n->prev;//下一个node
}
}
if (!n)// 如果没有找到指定节点则返回失败
return 0;
D("Found node: %p at accum %llu, idx %llu, sub+ %llu, sub- %llu", (void *)n,
accum, index, index - accum, (-index) - 1 + accum);
entry->node = n;//设置节点
// 设置在当前ziplist中还要偏移多少个位置才是真正的数据节点
if (forward) {
/* forward = normal head-to-tail offset. */
entry->offset = index - accum;//位置 即ziplist的index
} else {
/* reverse = need negative offset for tail-to-head, so undo
* the result of the original if (index < 0) above. */
entry->offset = (-index) - 1 + accum;//没太看懂
}
// 解压当前节点的ziplist,由于是将该节点给调用者使用,所以解压之后不再重新压缩
// 由调用者根据重压缩标志决定是否需要再压缩
quicklistDecompressNodeForUse(entry->node);
// 获取实际的数据节点首部指针
entry->zi = ziplistIndex(entry->node->zl, entry->offset);
ziplistGet(entry->zi, &entry->value, &entry->sz, &entry->longval);//获取节点的值 反编码
/* The caller will use our result, so we don't re-compress here.
* The caller can recompress or delete the node as needed. */
return 1;
}
对于逆序的查找,返回的是-值,是不是倒着数是对应的值?不太确定。答题步骤就是要先根据我们每个node的个数,从而找到对应的ziplist,调用ziplist的index就能成功找到。
删除得整理吧。困了。、。。每天都有点滴收获。
参考:
https://www.jianshu.com/p/2d0f4833470f