redis源码学习-quicklist篇

quicklist介绍

quicklist为基于zpilist做了一层优化的链表结构,quicklist为双端链表,每个节点指向独自的ziplist,在有着ziplist高效内存效率的情况下,适当优化了ziplist的增删效率。
本篇部分依赖ziplist,忘记的同学先移步回顾下

quicklist分析

quicklist出现的原因在于,ziplist虽然有着极高的内存使用率,但也因此,在查找时只能单循环遍历,增删的时候有着极高的连锁更新的可能,最坏情况下的时间复杂度在O(n^2)。这种操纵效率相对比sds,dict或zskiplist等其他底层数据结构来说,实在是对不起‘redis’的名头。也因此,引入quicklist对这个数据结构做部分优化,降低更新复杂度,保持内存使用率。
如图

每个quicklistnode都会指向一个单独的ziplist,同时包含next和last的quicklistnode地址,
看图的话,会发现quicklist非常简单,总体实现来说也很简单,就不多做介绍了
quicklist数据结构

typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* total count of all entries in all ziplists */
    unsigned long len;          /* number of quicklistNodes */
    int fill : QL_FILL_BITS;              /* fill factor for individual nodes */
    unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
    unsigned int bookmark_count: QL_BM_BITS;
    quicklistBookmark bookmarks[];
} quicklist;

head为头节点,tail为尾节点,count记录的是所有的ziplist中的entry总数,len为quicklist节点总数。fill为16位,代表每个节点的最大容量,compress为16位,quicklist的压缩深度,与之后的LZF压缩相关。bookmark_count为4位,代表bookmarks数组的大小。bookmarks用来给quicklist重新分配内存空间时使用,不使用的时候不占空间
从这里看出,quicklist只保留首位两quicklistnode的地址,之后就是count等辅助字段了
quicklistnode数据结构

typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;
    unsigned int sz;             /* ziplist size in bytes */
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    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;

prev和next是双向链表的标准配置,指向上一个或下一个quicklistnode节点。*zl就是这个node所使用的ziplist了。sz为这个ziplist的字节数。count为这个ziplist的item数。encoding代表这个zpilist的类型,raw1,就算标准ziplist,lzf2,LZF代表着对这个ziplist进行了lzf压缩。attempted_compress则是记录这个list是否被压缩过。
上述中出现了LZF压缩,在这里只讲下概念。LZF压缩主要做两件事,一是对重复值进行压缩二是通过hash来判断是否为重复数据。
数据结构讲完,来看具体操作函数
create:

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;
    quicklist->bookmark_count = 0;
    return quicklist;
}

非常简单,分配空间,字段初始化
insert:

/* 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);
    }
}

在这里根据where字段决定是使用头插法还是尾插法。头插和尾插区别不大,这里只看头插法。
头插法:

/* 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这个负责判断当前quicklistnode的ziplist是否太大了
            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
        quicklist->head->zl =
            ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
        quicklistNodeUpdateSz(quicklist->head);
    } else {
         //如果_quicklistNodeAllowInsert说当前的quictlistnode中ziplist过大无法再插入,就new一个新的quicklistnode,放到那里
        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);
}

delete:

/* Delete one element represented by 'entry'
 *
 * 'entry' stores enough metadata to delete the proper position in
 * the correct ziplist in the correct quicklist node. */
void quicklistDelEntry(quicklistIter *iter, quicklistEntry *entry) {
    quicklistNode *prev = entry->node->prev;
    quicklistNode *next = entry->node->next;
    int deleted_node = quicklistDelIndex((quicklist *)entry->quicklist,
                                         entry->node, &entry->zi);

    /* after delete, the zi is now invalid for any future usage. */
    iter->zi = NULL;

    /* If current node is deleted, we must update iterator node and offset. */
    if (deleted_node) {
        if (iter->direction == AL_START_HEAD) {
            iter->current = next;
            iter->offset = 0;
        } else if (iter->direction == AL_START_TAIL) {
            iter->current = prev;
            iter->offset = -1;
        }
    }
    /* else if (!deleted_node), no changes needed.
     * we already reset iter->zi above, and the existing iter->offset
     * doesn't move again because:
     *   - [1, 2, 3] => delete offset 1 => [1, 3]: next element still offset 1
     *   - [1, 2, 3] => delete offset 0 => [2, 3]: next element still offset 0
     *  if we deleted the last element at offset N and now
     *  length of this ziplist is N-1, the next call into
     *  quicklistNext() will jump to the next node. */
}
/* Delete one entry from list given the node for the entry and a pointer
 * to the entry in the node.
 *
 * Note: quicklistDelIndex() *requires* uncompressed nodes because you
 *       already had to get *p from an uncompressed node somewhere.
 *
 * Returns 1 if the entire node was deleted, 0 if node still exists.
 * Also updates in/out param 'p' with the next offset in the ziplist. */
REDIS_STATIC int quicklistDelIndex(quicklist *quicklist, quicklistNode *node,
                                   unsigned char **p) {
    int gone = 0;

    node->zl = ziplistDelete(node->zl, p);
    node->count--;
    if (node->count == 0) {
        gone = 1;
        __quicklistDelNode(quicklist, node);
    } else {
        quicklistNodeUpdateSz(node);
    }
    quicklist->count--;
    /* If we deleted the node, the original node is no longer valid */
    return gone ? 1 : 0;
}
REDIS_STATIC void __quicklistDelNode(quicklist *quicklist,
                                     quicklistNode *node) {
    /* Update the bookmark if any */
    quicklistBookmark *bm = _quicklistBookmarkFindByNode(quicklist, node);
    if (bm) {
        bm->node = node->next;
        /* if the bookmark was to the last node, delete it. */
        if (!bm->node)
            _quicklistBookmarkDelete(quicklist, bm);
    }

    if (node->next)
        node->next->prev = node->prev;
    if (node->prev)
        node->prev->next = node->next;

    if (node == quicklist->tail) {
        quicklist->tail = node->prev;
    }

    if (node == quicklist->head) {
        quicklist->head = node->next;
    }

    /* Update len first, so in __quicklistCompress we know exactly len */
    quicklist->len--;
    quicklist->count -= node->count;

    /* If we deleted a node within our compress depth, we
     * now have compressed nodes needing to be decompressed. */
    __quicklistCompress(quicklist, NULL);

    zfree(node->zl);
    zfree(node);
}

删除代码虽然长,但是逻辑相当简单,仅仅是将这个节点从节点剔除,然后分别zfree node->zl和node。
总体来说,从quicklist的名字来说,quick应该是对ziplist的操作复杂度说的,quicklist本身没什么难度,重点放在ziplist,看懂之后ziplist,quicklist只需明白数据结构就足够了。