📚 实战篇 28. Redis 消息队列 - 基于 Stream 的单消费模式学习文档
一、 核心认知:什么是 Stream?
Stream 是 Redis 5.0 专门为消息队列设计的一种全新数据类型。
你可以把它想象成一个追加日志(Append-only Log) 。生产者把消息追加到日志的末尾,消费者则从日志中按顺序读取消息。
核心优势:
- 绝对持久化: 消息写入 Stream 后,会像
String或Hash一样被安全地保存在 Redis 内存/磁盘中,再也不怕“阅后即焚”丢数据了。 - 消息 ID 天然有序: Redis 会为写入 Stream 的每一条消息自动生成一个全局唯一的递增 ID(通常是
时间戳-序号,例如1678901234567-0)。 - 结构化存储: 一条消息不仅仅是一个单纯的字符串,它可以包含多个键值对(类似于 Hash),非常适合存储“订单对象”。
二、 核心命令实操:XADD 与 XREAD
在“单消费模式”(独立消费模式)下,我们主要使用两个命令:发送消息的 XADD 和 读取消息的 XREAD。
1. 生产者发送消息:XADD
-
语法:
XADD key [MAXLEN threshold] *|ID field value [field value ...] -
演示:
XADD stream:orders * userId 1001 voucherId 5 -
解析:
*:代表让 Redis 自动生成单调递增的消息 ID。userId 1001 voucherId 5:这就是消息体,典型的键值对结构。
-
防 OOM 技巧: 如果怕 Stream 无限增长撑爆内存,可以加上
MAXLEN参数(例如XADD stream:orders MAXLEN 100000 * ...),Redis 会自动淘汰旧消息,保持队列长度。
2. 消费者读取消息:XREAD
-
语法:
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...] -
演示 1(阻塞读最新):
XREAD COUNT 1 BLOCK 2000 STREAMS stream:orders $BLOCK 2000:如果没有消息,最多阻塞等待 2000 毫秒(填 0 代表死等)。$:这是一个特殊的 ID,代表只读取最新到达的消息(类似于 Unix 的tail -f)。
-
演示 2(按 ID 读历史):
XREAD COUNT 1 STREAMS stream:orders 1678901234567-0- 代表读取 ID 大于
1678901234567-0的后续消息。
- 代表读取 ID 大于
三、 单消费模式的“防漏消息”最佳实践 (面试重点)
如果你在后台线程写一个死循环,每次都用 $ 去阻塞读取最新消息,会发生什么惨剧?
❌ 错误代码逻辑(漏消息漏洞):
Java
while(true) {
// 永远用 $ 阻塞读取最新的一条消息
Message msg = redis.execute("XREAD COUNT 1 BLOCK 2000 STREAMS stream:orders $");
// 假设处理写数据库花了 1 秒钟
handle(msg);
}
漏洞剖析: 就在你处理写库的那 1 秒钟里,前台瞬间又生成了 3 条订单消息。等你循环回来再次执行 XREAD ... $ 时,$ 指示 Redis 去拿从此刻开始的最新消息。那么中间漏掉的那 3 条消息,你永远也读不到了!
✅ 正确的单消费代码逻辑(记录偏移量):
为了保证一条消息都不漏,消费者必须自己“长记性”,记住上一条处理完的消息 ID 是多少。
Java
// 初始化时,从 0 或者是 $ 开始读
String lastId = "0-0";
while(true) {
try {
// 1. 根据记住的 lastId 去读后面的新消息
Message msg = redis.execute("XREAD COUNT 1 BLOCK 2000 STREAMS stream:orders " + lastId);
if (msg == null) {
continue; // 没拿到,继续下一轮阻塞
}
// 2. 拿到消息,处理业务逻辑 (写 MySQL)
handle(msg);
// 3. 业务处理成功后,更新 lastId 为当前消息的 ID!
// 这样下一轮循环就会从这条消息之后继续读,绝对不漏!
lastId = msg.getId();
} catch (Exception e) {
// 异常处理逻辑...
}
}
四、 单消费模式的局限性(为什么还要进化?)
通过上述代码,我们利用 XADD 和 XREAD (lastId) 完美解决了一对一情况下的消息持久化和防丢失问题。
但是,如果我们的后台并发量依然太大,单台服务器的后台线程写库速度跟不上怎么办?
- 我们想部署 3 台后台服务器一起来分担压力(负载均衡)。
- 如果用刚才的
XREAD单消费模式,这 3 台服务器都会读到一模一样的全量订单,导致同一个订单被重复插入 3 次数据库!