Redis 里做消息队列,最常见的两个选择是 List(LPUSH + BRPOP)和 Stream。它们常被拿来比较,但比较要有数据。这篇在同一台实例上量它们的内存、吞吐、延迟,再把两个最本质的差别摆出来:消费者崩溃时消息去哪了、能不能有多个消费方各读一份。
List 7349183 B (73.5 B/条) encoding=quicklist Stream 8173584 B (81.7 B/条) (每条含一个字段名 p)
List 用 quicklist 编码,73.5 字节/条;Stream 每条带一个字段名 p,81.7 字节/条,多约 8.2 字节。差别不大;消息更小或字段更多时会不同(见第 39 篇关于字段名的对比)。
LPUSH 768.4 k 条/秒 (三次: 766k, 768k, 769k) XADD 509.8 k 条/秒 (三次: 509k, 510k, 512k)
管道 2000 条一批、写 20 万条,三次取中位:LPUSH 768k/s,XADD 510k/s,XADD 慢约 34%。这是单连接管道的上限,不代表真实业务里的差距(真实业务通常受网络和处理逻辑限制)。
List: RPOP 取出 ['j1', 'j2', 'j3', 'j4', 'j5'] → 崩溃后,这 5 条在 Redis 里还找得到吗? LLEN = 5 (只剩没取的 5 条)
Stream: XREADGROUP 取出 ['1-0', '2-0', '3-0', '4-0', '5-0'] → 崩溃后 XPENDING:
> XPENDING S g
1) (integer) 5
2) "1-0"
3) "5-0"
4) 1) 1) "A"
2) "5"List 的 RPOP 是「取出即删除」:取走 5 条、消费者崩溃,LLEN 只剩没取的 5 条,取走的 5 条在 Redis 里再也查不到。Stream 的 XREADGROUP 把这 5 条放进 PEL,崩溃后 XPENDING 立刻能看到 5 条属于 A,可以认领重试。
LMOVE 取出 ['j1', 'j2', 'j3'] 崩溃后 L:proc = ['j3', 'j2', 'j1'] (要自己实现:超时检测、谁在处理、重试次数——Stream 的 PEL 里都有:消费者名、idle、delivery count)
List 想做到可靠,要用 LMOVE(取出时同时放进「处理中」列表),处理完再 LREM。可行,但超时检测、认领、重试次数、谁在处理都得自己实现;Stream 的 PEL 已经记录了消费者、空闲时间、投递次数。
billing 读到 ['1-0', '2-0', '3-0', '4-0', '5-0']
audit 读到 ['1-0', '2-0', '3-0', '4-0', '5-0']
> XRANGE S 2-0 3-0
1) 1) "2-0"
2) 1) "job"
2) "j2"
2) 1) "3-0"
2) 1) "job"
2) "j3"
List 弹出即消失:同一条消息只能被一个消费者拿到,读过就不能回看两个组 billing 和 audit 各自读到全部 5 条,读过的消息还能用 XRANGE 回看;List 的消息一旦被弹出就消失,同一条消息只能被一个消费者拿到。
LPUSH → BRPOP p50 0.191 ms p99 0.591 ms max 1.448 ms XADD → XREADGROUP BLOCK(不含 XACK) p50 0.208 ms p99 0.696 ms max 0.888 ms
消费者阻塞在 BRPOP / XREADGROUP BLOCK,生产者发 500 条各间隔 4 ms,量「发出 → 收到」:BRPOP p50 0.191 ms、p99 0.591 ms;XREADGROUP p50 0.208 ms、p99 0.696 ms。我先前第一次测的时候把 XACK 的往返也算了进去,得到 XREADGROUP 慢一倍的结论;拆开之后两者相近——这里的数字只计到「收到消息」为止,XACK 是额外的一次往返。