在一次使用redis list的时候,使用object encoding
命令查看编码类型。时出现了quicklist
192.168.99.100:6379> lpush list a b c d
(integer) 4
192.168.99.100:6379> object encoding list
"quicklist"
192.168.99.100:6379>
而我只记得linkedlist
和ziplist
。 这次抽了个时间来翻翻源码,为了弄清楚:
list对象的
linkedlist
编码、ziplist
编码、以及编码转换机制是否还存在?
List
redis/src/t_list.c(3.0)
# 使用LPUSH命令,lpushCommand会被调用
void lpushCommand(redisClient *c) {
pushGenericCommand(c,REDIS_HEAD);
}
void pushGenericCommand(redisClient *c, int where) {
int j, waiting = 0, pushed = 0;
# 如果命令是 LPUSH key value1 value2
# 则这里的argv = ["LPUSH", "key", "value1", "value2"]
robj *lobj = lookupKeyWrite(c->db,c->argv[1]);
# 如果key对应的类型不是list会报错并退出
if (lobj && lobj->type != REDIS_LIST) {
addReply(c,shared.wrongtypeerr);
return;
}
# 每一次循环就是在处理LPUSH操作的一个值
for (j = 2; j < c->argc; j++) {
# 尝试将argv[j]转换成合适的编码:RAW、EMBSTR、INT
c->argv[j] = tryObjectEncoding(c->argv[j]);
# list的自动创建,创建的list默认使用Ziplist编码
if (!lobj) {
lobj = createZiplistObject();
dbAdd(c->db,c->argv[1],lobj);
}
# Push的操作在listTypePush
listTypePush(lobj,c->argv[j],where);
pushed++;
}
# 命令回复
addReplyLongLong(c, waiting + (lobj ? listTypeLength(lobj) : 0));
if (pushed) {
char *event = (where == REDIS_HEAD) ? "lpush" : "rpush";
# 发送信号:key已修改
signalModifiedKey(c->db,c->argv[1]);
# 发送键空间、键时间通知
notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event,c->argv[1],c->db->id);
}
server.dirty += pushed;
}
void listTypePush(robj *subject, robj *value, int where) {
# 判断value是否满足编码转换条件
listTypeTryConversion(subject,value);
# 判断list是否满足编码转换条件
# 如果list长度大于等于server.list_max_ziplist_entries
# 那么list会从Ziplist转换成Linkedlist编码
if (subject->encoding == REDIS_ENCODING_ZIPLIST &&
ziplistLen(subject->ptr) >= server.list_max_ziplist_entries)
listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
# 针对不同编码,调用不同API来增加list节点
if (subject->encoding == REDIS_ENCODING_ZIPLIST) {
int pos = (where == REDIS_HEAD) ? ZIPLIST_HEAD : ZIPLIST_TAIL;
value = getDecodedObject(value);
subject->ptr = ziplistPush(subject->ptr,value->ptr,sdslen(value->ptr),pos);
decrRefCount(value);
} else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) {
if (where == REDIS_HEAD) {
listAddNodeHead(subject->ptr,value);
} else {
listAddNodeTail(subject->ptr,value);
}
incrRefCount(value);
} else {
redisPanic("Unknown list encoding");
}
}
# 如果添加的value是sds(Simple Dynamic String), sds可以是raw或embstr编码
# 且sds的长度大于server.list_max_ziplist_value
# 那么list会从Ziplist转换成Linkedlist编码
void listTypeTryConversion(robj *subject, robj *value) {
if (subject->encoding != REDIS_ENCODING_ZIPLIST) return;
if (sdsEncodedObject(value) &&
sdslen(value->ptr) > server.list_max_ziplist_value)
listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
}
这是我所熟知的list编码转换机制,而quicklist是redis 3.2新加入的结构。那我直接去看了redis 4.0的版本,了解下list编码转换机制是否有所改变。
redis/src/t_list.c(4.0)
void pushGenericCommand(client *c, int where) {
...
for (j = 2; j < c->argc; j++) {
if (!lobj) {
# list自动创建时,使用的是quicklist
lobj = createQuicklistObject();
quicklistSetOptions(lobj->ptr, server.list_max_ziplist_size,
server.list_compress_depth);
dbAdd(c->db,c->argv[1],lobj);
}
listTypePush(lobj,c->argv[j],where);
pushed++;
}
...
}
void listTypePush(robj *subject, robj *value, int where) {
# 添加元素时,如果不是quicklist编码就会报错
# 那很大程度上意味着list只有一种编码了
if (subject->encoding == OBJ_ENCODING_QUICKLIST) {
int pos = (where == LIST_HEAD) ? QUICKLIST_HEAD : QUICKLIST_TAIL;
value = getDecodedObject(value); # 确保value是一个字符串对象,而不是INT
size_t len = sdslen(value->ptr);
# 直接调用的quicklist API
quicklistPush(subject->ptr, value->ptr, len, pos);
decrRefCount(value); # 引用计数器
} else {
serverPanic("Unknown list encoding");
}
}
3.2版本的listTypePush会根据不同的编码调用不同数据结构的API,而4.0直接调用了quicklist的API,以前的编码转换的机制已经不存在了。
Quicklist
这次的目的已达到,但还是来了解下quicklist
quicklist - A doubly linked list of ziplists
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 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 quicklistEntry {
const quicklist *quicklist;
quicklistNode *node;
unsigned char *zi;
unsigned char *value;
long long longval;
unsigned int sz;
int offset;
} quicklistEntry;
- quicklist 是一个双向链表,head、tail分别指向头尾节点
- quicklistNode 是双向链表的节点,prev、next分别指向前驱、后继结点
- quicklistNode.zl 指向一个ziplist(或者quicklistLZF结构)
- quicklistEntry 包裹着list的每一个值,作为ziplist的一个节点
可以想象得到,当一个空的quicklist加入一个值value时,会有以下操作(不一定以这个顺序):
- 使用Entry包裹value
- 创建一个ziplist,把Entry加入到ziplist中
- 创建一个Node,Node.zl指向ziplist
- 创建quicklist,将Node加入quicklist中
原来版本的list直接使用的一个ziplist,而现在版本的list可以看成使用多个ziplist,然后通过双向链表连接起来。如果知道ziplist的特性,那这样的优点就很明显了。
由于ziplist的连锁更新。ziplist的插入删除操作在最坏情况下的复杂度为O(n^2),虽然导致整条ziplist连锁更新的概率很低。将原来的ziplist划分成多条ziplist后,插入删除一个元素,即使情况再极端也只会引起一段ziplist的连锁更新。