《redis设计与实现》-8 列表list

一 序

     书上这一节内容不是很多,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

https://www.cnblogs.com/virgosnail/p/9542470.html

https://blog.csdn.net/men_wen/article/details/70229375

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值