bigywater.tech
Redis · 42

Pending Entries List

消费组读走一条消息,并不意味着它已经处理完了。Redis 会把这条消息记进组里的 Pending Entries List(PEL),等消费者显式 XACK 才移除。PEL 是 Stream 「至少投递一次」语义的核心。这篇量一量它里面到底存了什么、怎么看、什么会让它变大。

4 项
PEL 每条记录:ID、消费者、上次投递后的 idle 毫秒、投递次数
1 → 2
用 XREADGROUP … 0 重读自己的历史一次,投递次数 +1
94.3 B/条
10 万条不 ack 时 PEL 增加的内存

读了不 ack:消息进 PEL

实测 · 本机执行输出
> XPENDING s g
1) (integer) 8
2) "1-0"
3) "8-0"
4) 1) 1) "a"
      2) "5"
   2) 1) "b"
      2) "3"

XPENDING s g 不带范围时返回汇总:PEL 总条数、最小和最大 ID、每个消费者各有几条(a 5 条、b 3 条)。

XPENDING 明细

实测 · 本机执行输出
> XPENDING s g - + 10
1) 1) "1-0"
   2) "a"
   3) (integer) 1202
   4) (integer) 1
2) 1) "2-0"
   2) "a"
   3) (integer) 1202
   4) (integer) 1
3) 1) "3-0"
   2) "a"
   3) (integer) 1202
   4) (integer) 1
4) 1) "4-0"
   2) "a"
   3) (integer) 1202
   4) (integer) 1
5) 1) "5-0"
   2) "a"
   3) (integer) 1202
   4) (integer) 1
6) 1) "6-0"
   2) "b"
   3) (integer) 1202
   4) (integer) 1
7) 1) "7-0"
   2) "b"
   3) (integer) 1202
   4) (integer) 1
8) 1) "8-0"
   2) "b"
   3) (integer) 1202
   4) (integer) 1

带范围参数时,每条返回 [ID, 消费者, idle 毫秒, 投递次数]。等了 1.2 秒之后,idle 都在 1202 毫秒上下。idle 一直涨,但 Redis 不会自动重新投递——这是留给消费者(或监控)处理的信号。

重读历史:XREADGROUP … 0

实测 · 本机执行输出
> XREADGROUP GROUP g b STREAMS s 0
1) 1) "s"
   2) 1) 1) "6-0"
         2) 1) "job"
            2) "j6"
      2) 1) "7-0"
         2) 1) "job"
            2) "j7"
      3) 1) "8-0"
         2) 1) "job"
            2) "j8"
> XPENDING s g - + 10 b
1) 1) "6-0"
   2) "b"
   3) (integer) 1
   4) (integer) 2
2) 1) "7-0"
   2) "b"
   3) (integer) 1
   4) (integer) 2
3) 1) "8-0"
   2) "b"
   3) (integer) 1
   4) (integer) 2

用 ID 0(而不是 >)读,返回的是「分配给这个消费者、还没 ack 的历史消息」。观察 b 的三条:重读之后 idle 变成 1 ms、投递次数从 1 变成 2。所以「投递次数」不只是被别人认领时增加,自己重读也会加。

实测 · 本机执行输出
> XPENDING s g IDLE 1000 - + 10 a
1) 1) "1-0"
   2) "a"
   3) (integer) 1203
   4) (integer) 1
2) 1) "2-0"
   2) "a"
   3) (integer) 1203
   4) (integer) 1
3) 1) "3-0"
   2) "a"
   3) (integer) 1203
   4) (integer) 1
4) 1) "4-0"
   2) "a"
   3) (integer) 1203
   4) (integer) 1
5) 1) "5-0"
   2) "a"
   3) (integer) 1203
   4) (integer) 1

XPENDING 可以加 IDLE 过滤,只看空闲超过某个毫秒数的条目,用来找「卡住」的消息。

NOACK:不进 PEL

实测 · 本机执行输出
> XPENDING s g2
1) (integer) 0
2) (nil)
3) (nil)
4) (nil)

g2 组读了 4 条,NOACK 让它们不进 PEL,XPENDING 显示 0。没有 PEL 就没有重投机制:消费者崩溃就丢了这几条。

PEL 只存 ID

实测 · 本机执行输出
> XDEL s 1-0
(integer) 1
> XPENDING s g - + 10 a
1) 1) "1-0"
   2) "a"
   3) (integer) 1204
   4) (integer) 1
2) 1) "2-0"
   2) "a"
   3) (integer) 1204
   4) (integer) 1
3) 1) "3-0"
   2) "a"
   3) (integer) 1204
   4) (integer) 1
4) 1) "4-0"
   2) "a"
   3) (integer) 1204
   4) (integer) 1
5) 1) "5-0"
   2) "a"
   3) (integer) 1204
   4) (integer) 1
> XREADGROUP GROUP g a STREAMS s 0
1) 1) "s"
   2) 1) 1) "1-0"
         2) (nil)
      2) 1) "2-0"
         2) 1) "job"
            2) "j2"
      3) 1) "3-0"
         2) 1) "job"
            2) "j3"
      4) 1) "4-0"
         2) 1) "job"
            2) "j4"
      5) 1) "5-0"
         2) 1) "job"
            2) "j5"

把 1-0 XDEL 之后,PEL 里的 1-0 还在(idle 继续涨);用 XREADGROUP … 0 重读时,1-0 的内容是 (nil)——ID 还在 PEL,消息已经不在流里。消费者得处理这种「有 ID 没内容」的情形。

代价:PEL 的内存

10 万条消息,读了不 ack
实测 · 本机执行输出
读之前 2447454 B;读完(PEL 100000 条)后 11875281 B;PEL 增加 9427827 B,平均 94.3 B/条

10 万条消息读走不 ack:MEMORY USAGE 从 2447454 B 涨到 11875281 B,PEL 增加约 94.3 字节/条。第 43 篇会看到 ack 之后这部分内存会还回去。

分步演示:一条消息在 PEL 里的一生

Pending Entries List
本步变化的记录

    本文用到的工具与边界

    • 本机 macOS 27 / Redis 8.10.2 独立实例(本机回环端口,save 关闭),redis-cli 风格输出由自写的 RESP 客户端打印;计时用 time.perf_counter,数字每次运行会有小幅抖动
    • 没有验证:PEL 在复制与 RDB/AOF 持久化中的形态、消费者数量很多时 XPENDING 的耗时、不同 value 大小对 PEL 每条内存的影响(这里的值只有十几个字节)
    京ICP备2026067502号-1京公网安备11010502063487号