实战篇 28. Redis 消息队列 - 基于 Stream 的单消费模式学习文档

7 阅读3分钟

📚 实战篇 28. Redis 消息队列 - 基于 Stream 的单消费模式学习文档

一、 核心认知:什么是 Stream?

Stream 是 Redis 5.0 专门为消息队列设计的一种全新数据类型。

你可以把它想象成一个追加日志(Append-only Log) 。生产者把消息追加到日志的末尾,消费者则从日志中按顺序读取消息。

核心优势:

  1. 绝对持久化: 消息写入 Stream 后,会像 StringHash 一样被安全地保存在 Redis 内存/磁盘中,再也不怕“阅后即焚”丢数据了。
  2. 消息 ID 天然有序: Redis 会为写入 Stream 的每一条消息自动生成一个全局唯一的递增 ID(通常是 时间戳-序号,例如 1678901234567-0)。
  3. 结构化存储: 一条消息不仅仅是一个单纯的字符串,它可以包含多个键值对(类似于 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 的后续消息。

三、 单消费模式的“防漏消息”最佳实践 (面试重点)

如果你在后台线程写一个死循环,每次都用 $ 去阻塞读取最新消息,会发生什么惨剧?

❌ 错误代码逻辑(漏消息漏洞):

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) {
        // 异常处理逻辑...
    }
}

四、 单消费模式的局限性(为什么还要进化?)

通过上述代码,我们利用 XADDXREAD (lastId) 完美解决了一对一情况下的消息持久化和防丢失问题。

但是,如果我们的后台并发量依然太大,单台服务器的后台线程写库速度跟不上怎么办?

  • 我们想部署 3 台后台服务器一起来分担压力(负载均衡)。
  • 如果用刚才的 XREAD 单消费模式,这 3 台服务器都会读到一模一样的全量订单,导致同一个订单被重复插入 3 次数据库!