Redis 消息队列全网精讲:从原理到实战,一篇搞定面试+开发
前言
很多同学学 Redis 消息队列时都会遇到这些灵魂拷问:
- List、PubSub、Stream 到底该选谁?
- 为什么 Stream 是生产级首选?
- 消费者组 ACK、Pending-List 到底解决什么问题?
- 秒杀场景为什么要用 Stream 做异步下单?
本文不讲废话,只讲你最容易忘、面试最爱问、开发最容易踩坑的内容,结构清晰,可直接当博客发布。
一、消息队列到底解决什么问题?(必背)
1.1 三大角色
- 生产者:发消息
- 消息队列:存消息
- 消费者:消费消息
1.2 核心价值(高频考点)
- 解耦:生产者不依赖消费者,系统更灵活
- 异步:无需同步等待,接口响应极快
- 削峰:高并发流量排队,保护数据库
秒杀场景经典用法:
Redis 校验资格 → 发消息到队列 → 异步创建订单
前端快速返回,后端慢慢消费,完美抗住高并发
二、Redis 三种消息队列对比(面试必考)
Redis 实现消息队列有三种方案,一定要记住优缺点和适用场景,这是最容易忘的点!
2.1 基于 List 实现(最原始)
原理 :利用双向链表实现 FIFO,LPUSH + BRPOP
优点
- 支持 Redis 持久化
- 消息有序
- 不受 JVM 内存限制
缺点
- 只能单消费者
- 无法避免消息丢失
- 不支持广播
适用场景:简单单消费者业务,不推荐生产
2.2 基于 PubSub 发布订阅(Redis 2.0)
原理:发布-订阅模型,多消费者可同时收到消息
优点
- 支持多生产多消费
- 实时性高
缺点
- 不支持持久化
- 消息极易丢失
- 消费者离线就丢消息
适用场景:临时通知、广播,不适合核心业务
2.3 基于 Stream(Redis 5.0 生产级王者)
真正企业级消息队列,支持持久化、ACK、消费者组、回溯。
优点
- 消息可持久化、可回溯
- 支持消费者组负载均衡
- 有 ACK 确认机制
- Pending-List 保证消息至少消费一次
- 无漏读、不丢失
缺点
- 学习成本稍高
适用场景:秒杀、订单、支付等核心业务(秒杀标配)
2.4 三张表总结(建议收藏)
| 方案 | 持久化 | 多消费者 | 漏读 | 消息丢失 | 生产可用 |
|---|---|---|---|---|---|
| List | ✅ | ❌ | ✅ | ✅ | ❌ |
| PubSub | ❌ | ✅ | ❌ | ✅ | ❌ |
| Stream | ✅ | ✅ | ❌ | ❌ | ✅ |
一句话结论:生产只推荐 Stream
三、Stream 核心知识点(最容易忘,背会涨薪)
3.1 XREAD 与 XREADGROUP 区别
XREAD
- 多消费者都能收到同一条消息
- 无 ACK,无 Pending-List
- 存在漏读风险
XREADGROUP(消费者组)
- 一条消息只被一个消费者消费
- ACK 机制:必须手动确认
- Pending-List:保存未 ACK 消息,异常可重试
- 无漏读、负载均衡
高频面试题:为什么消费者组要 ACK?
答:保证消息至少被消费一次,避免业务异常导致消息丢失。
3.2 关键符号含义(极易忘)
>:读取下一条未消费的新消息0:从 Pending-List 读取未 ACK 的历史消息$:读取最新消息(XREAD 使用,会漏读)
3.3 核心命令
bash
# 创建队列+消费者组
XGROUP CREATE stream.orders g1 0 MKSTREAM
# 消费者组消费
XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders >
# 确认消息
XACK stream.orders g1 消息ID
四、秒杀场景实战:Stream 异步下单(面试必问)
4.1 为什么要用 Stream 做秒杀?
- Redis 抗高并发,Lua 原子判断库存+一人一单
- 异步削峰,避免数据库被打崩
- 消息不丢失、不重复、可重试
- 完美契合秒杀业务模型
4.2 流程(背会直接讲给面试官)
- Lua 脚本判断库存、用户资格
- 校验通过 → XADD 写入 Stream
- 后台线程使用消费者组监听消息
- 消费 → 创建订单 → ACK
- 异常时重试 Pending-List 消息
4.3 核心消费代码(生产可用)
java
@Slf4j
private class VoucherOrderHandler implements Runnable {
@Override
public void run() {
while (true) {
try {
// 1. 读取新消息
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
);
if (list == null || list.isEmpty()) continue;
// 2. 解析消息
MapRecord<String, Object, Object> record = list.get(0);
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(record.getValue(), new VoucherOrder(), true);
// 3. 创建订单(事务+幂等)
createVoucherOrder(voucherOrder);
// 4. ACK 确认
stringRedisTemplate.opsForStream().acknowledge("stream.orders", "g1", record.getId());
} catch (Exception e) {
log.error("订单处理异常", e);
handlePendingList();
}
}
}
// 处理未ACK消息,保证不丢失
private void handlePendingList() {
while (true) {
try {
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create("stream.orders", ReadOffset.from("0"))
);
if (list == null || list.isEmpty()) break;
MapRecord<String, Object, Object> record = list.get(0);
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(record.getValue(), new VoucherOrder(), true);
createVoucherOrder(voucherOrder);
stringRedisTemplate.opsForStream().acknowledge("stream.orders", "g1", record.getId());
} catch (Exception e) {
try { Thread.sleep(20); } catch (InterruptedException ignored) {}
}
}
}
}
五、高频疑问解答
Q1:消费者组为什么必须处理 Pending-List?
答:消费者崩溃、网络异常会导致消息未 ACK,这些消息会留在 Pending-List,不处理就相当于消息丢失,所以必须循环重试。
Q2:XREAD 为什么会漏读?
答:使用 $ 只读最新消息,如果中间堆积多条,只会读最后一条,导致消息丢失。
Q3:Stream 可以替代 Kafka 吗?
答:简单业务可以,高可靠性、事务消息、回溯查询等复杂场景还是 Kafka 更强。
Q4:秒杀为什么不用阻塞队列 ArrayBlockingQueue?
答:单机可行,集群环境下消息不同步,Stream 是分布式消息队列,适合集群部署。
六、总结(看完这一段就够复习)
- Redis 消息队列三种方案:List、PubSub、Stream
- 只有 Stream 适合生产,支持 ACK、消费者组、Pending-List
- 消费者组 = 负载均衡 + 消息可靠
- ACK + Pending-List = 保证消息至少消费一次
- 秒杀架构标配:Lua + Stream + 异步消费
本文覆盖 Redis 消息队列所有高频考点+易忘点+实战坑点,无论是面试还是开发,收藏这一篇就够了!