列表类型是用来存贮多个字符串对象的结构。一个列表可以存贮232-1个元素,可以对列表两端进行插入(push)、弹出(pop),还可以获取指定范围内的元素列表、获取指定索引的元素等等,它可以灵活的充当栈和队列的角色。下面列出列表的命令:
列表命令
操作说明 | 命令 | 时间复杂度 |
---|---|---|
向尾部添加 | rpush key value [value…] | O(n) |
向头部添加 | lpush key value [value…] | O(n) |
插入在指定键前/后 | linsert key before/after pivot value | O(n) |
返回范围内的键 | lrange key start end | O(n) |
返回索引的键 | lindex key index | O(n) |
返回列表的长度 | llen key index | O(1) |
弹出头部节点 | lpop key | O(1) |
弹出尾部节点 | rpop key | O(1) |
删除指定cout数目且值等于value的键 | lremkey count value | O(n) |
删除指定范围的键 | ltrim key start end | O(n) |
修改指定索引的键值 | lset key index value | O(n) |
阻塞操作 | blpop、brpop | O(1) |
编码的转换
上次在介绍redis对象时提到过,列表对象的编码格式会在一定情况下从ziplist转变成adlist链表。列表每次push元素时,都会检测是否满足这一情况:
/*
* 对输入值 value 进行检查,看是否需要将 subject 从 ziplist 转换为双端链表,
* 以便保存值 value 。
* 函数只对 REDIS_ENCODING_RAW 编码的 value 进行检查,
* 因为整数编码的值不可能超长。
*/
//server.list_max_ziplist_value是服务器的属性,可更改,默认64字节
void listTypeTryConversion(robj *subject, robj *value) {
// 确保 subject 为 ZIPLIST 编码
if (subject->encoding != REDIS_ENCODING_ZIPLIST) return;
if (sdsEncodedObject(value) &&
// 看字符串是否过长
sdslen(value->ptr) > server.list_max_ziplist_value)
// 将编码转换为双端链表
listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
}
/*
* 将列表的底层编码从 ziplist 转换成双端链表
*/
void listTypeConvert(robj *subject, int enc) {
//定义一个链表的迭代器
listTypeIterator *li;
listTypeEntry entry;
redisAssertWithInfo(NULL,subject,subject->type == REDIS_LIST);
// 转换成双端链表
if (enc == REDIS_ENCODING_LINKEDLIST) {
list *l = listCreate();
//设置链表的释放函数
listSetFreeMethod(l,decrRefCountVoid);
/* listTypeGet returns a robj with incremented refcount */
// 遍历 ziplist ,并将里面的值全部添加到双端链表中
li = listTypeInitIterator(subject,0,REDIS_TAIL);
while (listTypeNext(li,&entry)) listAddNodeTail(l,listTypeGet(&entry));
listTypeReleaseIterator(li);
// 更新编码
subject->encoding = REDIS_ENCODING_LINKEDLIST;
// 释放原来的 ziplist
zfree(subject->ptr);
// 更新对象值指针
subject->ptr = l;
} else {
redisPanic("Unsupported list conversion");
}
}
也就是说,当列表里插入的元素字节长大于64字节时,列表会启动编码转换程序。
列表的新增
不仅是插入的元素字节长大于默认的64字节会激动编码转换,新增元素的底层实现,在插入之前会检测列表的总长度是否大于默认ziplist最大长度也会引起编码转换,默认长度是512个元素,可以在服务器的配置中更改,列表插入底层实现如下:
//server.list_max_ziplist_entries表示最大长度,where表示插入到表头还是表尾
void listTypePush(robj *subject, robj *value, int where) {
/* Check if we need to convert the ziplist */
// 是否需要转换编码?
listTypeTryConversion(subject,value);
if (subject->encoding == REDIS_ENCODING_ZIPLIST &&
ziplistLen(subject->ptr) >= server.list_max_ziplist_entries)
listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
// ZIPLIST
if (subject->encoding == REDIS_ENCODING_ZIPLIST) {
int pos = (where == REDIS_HEAD) ? ZIPLIST_HEAD : ZIPLIST_TAIL;
// 取出对象的值,因为 ZIPLIST 只能保存字符串或整数
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 插入到列表节点的之前或之后。
* where 参数决定了插入的位置:
* - REDIS_HEAD 插入到节点之前
* - REDIS_TAIL 插入到节点之后
*/
void listTypeInsert(listTypeEntry *entry, robj *value, int where) {
robj *subject = entry->li->subject;
// 插入到 ZIPLIST
if (entry->li->encoding == REDIS_ENCODING_ZIPLIST) {
// 返回对象未编码的值
value = getDecodedObject(value);
if (where == REDIS_TAIL) {
unsigned char *next = ziplistNext(subject->ptr,entry->zi);
/* When we insert after the current element, but the current element
* is the tail of the list, we need to do a push. */
if (next == NULL) {
// next 是表尾节点,push 新节点到表尾
subject->ptr = ziplistPush(subject->ptr,value->ptr,sdslen(value->ptr),REDIS_TAIL);
} else {
// 插入到到节点之后
subject->ptr = ziplistInsert(subject->ptr,next,value->ptr,sdslen(value->ptr));
}
} else {
subject->ptr = ziplistInsert(subject->ptr,entry->zi,value->ptr,sdslen(value->ptr));
}
decrRefCount(value);
// 插入到双端链表
} else if (entry->li->encoding == REDIS_ENCODING_LINKEDLIST) {
if (where == REDIS_TAIL) {
listInsertNode(subject->ptr,entry->ln,value,AL_START_TAIL);
} else {
listInsertNode(subject->ptr,entry->ln,value,AL_START_HEAD);
}
incrRefCount(value);
} else {
redisPanic("Unknown list encoding");
}
}
PUSH系列命令的底层实现:
void pushGenericCommand(redisClient *c, int where) {
int j, waiting = 0, pushed = 0;
// 取出列表对象
robj *lobj = lookupKeyWrite(c->db,c->argv[1]);
// 如果列表对象不存在,那么可能有客户端在等待这个键的出现
int may_have_waiting_clients = (lobj == NULL);
if (lobj && lobj->type != REDIS_LIST) {
addReply(c,shared.wrongtypeerr);
return;
}
// 将列表状态设置为就绪
if (may_have_waiting_clients) signalListAsReady(c,c->argv[1]);
// 遍历所有输入值,并将它们添加到列表中
for (j = 2; j < c->argc; j++) {
// 编码值
c->argv[j] = tryObjectEncoding(c->argv[j]);
// 如果列表对象不存在,那么创建一个,并关联到数据库
if (!lobj) {
lobj = createZiplistObject();
dbAdd(c->db,c->argv[1],lobj);
}
// 将值推入到列表
listTypePush(lobj,c->argv[j],where);
pushed++;
}
// 返回添加的节点数量
addReplyLongLong(c, waiting + (lobj ? listTypeLength(lobj) : 0));
// 如果至少有一个元素被成功推入,那么执行以下代码
if (pushed) {
char *event = (where == REDIS_HEAD) ? "lpush" : "rpush";
// 发送键修改信号
signalModifiedKey(c->db,c->argv[1]);
// 发送事件通知
notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event,c->argv[1],c->db->id);
}
server.dirty += pushed;
}
索引
索引的策略很简单,判断是哪种编码格式调用哪种遍历:
void lindexCommand(redisClient *c) {
robj *o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk);
if (o == NULL || checkType(c,o,REDIS_LIST)) return;
long index;
robj *value = NULL;
// 取出整数值对象 index
if ((getLongFromObjectOrReply(c, c->argv[2], &index, NULL) != REDIS_OK))
return;
// 根据索引,遍历 ziplist ,直到指定位置
if (o->encoding == REDIS_ENCODING_ZIPLIST) {
unsigned char *p;
unsigned char *vstr;
unsigned int vlen;
long long vlong;
p = ziplistIndex(o->ptr,index);
if (ziplistGet(p,&vstr,&vlen,&vlong)) {
if (vstr) {
value = createStringObject((char*)vstr,vlen);
} else {
value = createStringObjectFromLongLong(vlong);
}
addReplyBulk(c,value);
decrRefCount(value);
} else {
addReply(c,shared.nullbulk);
}
// 根据索引,遍历双端链表,直到指定位置
} else if (o->encoding == REDIS_ENCODING_LINKEDLIST) {
listNode *ln = listIndex(o->ptr,index);
if (ln != NULL) {
value = listNodeValue(ln);
addReplyBulk(c,value);
} else {
addReply(c,shared.nullbulk);
}
} else {
redisPanic("Unknown list encoding");
}
}
pop操作
pop操作其实就是将队列的pop操作而已,弹出元素后如果列表为空,则删除该列表:
void popGenericCommand(redisClient *c, int where) {
// 取出列表对象
robj *o = lookupKeyWriteOrReply(c,c->argv[1],shared.nullbulk);
if (o == NULL || checkType(c,o,REDIS_LIST)) return;
// 弹出列表元素
robj *value = listTypePop(o,where);
// 根据弹出元素是否为空,决定后续动作
if (value == NULL) {
addReply(c,shared.nullbulk);
} else {
char *event = (where == REDIS_HEAD) ? "lpop" : "rpop";
//回复给客户端,弹出的value
addReplyBulk(c,value);
//引用计数减1
decrRefCount(value);
notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event,c->argv[1],c->db->id);
if (listTypeLength(o) == 0) {
notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",
c->argv[1],c->db->id);
dbDelete(c->db,c->argv[1]);
}
signalModifiedKey(c->db,c->argv[1]);
server.dirty++;
}
}
裁剪操作
//LTRIM命令
void ltrimCommand(redisClient *c) {
robj *o;
long start, end, llen, j, ltrim, rtrim;
list *list;
listNode *ln;
// 取出索引值 start 和 end
if ((getLongFromObjectOrReply(c, c->argv[2], &start, NULL) != REDIS_OK) ||
(getLongFromObjectOrReply(c, c->argv[3], &end, NULL) != REDIS_OK)) return;
// 取出列表对象
if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.ok)) == NULL ||
checkType(c,o,REDIS_LIST)) return;
// 列表长度
llen = listTypeLength(o);
/* convert negative indexes */
// 将负数索引转换成正数索引
if (start < 0) start = llen+start;
if (end < 0) end = llen+end;
if (start < 0) start = 0;
/* Invariant: start >= 0, so this test will be true when end < 0.
* The range is empty when start > end or start >= length. */
if (start > end || start >= llen) {
/* Out of range start or start > end result in empty list */
ltrim = llen;
rtrim = 0;
} else {
if (end >= llen) end = llen-1;
ltrim = start;
rtrim = llen-end-1;
}
/* Remove list elements to perform the trim */
// 删除指定列表两端的元素
if (o->encoding == REDIS_ENCODING_ZIPLIST) {
// 删除左端元素
o->ptr = ziplistDeleteRange(o->ptr,0,ltrim);
// 删除右端元素
o->ptr = ziplistDeleteRange(o->ptr,-rtrim,rtrim);
} else if (o->encoding == REDIS_ENCODING_LINKEDLIST) {
list = o->ptr;
// 删除左端元素
for (j = 0; j < ltrim; j++) {
ln = listFirst(list);
listDelNode(list,ln);
}
// 删除右端元素
for (j = 0; j < rtrim; j++) {
ln = listLast(list);
listDelNode(list,ln);
}
} else {
redisPanic("Unknown list encoding");
}
// 发送通知
notifyKeyspaceEvent(REDIS_NOTIFY_LIST,"ltrim",c->argv[1],c->db->id);
// 如果列表已经为空,那么删除它
if (listTypeLength(o) == 0) {
dbDelete(c->db,c->argv[1]);
notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",c->argv[1],c->db->id);
}
signalModifiedKey(c->db,c->argv[1]);
server.dirty++;
addReply(c,shared.ok);
}
移除列表中指定的值
void lremCommand(redisClient *c) {
robj *subject, *obj;
// 编码目标对象 elem
obj = c->argv[3] = tryObjectEncoding(c->argv[3]);
long toremove;
long removed = 0;
listTypeEntry entry;
// 取出指定删除模式的 count 参数
if ((getLongFromObjectOrReply(c, c->argv[2], &toremove, NULL) != REDIS_OK))
return;
// 取出列表对象
subject = lookupKeyWriteOrReply(c,c->argv[1],shared.czero);
if (subject == NULL || checkType(c,subject,REDIS_LIST)) return;
/* Make sure obj is raw when we're dealing with a ziplist */
if (subject->encoding == REDIS_ENCODING_ZIPLIST)
obj = getDecodedObject(obj);
listTypeIterator *li;
// 根据 toremove 参数,决定是从表头还是表尾开始进行删除
if (toremove < 0) {
toremove = -toremove;
li = listTypeInitIterator(subject,-1,REDIS_HEAD);
} else {
li = listTypeInitIterator(subject,0,REDIS_TAIL);
}
// 查找,比对对象,并进行删除
while (listTypeNext(li,&entry)) {
if (listTypeEqual(&entry,obj)) {
listTypeDelete(&entry);
server.dirty++;
removed++;
// 已经满足删除数量,停止
if (toremove && removed == toremove) break;
}
}
listTypeReleaseIterator(li);
/* Clean up raw encoded object */
if (subject->encoding == REDIS_ENCODING_ZIPLIST)
decrRefCount(obj);
// 删除空列表
if (listTypeLength(subject) == 0) dbDelete(c->db,c->argv[1]);
addReplyLongLong(c,removed);
if (removed) signalModifiedKey(c->db,c->argv[1]);
}
阻塞版pop
阻塞版本的pop其实与普通pop操作没两样,只是加了阻塞操作而已,假如列表为空,阻塞时间为3s,那么客户端要等到3s后返回,如果在期间push了元素,那么立即返回该元素。其底层实现如下:
void blockingPopGenericCommand(redisClient *c, int where) {
robj *o;
mstime_t timeout;
int j;
// 取出 timeout 参数
if (getTimeoutFromObjectOrReply(c,c->argv[c->argc-1],&timeout,UNIT_SECONDS)
!= REDIS_OK) return;
// 遍历所有列表键
for (j = 1; j < c->argc-1; j++) {
// 取出列表键
o = lookupKeyWrite(c->db,c->argv[j]);
// 有非空列表?
if (o != NULL) {
if (o->type != REDIS_LIST) {
addReply(c,shared.wrongtypeerr);
return;
} else {
// 非空列表
if (listTypeLength(o) != 0) {
/* Non empty list, this is like a non normal [LR]POP. */
char *event = (where == REDIS_HEAD) ? "lpop" : "rpop";
// 弹出值
robj *value = listTypePop(o,where);
redisAssert(value != NULL);
// 回复客户端
addReplyMultiBulkLen(c,2);
// 回复弹出元素的列表
addReplyBulk(c,c->argv[j]);
// 回复弹出值
addReplyBulk(c,value);
decrRefCount(value);
notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event,
c->argv[j],c->db->id);
// 删除空列表
if (listTypeLength(o) == 0) {
dbDelete(c->db,c->argv[j]);
notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",
c->argv[j],c->db->id);
}
signalModifiedKey(c->db,c->argv[j]);
server.dirty++;
/* Replicate it as an [LR]POP instead of B[LR]POP. */
// 传播一个 [LR]POP 而不是 B[LR]POP
rewriteClientCommandVector(c,2,
(where == REDIS_HEAD) ? shared.lpop : shared.rpop,
c->argv[j]);
return;
}
}
}
}
/* If we are inside a MULTI/EXEC and the list is empty the only thing
* we can do is treating it as a timeout (even with timeout 0). */
// 如果命令在一个事务中执行,那么为了不产生死等待
// 服务器只能向客户端发送一个空回复
if (c->flags & REDIS_MULTI) {
addReply(c,shared.nullmultibulk);
return;
}
/* If the list is empty or the key does not exists we must block */
// 所有输入列表键都不存在,只能阻塞了,阻塞其指定的时间
blockForKeys(c, c->argv + 1, c->argc - 2, timeout, NULL);
}
// 根据给定数量的 key ,对给定客户端进行阻塞
// 参数:
// keys 任意多个 key
// numkeys keys 的键数量
// timeout 阻塞的最长时限
// target 在解除阻塞时,将结果保存到这个 key 对象,而不是返回给客户端
// 只用于 BRPOPLPUSH 命令
void blockForKeys(redisClient *c, robj **keys, int numkeys, mstime_t timeout, robj *target) {
dictEntry *de;
list *l;
int j;
// 设置阻塞状态的超时和目标选项
c->bpop.timeout = timeout;
// target 在执行 RPOPLPUSH 命令时使用
c->bpop.target = target;
if (target != NULL) incrRefCount(target);
// 关联阻塞客户端和键的相关信息
for (j = 0; j < numkeys; j++) {
/* If the key already exists in the dict ignore it. */
// c->bpop.keys 是一个集合(值为 NULL 的字典)
// 它记录所有造成客户端阻塞的键
// 以下语句在键不存在于集合的时候,将它添加到集合
if (dictAdd(c->bpop.keys,keys[j],NULL) != DICT_OK) continue;
incrRefCount(keys[j]);
/* And in the other "side", to map keys -> clients */
// c->db->blocking_keys 字典的键为造成客户端阻塞的键
// 而值则是一个链表,链表中包含了所有被阻塞的客户端
// 以下程序将阻塞键和被阻塞客户端关联起来
de = dictFind(c->db->blocking_keys,keys[j]);
if (de == NULL) {
// 链表不存在,新创建一个,并将它关联到字典中
int retval;
/* For every key we take a list of clients blocked for it */
l = listCreate();
retval = dictAdd(c->db->blocking_keys,keys[j],l);
incrRefCount(keys[j]);
redisAssertWithInfo(c,keys[j],retval == DICT_OK);
} else {
l = dictGetVal(de);
}
// 将客户端填接到被阻塞客户端的链表中
listAddNodeTail(l,c);
}
blockClient(c,REDIS_BLOCKED_LIST);
}