KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
02 · Stream vs Pub/Sub vs List:到底什么时候用哪个 — keel 龙骨
上一章把 Stream 吃透了。这一章回答一个更实用的问题:Redis 里能「传消息」的结构至少有三种(List / Pub/Sub / Stream),我该选哪个? 选型不是比参数,而是比语义对不对口——用错结构,后面的坑全是你自己挖的。
上一章把 Stream 吃透了。这一章回答一个更实用的问题:Redis 里能「传消息」的结构至少有三种(List / Pub/Sub / Stream),我该选哪个? 选型不是比参数,而是比语义对不对口——用错结构,后面的坑全是你自己挖的。
一、先给三种结构定性
| 结构 | 一句话定性 | 消息会丢吗 | 能多个消费者分工吗 | 能补读历史吗 |
|---|---|---|---|---|
| List | 最朴素的队列 | 读走即删(lpop) | 不行,一条只给最先 pop 的 | 不行 |
| Pub/Sub | 即发即弃的广播 | 不在线就永远丢 | 所有订阅者都收到 | 不行(不落盘) |
| Stream | 持久化日志 + 消费者组 | 读完不删,ack 才删 | 可以(消费组分配) | 可以(从任意 ID 续读) |
三者不是「谁更高级」,是解决不同的问题。
二、逐个拆解:它解决什么、不解决什么
List —— 简单削峰够用,但 ack 自己写
LPUSH queue:tasks "{...}" # 生产者投递
RPOP queue:tasks # 消费者取走(取走即删)
解决的问题:最简单的「生产-消费」削峰。单机多进程抢着 RPOP,天然互斥,够用。
不解决:
- 取走即删——消费者取到后崩溃,任务就没了,无法重试;
- 没有「处理中 / 已完成」的区分,没法做超时重投;
- 多个消费者无法协调「你负责哪些、我负责哪些」。
所以 List 适合「任务丢了也不要紧 / 一定能重算」的场景,比如日志收集。
Pub/Sub —— 广播通知,不是队列
SUBSCRIBE notifications # 订阅
PUBLISH notifications "hi" # 发布,所有订阅者立刻收到
解决的问题:一对多广播、事件通知。「通知一下大家缓存失效了」这类。
不解决:
- 不落盘——发布时没人订阅,这条消息永久消失;
- 不保证到达,不保证顺序之外的一致性;
- 不能当队列用(消息没有「被谁消费」的概念)。
所以 Pub/Sub 适合「丢了就丢了」的实时通知,绝不适合「必须处理」的任务。
Stream —— 任务队列 + 事件日志的正式方案
上一章已经详述。它同时解决了 List 和 Pub/Sub 都解决不了的:ack 机制、多消费者协调、断线补读、故障转移(XCLAIM)。代价是复杂度最高,且必须自己管内存(XTRIM / 删除)。
三、决策表(直接照着选)
| 你的诉求 | 选 |
|---|---|
| 简单削峰,丢了能重算 | List |
| 一对多广播通知,丢了无所谓 | Pub/Sub |
| 一条任务必须被处理且只处理一次(可重试) | Stream 消费者组 |
| 前端要实时看到进度,断线要补历史 | Stream |
| Worker 会崩,要自动把卡住的任务转给别人 | Stream(XCLAIM) |
四、一个反例:把 Pub/Sub 当任务队列
有人图省事,用 Pub/Sub 把「异步任务」广播给消费者,以为「订阅的 Worker 会处理」。问题:
- Worker 重启的 10 秒里,所有发布的任务永久丢失;
- 两个 Worker 同时在线时,一条任务会被两个都执行(重复);
- 没有 ack,无法知道任务做完了没有。
这就是「语义不对口」的典型——Pub/Sub 是广播不是队列。换成 Stream 消费者组,三个问题一次性解决。
↓ 下一步:03 章 · 用 redis-py 手写最小消费组 —— 别急着用 ARQ,自己先搓一遍。