Redis 6.2 RPOP 命令带 count 参数导致的性能退化分析

问题背景

为降低网络开销,将循环调用 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
相关推荐
代码丰1 小时前
实际例子理解Redis 缓存与 MySQL 数据一致性 以及常见的细节
redis·mysql·缓存
indexsunny2 小时前
互联网大厂Java面试实战:Spring Boot与微服务在电商场景中的应用
java·spring boot·redis·微服务·kafka·spring security·电商
QZ_orz_freedom2 小时前
后端学习笔记-Redis
redis·笔记·学习
2501_944521592 小时前
Flutter for OpenHarmony 微动漫App实战:骨架屏加载实现
android·开发语言·javascript·数据库·redis·flutter·缓存
时艰.2 小时前
Redis 核心知识点归纳与详解
数据库·redis·缓存
不想写bug呀13 小时前
Redis主从复制介绍
数据库·redis
XT462514 小时前
交易、订单轮询策略(能用数据库轮询解决的不用Redis,能用Redis解决的不用消息队列)
数据库·redis·bootstrap
梦茹^_^16 小时前
flask框架(笔记一次性写完)
redis·python·flask·cookie·session
panzer_maus17 小时前
Redis简单介绍(3)-持久化的实现
java·redis·mybatis