Redis源码阅读之lpush&rpush

431 阅读7分钟
Redis版本:7.0.2


Redis的lpush和rpush命令用于在list的头部和尾部添加元素。源码如下:
/* LPUSH <key> <element> [<element> ...] */
void lpushCommand(client *c) {
    pushGenericCommand(c,LIST_HEAD,0);
}

/* RPUSH <key> <element> [<element> ...] */
void rpushCommand(client *c) {
    pushGenericCommand(c,LIST_TAIL,0);
}

可以看到,这两个命令都调用了pushGenericCommand函数,只是第二个参数不同,lpush对应的参数值是LIST_HEAD (0),头部插入; rpush对应的参数值是LIST_TAIL(1),尾部插入。pushGenericCommand函数的定义如下:

/**********************************t_list.c******************************/
/* Implements LPUSH/RPUSH/LPUSHX/RPUSHX. 
 * 'xx': push if key exists. */
void pushGenericCommand(client *c, int where, int xx) {
    int j;

    robj *lobj = lookupKeyWrite(c->db, c->argv[1]);
    if (checkType(c,lobj,OBJ_LIST)) return;
    if (!lobj) {
        if (xx) {
            addReply(c, shared.czero);
            return;
        }

        lobj = createQuicklistObject();
        quicklistSetOptions(lobj->ptr, server.list_max_listpack_size,
                            server.list_compress_depth);
        dbAdd(c->db,c->argv[1],lobj);
    }

    for (j = 2; j < c->argc; j++) {
        listTypePush(lobj,c->argv[j],where);
        server.dirty++;
    }

    addReplyLongLong(c, listTypeLength(lobj));

    char *event = (where == LIST_HEAD) ? "lpush" : "rpush";
    signalModifiedKey(c,c->db,c->argv[1]);
    notifyKeyspaceEvent(NOTIFY_LIST,event,c->argv[1],c->db->id);
}

其重要逻辑为:

  1. 通过lookupKeyWrite函数查找key对应的值对象。
  2. 校验类型。
  3. 如果key不存在,且xx为真(即只有在key存在时才push),则返回;否则,调用createQuicklistObject函数创建一个quicklist对象,并将其添加到对应db中。
  4. 遍历,将将待插入的元素push。
  5. 返回list的长度。
  6. 发送信号,通知键空间事件。

接下来,我们着重看一下quicklist的定义、怎样创建quicklist对象,以及怎样插入。

quicklist的定义

首先来看一下quicklist以及quicklistNode的定义:

/*********************quicklist.h************************/
/* quicklist is a 40 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: 0 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.
 * 'bookmarks are an optional feature that is used by realloc this struct,
 *      so that they don't consume memory when not used. 
 * quicklist定义了一个quicklist的数据结构,它是一个40字节的结构体(在64位系统上)。
 * 'count'是所有entry(条目/元素)的总数。
 * 'len'是quicklist节点的数量。
 * 'compress'是:如果压缩被禁用,则为0,否则它是要保留在quicklist末端的未压缩的quicklistNodes的数量。
 * 'fill'是用户请求的(或默认的)填充因子。
 * 'bookmarks'是一个可选的特性,它被realloc使用,所以当不使用时,它不会占用内存。
 * */
typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* total count of all entries in all listpacks 所有条目数量*/
    unsigned long len;          /* number of quicklistNodes 节点数量*/
    signed 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;

/* quicklistNode is a 32 byte struct describing a listpack for a quicklist.
 * We use bit fields keep the quicklistNode at 32 bytes.
 * count: 16 bits, max 65536 (max lp bytes is 65k, so max count actually < 32k).
 * encoding: 2 bits, RAW=1, LZF=2.
 * container: 2 bits, PLAIN=1 (a single item as char array), PACKED=2 (listpack with multiple items).
 * recompress: 1 bit, bool, true if node is temporary decompressed for usage.
 * attempted_compress: 1 bit, boolean, used for verifying during testing.
 * extra: 10 bits, free for future use; pads out the remainder of 32 bits 
 * quicklistNode是一个32字节的结构体,定义了quicklist的listpack。
 * 我们使用位字段将quicklistNode保持在32字节。
 * count: 16位,最大65536(最大lp字节为65k,所以最大count实际上小于32k)。
 * encoding: 2位,RAW=1,LZF=2。
 * container: 2位,PLAIN=1(一个条目作为char数组),PACKED=2(包含多个条目的listpack)。
 * recompress: 1位,bool,如果节点是临时解压缩的,则为true。
 * attempted_compress: 1位,布尔值,用于测试期间的验证。
 * extra: 10位,用于未来使用;填充剩余的32位。
 * */
typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *entry;
    size_t sz;             /* entry size in bytes  :entry大小,单位为字节*/
    unsigned int count : 16;     /* count of items in listpack :listpack中的条目数量*/
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* PLAIN==1 or PACKED==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;

createQuicklistObject-创建quicklist对象

创建quicklist时会调用createQuicklistObject函数,源码如下:

/*********************object.c************************/

robj *createQuicklistObject(void) {
    quicklist *l = quicklistCreate();
    robj *o = createObject(OBJ_LIST,l);
    o->encoding = OBJ_ENCODING_QUICKLIST;
    return o;
}

/* Create a new quicklist.
 * Free with quicklistRelease(). 
 * 创建一个新的quicklist。
 */
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;
}

可以看到,逻辑其实很简单,就是分配内存,初始化quicklist的各个字段。

listTypePush-将元素添加到list中

listTypePush函数用于将元素添加到list中,源码如下:

/**************************t_list.c****************************/

/* The function pushes an element to the specified list object 'subject',
 * at head or tail position as specified by 'where'.
 * 向指定的list对象'subject'中添加一个元素,是从头部插入还是从尾部插入,由'where'指定。
 *
 * There is no need for the caller to increment the refcount of 'value' as
 * the function takes care of it if needed. 
 * 调用者无需增加'value'的引用计数,因为如果需要,函数会处理。
 * */
void listTypePush(robj *subject, robj *value, int where) {
    if (subject->encoding == OBJ_ENCODING_QUICKLIST) {
        int pos = (where == LIST_HEAD) ? QUICKLIST_HEAD : QUICKLIST_TAIL;
        if (value->encoding == OBJ_ENCODING_INT) {
            char buf[32];
            ll2string(buf, 32, (long)value->ptr);
            quicklistPush(subject->ptr, buf, strlen(buf), pos);
        } else {
            quicklistPush(subject->ptr, value->ptr, sdslen(value->ptr), pos);
        }
    } else {
        serverPanic("Unknown list encoding");
    }
}

/***********************quicklist.c*********************/

/* Wrapper to allow argument-based switching between HEAD/TAIL pop */
void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
                   int where) {
    /* The head and tail should never be compressed (we don't attempt to decompress them) */
    if (quicklist->head)
        assert(quicklist->head->encoding != QUICKLIST_NODE_ENCODING_LZF);
    if (quicklist->tail)
        assert(quicklist->tail->encoding != QUICKLIST_NODE_ENCODING_LZF);

    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.
 * 向quicklist的头部添加一个元素。
 *
 * Returns 0 if used existing head.
 * Returns 1 if new head created. 
 * 如果使用已存在的节点,则返回0.
 * 如果是创建了新节点,则返回1.
 * */
int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *orig_head = quicklist->head;

    if (unlikely(isLargeElement(sz))) {
        __quicklistInsertPlainNode(quicklist, quicklist->head, value, sz, 0);
        return 1;
    }

    if (likely(
            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
        quicklist->head->entry = lpPrepend(quicklist->head->entry, value, sz);
        quicklistNodeUpdateSz(quicklist->head);
    } else {
        quicklistNode *node = quicklistCreateNode();
        node->entry = lpPrepend(lpNew(0), value, sz);

        quicklistNodeUpdateSz(node);
        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
    }
    quicklist->count++;
    quicklist->head->count++;
    return (orig_head != quicklist->head);
}

/* Add new entry to tail node of quicklist.
 * 向quicklist的尾部添加条目。
 * 
 * Returns 0 if used existing tail.
 * Returns 1 if new tail created. 
 * 如果使用已存在的尾部,返回0.
 * 如果是创建了新的尾部,返回1.
 * */
int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *orig_tail = quicklist->tail;
    if (unlikely(isLargeElement(sz))) {
        __quicklistInsertPlainNode(quicklist, quicklist->tail, value, sz, 1);
        return 1;
    }

    if (likely(
            _quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
        quicklist->tail->entry = lpAppend(quicklist->tail->entry, value, sz);
        quicklistNodeUpdateSz(quicklist->tail);
    } else {
        quicklistNode *node = quicklistCreateNode();
        node->entry = lpAppend(lpNew(0), value, sz);

        quicklistNodeUpdateSz(node);
        _quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
    }
    quicklist->count++;
    quicklist->tail->count++;
    return (orig_tail != quicklist->tail);
}

listTypePush函数的逻辑也很简单:

  1. 判断encoding是否为OBJ_ENCODING_QUICKLIST。不是,则panic。
  2. 判断待添加元素编码是否为OBJ_ENCODING_INT,如果是,则将其转换为字符串,然后调用quicklistPush函数。
  3. 否则,直接调用quicklistPush函数。

而quicklistPush又根据添加位置(头部或尾部)的不同,分别调用了quicklistPushHead和quicklistPushTail函数。

实际上,从头部插入和从尾部插入,代码相似度很高,我们仅从quicklistPushTail开始看一下,怎样从尾部插入。

从quicklistPushTail的源码可以看到,其逻辑其实并不简单:

  1. 判断元素数量是否过大,如果是否,则调用__quicklistInsertPlainNode进行插入普通节点。否则,继续。
  2. 判断是否可以在尾节点插入。如果可以,则直接在尾节点插入。
  3. 否则,调用quicklistCreateNode创建一个新的节点。并将元素写入的新节点中。这里请注意,这种情况下,新节点的entry是一个listpack,一个entry可以存贮多个元素
  4. 调用_quicklistInsertNodeAfter将新节点插入到尾节点之后。

小结

  1. quicklist是一个双向链表。
  2. quicklist的每个node可以是普通节点,也可以是listpack。
  3. 如果是普通节点,则一个节点只能存储一个元素。
  4. 如果是listpack,则一个节点可以存储多个元素。
  5. 添加新元素时,如果元素大小过大,则会使用listpack,否则会使用普通节点。

关于listpack这种数据类型,我们会在后续的文章中详细研读。