Redis实现消息队列(List-PubSub-Stream)#
一句话答案#
Redis 做消息队列有三代方案:List(LPUSH/BRPOP 简单可靠但不支持广播、无 ACK)、Pub/Sub(发布订阅支持广播但消息不持久、离线丢失)、Stream(5.0 引入的专业方案,支持消费组、ACK、持久化、回溯,最接近 Kafka)。能力递增:List < Pub/Sub < Stream。
核心要点
三种方案对比:
| 维度 | List | Pub/Sub | Stream(5.0+) |
|---|---|---|---|
| 模型 | 点对点队列 | 发布订阅(广播) | 消费组 + 点对点/广播 |
| 持久化 | ✅ 进 RDB/AOF | ❌ 即发即弃 | ✅ 进 RDB/AOF |
| 离线消息 | ✅ 留在队列 | ❌ 不在线就丢 | ✅ 留在 Stream |
| ACK 确认 | ❌ 无 | ❌ 无 | ✅ XACK + PEL |
| 消费组 | ❌(多消费者抢) | ❌(全收同一份) | ✅ 组内负载均衡 |
| 消息回溯 | ❌ 取出即删 | ❌ | ✅ 按 ID 重读 |
| 阻塞消费 | ✅ BRPOP | ✅ 订阅即推 | ✅ XREAD BLOCK |
| 典型缺陷 | 无广播、需轮询/阻塞 | 消息易丢、无堆积能力 | 用得少、运维生态弱 |
1. List 方案(最早、最简单)
LPUSH mq:order order1 # 生产者左进
BRPOP mq:order 0 # 消费者右出,阻塞等待(避免空轮询)bash- 本质是阻塞队列:FIFO、消息持久化、天然支持堆积
- 缺陷:不支持一对多广播;没有 ACK,消费者 BRPOP 取出后崩溃 → 消息丢失(可用 BLMOVE/RPOPLPUSH 进 backup 队列做”可靠队列”补救)
2. Pub/Sub 方案(发布订阅)
SUBSCRIBE channel:news # 订阅者
PUBLISH channel:news "hi" # 发布者,所有在线订阅者都收到bash- 一对多广播,实时性好;支持 PSUBSCRIBE 通配订阅
- 致命缺陷:消息不持久化、不堆积——发布瞬间没有在线订阅者就永久丢失;订阅者断线重连期间的消息也收不到
- 适合:实时通知、配置变更广播等”丢了无所谓”的场景,不能用于可靠投递
3. Stream 方案(5.0 专业消息队列)
XADD mq * field1 v1 # 追加消息,* 自动生成 ID(时间戳-序号)
XGROUP CREATE mq g1 0 # 创建消费组
XREADGROUP GROUP g1 c1 COUNT 10 BLOCK 0 STREAMS mq > # 组内消费者拉取
XACK mq g1 <id> # 确认消费完成
XPENDING mq g1 # 查未确认消息(PEL)
XCLAIM mq g1 c2 60000 <id> # 转交超时未 ACK 的消息给其他消费者bash- 完整 MQ 能力:持久化、消费组负载均衡、ACK 机制、PEL(Pending Entries List)记录未确认消息、消息回溯、阻塞读
- 解决了 List 无广播无 ACK、Pub/Sub 易丢的所有问题
- 仍要注意:单 Stream 数据在单机内存,需 XTRIM/MAXLEN 控制长度防止大 Key;高吞吐高可靠场景仍首选 Kafka/RocketMQ
面试回答(2分钟版)
用 Redis 实现消息队列有三种方案,能力是层层递进的。最早是用 List,生产者 LPUSH、消费者 BRPOP 阻塞拉取,它是个 FIFO 阻塞队列,消息能持久化也能堆积,但有两个硬伤:不支持一对多广播,而且没有 ACK 机制,消费者取出消息后崩溃就丢了,补救办法是用 RPOPLPUSH 把消息先转到备份队列做可靠队列。第二种是 Pub/Sub 发布订阅,支持一对多广播、实时性好,但消息完全不持久化也不堆积,发布的瞬间没有在线订阅者就永久丢失,订阅者断线期间的消息也收不到,所以只适合实时通知这种丢了无所谓的场景。第三种是 Redis 5.0 引入的 Stream,这是真正专业的方案,借鉴了 Kafka 的设计,支持消费组做负载均衡、ACK 确认、用 PEL 记录未确认消息、消息持久化和按 ID 回溯,前两种的缺陷它都解决了。总结就是 List 适合简单点对点队列、Pub/Sub 适合可丢的实时广播、Stream 适合需要可靠投递的场景。不过即便是 Stream,数据也都在单机内存、生态偏弱,真正高吞吐高可靠的业务我们还是会用 Kafka 或 RocketMQ,Redis 消息队列更多用在轻量级、想少引一个中间件的场景。
追问与易错
追问方向:
- “为什么不直接用 List 而用 BRPOP?”→ 普通 RPOP 取空要靠应用层空轮询,浪费 CPU;BRPOP 在队列空时阻塞挂起,有消息才唤醒,等价于事件驱动,避免忙等
- “List 怎么实现消息不丢(可靠队列)?”→ 用 RPOPLPUSH/BLMOVE 把消息原子地从主队列移到”处理中”备份队列,消费成功后再从备份队列删除;消费者崩溃时备份队列里的消息可被重新投递,类似 ACK 语义
- “Pub/Sub 消息为什么会丢?”→ 它是即发即弃的推模型,Redis 不为消息做任何存储,没有在线订阅者时直接丢弃;它本质是广播通道不是队列,没有堆积和重试能力
- “Stream 的 PEL 是什么?”→ Pending Entries List,每个消费组维护一份已投递但未 XACK 的消息列表;消费者宕机后这些消息留在 PEL,可用 XPENDING 查看、XCLAIM 转交给其他消费者重新处理,保证至少一次投递
- “Stream 和 Kafka 比差在哪?”→ Stream 数据在单机内存受容量限制(需 MAXLEN 裁剪)、无分区水平扩展能力、副本和持久化可靠性弱于 Kafka、生态工具少;优势是轻量、无需额外部署中间件、延迟低
易错点:
- ❌ “Redis Pub/Sub 能做可靠消息队列”——它不持久化,离线即丢,只能做实时广播
- ❌ “Stream 是新的数据结构、和 List 一样”——Stream 是 5.0 新增的独立类型,自带消费组/ACK/PEL,定位是消息队列而非简单列表
- ❌ 用 List/Stream 当队列却不控制长度——消息堆积会形成大 Key,需配合 LTRIM / XTRIM MAXLEN 限长