Redis高级之List底层源码(3)-List源码分析

1 概述

        在了解Redis中的list数据结构之前,先来回顾和学习一下单向链表和双向链表,以便更好的学习和理解Redis中List的底层数据结构Redis链表。

1.1 单向链表

        数组是需要开辟一块连续的内存来存储元素的,这个特点既有优点也有缺点:优点是可以根据下标来访问元素,时间复杂度为O(1);缺点是为了保证在内存中的连续性,插入和删除操作变得非常低效,并且只要数组一声明就会占用整块连续的内存空间。数组声明过大会导致浪费内存,过小可能不够用,当数组的空间不足时需要对其扩容,申请一个更大空间的数组,把原数组的元素复制过去。

        链表不需要连续的内存空间,可以通过指针将一组零散的内存连接起来。其优点在于本身没有大小限制,天然支持扩容,插入和删除操作高效,时间复杂度为O(1),缺点是需要额外的空间来存储指针。单向链表结构如下:

        单向链表进行节点的插入和删除操作,如图所示:

        单向链表中每个节点除了包含数据之外,还需要一个指针,即后继指针(存储链表下一个节点地址),所以需要额外的空间来存储后继结点的地址。单向链表中有两个特殊的节点:头结点和尾节点。其中,头结点用来记录链表的起始地址,尾结点的后继指针不是指向像一个节点,而是指向一个空地址(表示这个链表的最后一个节点)。与数组一样,单向链表也支持数据的查找、插入和删除,其中插入和删除操作只需要开率相邻节点指针的变化,因此时间复杂度为O(1)。不过,想要随机访问单向链表的地n个元素,就没有数组那么高效了,因为链表中的数据不是连续存储的,所以不能像数组那样,根据首地址和下标通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针逐个节点地依次遍历,知道找到相应的节点,时间复杂度为O(N)。

1.2 双向链表

        双向链表的结构如下图:

 

        双向链表和单向链表不同的是多了一个前驱指针。双向链表需要额外的两个空间来存储后继结点和前驱节点的地址,占用的空间比单向链表多,优点在于支持双向遍历,体现在以下两个方面:

        1、有序链表中查找某个元素,单向链表只有后继指针,因此只能从前往后遍历查找,时间复杂度为O(N);双向链表可以双向遍历,进行二分法查找,时间复杂度为O(log2n)。

        2、删除给定指针指向的节点更快捷。要删除节点就必须知道其前驱节点和后继节点,单向链表想要知道其前驱节点就只能从头遍历,而双向链表保存了其前驱节点的地址,所以查找速度更快。

1.3 Redis链表

        首先了解一下Redis链表的结果示意图:

 

        定义ListNode节点结构类型的源码如下:

typedef struct listNode {
    //前驱节点
    struct listNode *prev;
    //后继节点
    struct listNode *next;
    void *value;
} listNode;

typedef struct list {
    //表头节点
    listNode *head;
    //表尾节点
    listNode *tail;
    //节点值复制函数
    void *(*dup)(void *ptr);
    //节点值释放函数
    void (*free)(void *ptr);
    //节点值对比函数
    int (*match)(void *ptr, void *key);
    //链表所包含的节点数量
    unsigned long len;
} list;

        Redis链表结构的主要特性如下:

        1、双向五环:链表节点带有前驱、后继指针获取某个节点的前驱和后继节点的时间复杂度为O(1),表头节点的前驱指针和表尾节点的后继指针都指向NULL,对链表的访问以null为终点。

        2、长度计数器:通过List结构的len属性获取节点数量的时间复杂度为O(1)。

1.4 快速列表

        实际上QuickList(快速列表)是ZipList和LinkedList的结合体,它将LinkedList按段切分,每一段使用ZipList紧凑存储来节省空间,多个ZipList之间使用双向指针串接起来。QuickList在Redis 3.2版本引入。在引入QuickList之前,Redis采用压缩链表以及双向链表作为List的底层视线。当元素个数比较少并且元素长度比较小时,Redis采用ZipList作为底层存储结构。当任意一个条件不满足时,Redis采用链表作为底层存储结构。这么做的主要原因是:当元素长度较小时,使用ZipList虽然可以节省空间,但储存空间是连续的;当元素个数比较多时,修改元素时必须重新分配存储空间,会影响Redis的执行效率,所以采用双向链表。

        链表的附加空间相对较高,prev和next指针就要占16字节,而且每个字节的内存都是单独分配的,会加剧内存的碎片化,影响内存管理的效率。对于内存数据库来说,实在是难以接受,所以Redis 3.2开始对列表数据结构进行了改进,使用QuickList数据结构。

QuickList数据结构示意图如下:

       QuickListNode节点的源码如下:

typedef struct quicklistNode {
    //上一个node节点
    struct quicklistNode *prev;
    //下一个node节点
    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:指向链表后一个节点的指针

        zl:数据指针。如果当前节点的数据没有压缩,那么它指向一个ZipList结构,否则,它指向一个quickList结构。

        sz:表示zl指向的Ziplist的总大小。

        count:表示ziplist里面包含的数据项个数。

        encoding:一个预留字段,用来表明是在一个QuickList节点下直接存储数据还是使用Ziplist村塾数据,或者用其他数据结构存储数据。

        container:一个预留字典,用来表明是在一个QuickList节点下直接存储数据还是使用ZipList存储数据,或者用其他数据结构来存储。

        recompress:当我们使用index这样的命令来查看了某一项压缩的数据时,需要把该项数据暂时解压,设置recompress=1做一个标记,等有机会再把该项数据重新压缩。

        当利用LZF算法对ZipList进行压缩时,quicklistNode节点指向的结构为quicklistLZF。

typedef struct quicklistLZF {
    //表示压缩后所占用的字节数
    unsigned int sz;
    //一个柔性数组,存放压缩后的ZipList字节数组
    char compressed[];
} quicklistLZF;

     

typedef struct quicklist {
    //指向头结点的指针
    quicklistNode *head;
    //指向尾节点的指针
    quicklistNode *tail;
    //所有ZipList数据项的总项数
    unsigned long count;  
    //quickList节点的个数
    unsigned long len;    
    //16bit,用于设置ZipList的大小,存放list-max-ziplist-depth参数的值
    int fill : QL_FILL_BITS;     
    //16bit,用于设置节点压缩的深度,存放list-compress-depth参数的值
    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;

  1.4.1 插入元素

        quickList 可以选择在头部或尾部进行插入,包含以下两种情况:

        1、如果ZipList大小没有超过限制,那么新数据会被直接插入ZipList中。

        2、如果ZipList超过限制,就需要新创建一个quicklistNode节点,对应的也会新创建一个ZipList,然后把这个新创建的节点插入quickList双向链表中。

int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *orig_head = quicklist->head;
    assert(sz < UINT32_MAX); /* TODO: add support for quicklist nodes that are sds encoded (not zipped) */
    if (likely(
            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
        //头部节点可以插入
        quicklist->head->zl =
            ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
        quicklistNodeUpdateSz(quicklist->head);
    } else {
        //头部节点不能继续插入,需要新创建quickListNode
        quicklistNode *node = quicklistCreateNode();
        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);

        quicklistNodeUpdateSz(node);
        //新建的quicklistNode插入quicklist结构体中
        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
    }
    quicklist->count++;
    quicklist->head->count++;
    return (orig_head != quicklist->head);
}

        除了push操作外,QuickList还提供了一种在任意位置插入的方法,流程如下:

        1、当ZipList大小没有超过限制,直接插入ZipList中。

        2、当ZipList大小超过了限制,但插入的位置位于ZipList两端,并且相邻的QuickList链表节点的ZipList大小没有超过限制,就转而插入相邻的QuickList链表节点的ZipList中。

        3、当ZipList大小超过了限制,但插入的位置位于ZipList两端,并且相邻的QuickList链表节点的ZipList大小也超过限制,就需要新创建一个QuickList链表节点插入。

        4、对于ZipList大小超过了限制的其他情况,如果数据插入在ZipList中间,就需要把当前ZipList分裂为两个节点,然后在其中一个节点上插入数据。

        1.4.2 删除元素

        QuickList提供了删除单个元素和删除一个区间内的所有元素两种删除方法。删除单个元素,我们可以使用quicklistDelEntry来实现,也可以通过quicklistPop将头部或尾部元素弹出。quicklistDelEntry函数调用底层quicklistDelIndex函数,该函数可以删除quicklistNode指向的ZipList中的某个元素,其中p指向ZipList中某个entry的起始位置。


//根据指定区间来删除元素
int quicklistDelRange(quicklist *quicklist, const long start,
                      const long count) {
    if (count <= 0)
        return 0;

    unsigned long extent = count; /* range is inclusive of start position */

    if (start >= 0 && extent > (quicklist->count - start)) {
        /* if requesting delete more elements than exist, limit to list size. */
        extent = quicklist->count - start;
    } else if (start < 0 && extent > (unsigned long)(-start)) {
        /* else, if at negative offset, limit max size to rest of list. */
        extent = -start; /* c.f. LREM -29 29; just delete until end. */
    }

    quicklistEntry entry;
    if (!quicklistIndex(quicklist, start, &entry))
        return 0;

    D("Quicklist delete request for start %ld, count %ld, extent: %ld", start,
      count, extent);
    quicklistNode *node = entry.node;

    /* iterate over next nodes until everything is deleted. */
    while (extent) {
        //保存下一个quicklistNode,因为本节点会被删掉
        quicklistNode *next = node->next;

        unsigned long del;
        int delete_entire_node = 0;
        if (entry.offset == 0 && extent >= node->count) {
            /* 需要删除整个quicklistNode */
            delete_entire_node = 1;
            del = node->count;
        } else if (entry.offset >= 0 && extent >= node->count) {
            /* 删除本节点剩余的所有元素 */
            del = node->count - entry.offset;
        } else if (entry.offset < 0) {
            /* 代表从后向前,其他数字代表ZipList后面剩余的元素个数 */
            del = -entry.offset;

            /* If the positive offset is greater than the remaining extent,
             * we only delete the remaining extent, not the entire offset.
             */
            if (del > extent)
                del = extent;
        } else {
            /* 删除本节点的元素*/
            del = extent;
        }

        D("[%ld]: asking to del: %ld because offset: %d; (ENTIRE NODE: %d), "
          "node count: %u",
          extent, del, entry.offset, delete_entire_node, node->count);

        if (delete_entire_node) {
            __quicklistDelNode(quicklist, node);
        } else {
            quicklistDecompressNodeForUse(node);
            node->zl = ziplistDeleteRange(node->zl, entry.offset, del);
            quicklistNodeUpdateSz(node);
            node->count -= del;
            quicklist->count -= del;
            quicklistDeleteIfEmpty(quicklist, node);
            if (node)
                quicklistRecompressOnly(quicklist, node);
        }

        extent -= del;

        node = next;

        entry.offset = 0;
    }
    return 1;
}

        QuickList在删除指定区间内的元素时,会先找到start所在的quicklistNode,计算删除的元素个数是否小于要删除的count个数,如果不满足删除个数,就会移动到下一个quicklistNode继续删除,直到删除到指定的元素个数完成为止。

        1.4.3 修改元素

        QuickList修改元素时按下标进行的,原理是先删除原有元素,之后插入新的元素。QuickList不适合直接改变原有的元素,主要是由于其内部是ZipList结构,在内存中是连续存储的,改变其中一个元素可能会影响后续元素。所以QuickList采用先删除后插入的方法。

int quicklistReplaceAtIndex(quicklist *quicklist, long index, void *data,
                            int sz) {
    quicklistEntry entry;
    if (likely(quicklistIndex(quicklist, index, &entry))) {
        /* 先删除元素 */
        entry.node->zl = ziplistDelete(entry.node->zl, &entry.zi);
        //再新插入元素
        entry.node->zl = ziplistInsert(entry.node->zl, entry.zi, data, sz);
        quicklistNodeUpdateSz(entry.node);
        quicklistCompress(quicklist, entry.node);
        return 1;
    } else {
        return 0;
    }
}

        1.4.4  查询元素

        列表(List)的查询操作主要是通过下标进行的,根据下标直到链表中ZipList的节点,然后调用ziplistGet就能成功找到。

//idx为要查找元素的下标,把查找结果写入entry,0表示未找到,1表示找到
int quicklistIndex(const quicklist *quicklist, const long long idx,
                   quicklistEntry *entry) {
    quicklistNode *n;
    unsigned long long accum = 0;
    unsigned long long index;
    //当idx值为负时,代表从尾部到头部的偏移量,-1代表尾部元素
    int forward = idx < 0 ? 0 : 1; /* < 0 -> reverse, 0+ -> forward */
    //初始化entry  index quicklistNode
    initEntry(entry);
    entry->quicklist = quicklist;

    if (!forward) {
        index = (-idx) - 1;
        n = quicklist->tail;
    } else {
        index = idx;
        n = quicklist->head;
    }

    if (index >= quicklist->count)
        return 0;
    //遍历quicklistNode节点,找到index对应的quicklistnode
    while (likely(n)) {
        if ((accum + n->count) > index) {
            break;
        } else {
            D("Skipping over (%p) %u at accum %lld", (void *)n, n->count,
              accum);
            accum += n->count;
            n = forward ? n->next : n->prev;
        }
    }

    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);
    //计算下标所在的ziplist的偏移量
    entry->node = n;
    if (forward) {
        /* forward = normal head-to-tail offset. */
        entry->offset = index - accum;
    } 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;
    }
    //解压节点数据
    quicklistDecompressNodeForUse(entry->node);
    entry->zi = ziplistIndex(entry->node->zl, entry->offset);
    //从ziplist获取元素
    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;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

geminigoth

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值