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);
}
其重要逻辑为:
- 通过lookupKeyWrite函数查找key对应的值对象。
- 校验类型。
- 如果key不存在,且xx为真(即只有在key存在时才push),则返回;否则,调用createQuicklistObject函数创建一个quicklist对象,并将其添加到对应db中。
- 遍历,将将待插入的元素push。
- 返回list的长度。
- 发送信号,通知键空间事件。
接下来,我们着重看一下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函数的逻辑也很简单:
- 判断encoding是否为OBJ_ENCODING_QUICKLIST。不是,则panic。
- 判断待添加元素编码是否为OBJ_ENCODING_INT,如果是,则将其转换为字符串,然后调用quicklistPush函数。
- 否则,直接调用quicklistPush函数。
而quicklistPush又根据添加位置(头部或尾部)的不同,分别调用了quicklistPushHead和quicklistPushTail函数。
实际上,从头部插入和从尾部插入,代码相似度很高,我们仅从quicklistPushTail开始看一下,怎样从尾部插入。
从quicklistPushTail的源码可以看到,其逻辑其实并不简单:
- 判断元素数量是否过大,如果是否,则调用__quicklistInsertPlainNode进行插入普通节点。否则,继续。
- 判断是否可以在尾节点插入。如果可以,则直接在尾节点插入。
- 否则,调用quicklistCreateNode创建一个新的节点。并将元素写入的新节点中。这里请注意,这种情况下,新节点的entry是一个listpack,一个entry可以存贮多个元素。
- 调用_quicklistInsertNodeAfter将新节点插入到尾节点之后。
小结
- quicklist是一个双向链表。
- quicklist的每个node可以是普通节点,也可以是listpack。
- 如果是普通节点,则一个节点只能存储一个元素。
- 如果是listpack,则一个节点可以存储多个元素。
- 添加新元素时,如果元素大小过大,则会使用listpack,否则会使用普通节点。
关于listpack这种数据类型,我们会在后续的文章中详细研读。