问题背景
为降低网络开销,将循环调用 RPOP list 优化为 RPOP list count 后,生产环境系统 TPS 显著下降。
使用 redis-benchmark 基准测试显示,RPOP list 1 的吞吐量远低于不带参数的 RPOP list。
shell
$ time redis-benchmark -n 50000 rpop mylist 1
====== rpop mylist 1 ======
Summary:
throughput summary: 9140.77 requests per second
latency summary (msec):
avg min p50 p95 p99 max
5.361 0.328 4.999 9.583 10.119 11.927
real 0m5.485s
user 0m0.292s
sys 0m0.463s
$ time redis-benchmark -n 50000 rpop mylist
====== rpop mylist ======
Summary:
throughput summary: 72463.77 requests per second
latency summary (msec):
avg min p50 p95 p99 max
0.379 0.128 0.359 0.559 0.863 3.599
real 0m0.705s
user 0m0.233s
sys 0m0.456s
分支差异
在 popGenericCommand 的关键分支中:
c
if (!count) {
/* Pop a single element. This is POP's original behavior that replies
* with a bulk string. */
value = listTypePop(o,where);
serverAssert(value != NULL);
addReplyBulk(c,value);
decrRefCount(value);
listElementsRemoved(c,c->argv[1],where,o,1);
} else {
/* Pop a range of elements. An addition to the original POP command,
* which replies with a multi-bulk. */
long llen = listTypeLength(o);
long rangelen = (count > llen) ? llen : count;
long rangestart = (where == LIST_HEAD) ? 0 : -rangelen;
long rangeend = (where == LIST_HEAD) ? rangelen - 1 : -1;
int reverse = (where == LIST_HEAD) ? 0 : 1;
addListRangeReply(c,o,rangestart,rangeend,reverse);
quicklistDelRange(o->ptr,rangestart,rangelen);
listElementsRemoved(c,c->argv[1],where,o,rangelen);
}
不带 count:直接调用 listTypePop -> quicklistPopCustom,通过 QUICKLIST_TAIL 指针立即定位 ,复杂度 O ( 1 ) O(1) O(1)。
c
robj *listTypePop(robj *subject, int where) {
long long vlong;
robj *value = NULL;
int ql_where = where == LIST_HEAD ? QUICKLIST_HEAD : QUICKLIST_TAIL;
if (subject->encoding == OBJ_ENCODING_QUICKLIST) {
if (quicklistPopCustom(subject->ptr, ql_where, (unsigned char **)&value,
NULL, &vlong, listPopSaver)) {
if (!value)
value = createStringObjectFromLongLong(vlong);
}
} else {
serverPanic("Unknown list encoding");
}
return value;
}
int quicklistPopCustom(quicklist *quicklist, int where, unsigned char **data,
unsigned int *sz, long long *sval,
void *(*saver)(unsigned char *data, unsigned int sz)) {
unsigned char *p;
unsigned char *vstr;
unsigned int vlen;
long long vlong;
int pos = (where == QUICKLIST_HEAD) ? 0 : -1;
if (quicklist->count == 0)
return 0;
if (data)
*data = NULL;
if (sz)
*sz = 0;
if (sval)
*sval = -123456789;
quicklistNode *node;
if (where == QUICKLIST_HEAD && quicklist->head) {
node = quicklist->head;
} else if (where == QUICKLIST_TAIL && quicklist->tail) {
node = quicklist->tail;
} else {
return 0;
}
p = ziplistIndex(node->zl, pos);
if (ziplistGet(p, &vstr, &vlen, &vlong)) {
if (vstr) {
if (data)
*data = saver(vstr, vlen);
if (sz)
*sz = vlen;
} else {
if (data)
*data = NULL;
if (sval)
*sval = vlong;
}
quicklistDelIndex(quicklist, node, &p);
return 1;
}
return 0;
}
带 count:调用 addListRangeReply,内部通过 listTypeInitIterator 初始化迭代器进行遍历。
c
void addListRangeReply(client *c, robj *o, long start, long end, int reverse) {
long rangelen, 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) {
addReply(c,shared.emptyarray);
return;
}
if (end >= llen) end = llen-1;
rangelen = (end-start)+1;
/* Return the result in form of a multi-bulk reply */
addReplyArrayLen(c,rangelen);
if (o->encoding == OBJ_ENCODING_QUICKLIST) {
int from = reverse ? end : start;
int direction = reverse ? LIST_HEAD : LIST_TAIL;
listTypeIterator *iter = listTypeInitIterator(o,from,direction);
while(rangelen--) {
listTypeEntry entry;
listTypeNext(iter, &entry);
quicklistEntry *qe = &entry.entry;
if (qe->value) {
addReplyBulkCBuffer(c,qe->value,qe->sz);
} else {
addReplyBulkLongLong(c,qe->longval);
}
}
listTypeReleaseIterator(iter);
} else {
serverPanic("Unknown list encoding");
}
}
listTypeIterator *listTypeInitIterator(robj *subject, long index,
unsigned char direction) {
listTypeIterator *li = zmalloc(sizeof(listTypeIterator));
li->subject = subject;
li->encoding = subject->encoding;
li->direction = direction;
li->iter = NULL;
/* LIST_HEAD means start at TAIL and move *towards* head.
* LIST_TAIL means start at HEAD and move *towards tail. */
int iter_direction =
direction == LIST_HEAD ? AL_START_TAIL : AL_START_HEAD;
if (li->encoding == OBJ_ENCODING_QUICKLIST) {
li->iter = quicklistGetIteratorAtIdx(li->subject->ptr,
iter_direction, index);
} else {
serverPanic("Unknown list encoding");
}
return li;
}
quicklistIter *quicklistGetIteratorAtIdx(const quicklist *quicklist,
const int direction,
const long long idx) {
quicklistEntry entry;
if (quicklistIndex(quicklist, idx, &entry)) {
quicklistIter *base = quicklistGetIterator(quicklist, direction);
base->zi = NULL;
base->current = entry.node;
base->offset = entry.offset;
return base;
} else {
return NULL;
}
}
而 quicklistIndex 实现有一个缺陷:给定负索引 会从 list 的队尾 开始查找,给定正索引 会从 list 的队头开始查找。
c
int quicklistIndex(const quicklist *quicklist, const long long idx,
quicklistEntry *entry) {
quicklistNode *n;
unsigned long long accum = 0;
unsigned long long index;
int forward = idx < 0 ? 0 : 1; /* 关键点:由索引正负决定遍历方向 */
initEntry(entry);
entry->quicklist = quicklist;
printf("forward: %d, idx: %lld\n",forward, idx);
if (!forward) {
index = (-idx) - 1;
n = quicklist->tail; // 只有负索引才从尾部找
} else {
index = idx;
n = quicklist->head; // 正索引(如 RPOP count 计算出的)从头开始遍历
}
...
}
问题点 :当执行 RPOP list 1 时,逻辑计算出的 from 索引是一个靠近队尾的正整数。代码因为索引为正,触发了 forward = 1,导致程序从 Head 开始跨越整个链表 去寻找 Tail 节点。此时,时间复杂度退化成 O ( N ) O(N) O(N) 其中 N 为队列元素个数。
定位优化
社区在 PR #9454 中优化了 quicklistIndex。

优化逻辑:不再单纯依赖 idx 的正负号,而是计算目标位置距离 Head 近还是 Tail 近。
c
int quicklistIndex(const quicklist *quicklist, const long long idx,
quicklistEntry *entry) {
quicklistNode *n;
unsigned long long accum = 0;
unsigned long long index;
int forward = idx < 0 ? 0 : 1; /* < 0 -> reverse, 0+ -> forward */
initEntry(entry);
entry->quicklist = quicklist;
index = forward ? idx : (-idx) - 1;
if (index >= quicklist->count)
return 0;
/* 如果从另一个方向查找路径更短,就改为从那个方向查找。 */
int seek_forward = forward;
unsigned long long seek_index = index;
if (index > (quicklist->count - 1) / 2) {
seek_forward = !forward;
seek_index = quicklist->count - 1 - index;
}
n = seek_forward ? quicklist->head : quicklist->tail;
while (likely(n)) {
if ((accum + n->count) > seek_index) {
break;
} else {
D("Skipping over (%p) %u at accum %lld", (void *)n, n->count,
accum);
accum += n->count;
n = seek_forward ? n->next : n->prev;
}
}
if (!n)
return 0;
/* Fix accum so it looks like we seeked in the other direction. */
if (seek_forward != forward) accum = quicklist->count - n->count - accum;
D("Found node: %p at accum %llu, idx %llu, sub+ %llu, sub- %llu", (void *)n,
accum, index, index - accum, (-index) - 1 + accum);
entry->node = n;
if (forward) {
/* forward = normal head-to-tail offset. */
entry->offset = index - accum;
} else {
/* reverse = need negative offset for tail-to-head, so undo
* the result of the original index = (-idx) - 1 above. */
entry->offset = (-index) - 1 + accum;
}
quicklistDecompressNodeForUse(entry->node);
entry->zi = ziplistIndex(entry->node->zl, entry->offset);
if (!ziplistGet(entry->zi, &entry->value, &entry->sz, &entry->longval))
assert(0); /* This can happen on corrupt ziplist with fake entry count. */
/* The caller will use our result, so we don't re-compress here.
* The caller can recompress or delete the node as needed. */
return 1;
}
结论
所以当 redis 版本低于 7.0 时,应该使用 LPOP list count 代替 RPOP list count。
shell
$ time redis-benchmark -n 50000 lpop mylist 1
====== lpop mylist 1 ======
Summary:
throughput summary: 71326.68 requests per second
latency summary (msec):
avg min p50 p95 p99 max
0.386 0.152 0.367 0.567 0.815 2.303
real 0m0.715s
user 0m0.233s
sys 0m0.469s