07、Redis 源码解析 - Redis 基本类型一链表list、ziplist、quicklist

Redis中的链表包括list、ziplist、quicklist三种,list常用来内部操作,ziplist和quicklist用来存储KV,也就是lpush、rpush等命令形成的对象。

list设计与实现

list的设计分为三部分:链表节点、迭代器、链表。这三部分示意如下:

typedef struct listNode {
   
     
	// 双向链表
    struct listNode *prev;
    struct listNode *next;
    // 链表的值
    void *value;
} listNode;

typedef struct listIter {
   
     
    listNode *next;
    // 迭代方向
    int direction;
} listIter;

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;

链表的API都很简单,示意图如下:
调用listCreate,创建一个空链表:
 
调用listAddNodeHead,插入一个字符串"a":
 
调用listAddNodeHead,插入一个字符串"b":
 
调用listAddNodeTail,插入一个字符串"c":
 
调用listInsertNode,在"b"后面插入一个字符串"d":
 
调用listDelNode,删除节点"a":
 

迭代器的API:
1、 申请迭代器;
2、 调用listNext,如果向后遍历,取节点的next,直到NULL;如果向前遍历,取节点的prev,直到NULL;

在Redis5.0版本之前,list也会存储KV,但是在Redis5.0版本之后,被quicklist取代,主要原因还是内存,一个list节点包含前后连个指针,在64位机器上,就占用16byte的内存。

ziplist设计与实现

ziplist为内存紧凑链表,占用一整块连续的内存空间,ziplist的设计在ziplist.c的注释中说明的很清楚,这里翻译一下:
一个ziplist的内存布局:
 
一个ziplist的额外内存占用为4+4+2+1 = 11byte

其中entry的内部布局:
 
prevlen:前一个entry内存量,用来从ziplist后往前遍历;如果prevlen < 254,则占用1byte;否则占用5byte,其中最高byte固定为254,后面4byte表示真正的前一个entry内存量
encoding:编码方式,对于字符串和整数有不同的编码方式:

1、 |00pppppp|-长度不超过63的字符串(6bits),低6bit表示字符串长度;
2、 |01pppppp|qqqqqqqq|-长度不超过16383的字符串(14bits),低14bit表示字符串长度;
3、 |10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt|-长度大于16384的字符串,只有低4byte表示字符串长度;
4、 |11000000|-编码为int16_t的整数(2byte);
5、 |11010000|-编码为int32_t的整数(4byte);
6、 |11100000|-编码为int64_t的整数(8byte);
7、 |11110000|-编码为24bit的整数(3byte);
8、 |11111110|-编码为8bit的整数(1byte);
9、 |1111xxxx|-xxxx在0000和1101之间,编码为4bit的整数;
10、 |11111111|-zl结束标志符;

entry的额外内存在prevlen和encoding上,最大为5+5=10byte,通常情况下为1+1=2byte,小于list中单个节点额外的16byte,同时为了节省内存,对于长度不同的数据,都有不同的编码方式,尤其对于prevlen的解释。
这样做虽然使得内存更加紧凑,节省内存,但是也存在一些缺点:

1、 添加节点、更新节点、删除节点等操作增加了很多复杂性;
2、 由于是一块内存,不可避免的需要很多memcpy、realloc等内存拷贝和分配操作,比如:在entryB之前插入entryA,entryB会更新自己的prevlen,有可能使得prevlen从1byte变成5byte,这样整体会重新分配一次内存,同时调用memcpy;如果entryB更新prevlen,导致在entryB之后的entryC也更新了prevlen,如果再次从1byte变成5byte,就会产生级联更新删除节点也有类似的风险;
3、 ziplist在查找时,需要遍历然后比较,时间复杂度是O(n);
基于以上原因,ziplist存储的entry数量不可能太多。Redis中存储list时,起初采用ziplist存储,当entry数量超过阈值时,会转化为quicklist。

quicklist设计与实现

quicklist结合了list和ziplist的优点,在节省内存的同时,简化了ziplist的插入、删除等操作,一定程度上可以避免连锁更新,其总体设计类似于std::deque。与list一样,分为三部分:节点、迭代器、链表,三部分的代码如下:

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;

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 : 16;              /* fill factor for individual nodes */
    unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;

typedef struct quicklistIter {
   
     
    const quicklist *quicklist;
    quicklistNode *current;
    unsigned char *zi;
    long offset; /* offset in current ziplist */
    int direction;
} quicklistIter;

可以看出:

  • quicklistNode就是一个ziplist,同时加入prev、next前后指针将其串联到quicklist中
  • 每个quicklist中维护首尾节点
  • 本质上quicklist和list结构一样,只不过每个节点是一个ziplist
  • 另外quicklist支持节点压缩,当quicklist中的节点数量超过quicklist::compress规定的长度时,就会将quicklist中的中间节点压缩,以此来节省空间;但是需要读取节点内容时,会增加一次解压的代价。
  • quicklist::fill用来控制扇出,当插入的entry总大小超过这个阈值时,需要新建quicknode,这样一定程度上可以避免连锁更新
     
    quicklist的API较多,以插入节点为例:
int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
   
     
    quicklistNode *orig_head = quicklist->head;
    if (likely(
            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
   
     
        // 判断是否可以在当前节点中插入entry
        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);
        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
    }
    quicklist->count++;
    quicklist->head->count++;
    return (orig_head != quicklist->head);
}

_quicklistNodeAllowInsert会计算新插入元素后的大小,判断该大小是否满足quicklist::fill扇出要求

REDIS_STATIC int _quicklistNodeAllowInsert(const quicklistNode *node,
                                           const int fill, const size_t sz) {
   
     
    if (unlikely(!node))
        return 0;

    int ziplist_overhead;
    /* size of previous offset */
    // 计算ziplist中的prevlen
    if (sz < 254)
        ziplist_overhead = 1;
    else
        ziplist_overhead = 5;

    /* size of forward offset */
    // 计算ziplist中的encoding增加长度
    if (sz < 64)
        ziplist_overhead += 1;
    else if (likely(sz < 16384))
        ziplist_overhead += 2;
    else
        ziplist_overhead += 5;

    /* new_sz overestimates if 'sz' encodes to an integer type */
    unsigned int new_sz = node->sz + sz + ziplist_overhead;
    // 判断是否满足quicklist::fill的要求
    if (likely(_quicklistNodeSizeMeetsOptimizationRequirement(new_sz, fill)))
        return 1;
    else if (!sizeMeetsSafetyLimit(new_sz))
        return 0;
    else if ((int)node->count < fill)
        return 1;
    else
        return 0;
}