实战篇 29. Redis 消息队列 - Stream 的消费者组模式学习文档

4 阅读5分钟

在上一节的“单消费模式”中,我们虽然解决了消息持久化和防丢失的问题,但它无法应对大型互联网架构中最常见的场景:集群部署(负载均衡) 。如果你的后台服务部署了 3 台服务器,使用单消费模式,这 3 台服务器会收到一模一样的全量订单,导致数据库被重复插入。

为了让多台服务器“协同作战、分摊压力”,并且拥有更强大的容错机制,Redis 借鉴了 Kafka 的设计,在 Stream 中引入了消费者组


📚 实战篇 29. Redis 消息队列 - Stream 的消费者组模式学习文档

一、 核心概念:什么是消费者组?

消费者组(Consumer Group) 是将多个消费者划分到一个组内,共同去消费同一个 Stream 的消息。

加入消费者组后,我们会获得三大无与伦比的企业级特性:

  1. 消息分发(负载均衡): 同一个组内的多个消费者,会竞争队列里的消息。一条消息只要被组内的消费者 A 拿走了,消费者 B 就绝对不会再收到这条消息。这就完美实现了集群环境下的任务分摊,极大地加快了写库速度!
  2. 游标记忆(自动记录进度): 消费者组在 Redis 底层维护了一个游标(last_delivered_id),记录组内到底消费到哪条消息了。就算所有的消费者都重启了,连上之后依然能接着上次的进度继续消费,不需要我们在 Java 代码里手动维护 lastId 了。
  3. 消息确认与重投递机制(PEL 机制 - 最强兜底): 消费者拿走消息后,消息处于“未确认”状态,会被放入一个叫 PEL (Pending Entries List,待处理列表) 的地方。只有消费者成功执行完业务并向 Redis 发送一条 ACK(确认应答) ,这条消息才会真正从 PEL 中移除。如果消费者宕机了,消息会永远躺在 PEL 里,等待被重新拉取消费,绝对保证消息不丢失!

二、 核心命令实操

要使用消费者组,我们需要掌握以下几个核心命令:

1. 创建消费者组:XGROUP CREATE

  • 语法: XGROUP CREATE key groupName ID [MKSTREAM]
  • 演示: XGROUP CREATE stream:orders g1 0 MKSTREAM
  • 解析: 创建一个名为 g1 的组来监听 stream:orders0 代表从队列的第一条消息开始读(如果是 $ 代表只读未来的新消息)。MKSTREAM 代表如果这个队列不存在,就自动创建它。

2. 分组读取消息:XREADGROUP

  • 语法: XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]

  • 演示(读取最新消息): XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream:orders >

  • 解析: 消费者 c1 作为 g1 组的成员去读取消息。

    • 核心考点:特殊的 ID > 。在这里,> 代表读取组内从未被任何消费者读取过的新消息

3. 确认消息:XACK

  • 语法: XACK key group ID [ID ...]
  • 演示: XACK stream:orders g1 1678901234567-0
  • 解析: 告诉 Redis,这条消息我已经成功写入 MySQL 了,你可以把它从 PEL (待处理列表) 中彻底删除了。

4. 查看未确认的消息:XPENDING

  • 语法: XPENDING key group (查看 PEL 的总体概况)

  • 演示(重新读取 PEL 中的消息): XREADGROUP GROUP g1 c1 COUNT 1 STREAMS stream:orders 0

    • 核心考点:特殊的 ID 0。当 ID 传入 0 时,它不是去拿新消息,而是去获取 PEL 中自己已经读取过、但还没来得及 ACK 的历史积压消息(比如上次处理到一半宕机了)。

三、 完美的 Java 后台消费逻辑 (企业级标准模板)

结合新消息(>)和 积压消息(0),我们可以写出一个绝对安全、不漏消息、支持集群分发的消费者死循环逻辑。

这也是你在项目中替换 BlockingQueue 的最终代码骨架:

Java

// 在项目启动时运行的后台线程逻辑
while(true) {
    try {
        // 1. 尝试读取【新消息】 (使用 >)
        // XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream:orders >
        List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(...);
        
        // 2. 判断是否拿到新消息
        if (list == null || list.isEmpty()) {
            continue; // 没拿到,说明没人在抢购,进入下一次阻塞等待
        }
        
        // 3. 拿到新消息,解析订单对象,执行数据库操作 (扣库存、写订单)
        MapRecord<String, Object, Object> record = list.get(0);
        VoucherOrder voucherOrder = parse(record); 
        handleVoucherOrder(voucherOrder); 
        
        // 4. 【最关键的一步】:业务执行成功,发送 ACK 确认!
        stringRedisTemplate.opsForStream().acknowledge("stream:orders", "g1", record.getId());
        
    } catch (Exception e) {
        log.error("处理订单异常,消息尚未 ACK,准备进入 PEL 异常处理逻辑", e);
        
        // 走到这里,说明 handleVoucherOrder 报错了,或者断电了没执行 ACK。
        // 此时必须进入死循环,不断去处理 PEL 中的积压消息,直到处理成功为止!
        while (true) {
            try {
                // 5. 尝试读取【PEL 积压消息】 (注意:这里不用阻塞,且 ID 使用 0)
                // XREADGROUP GROUP g1 c1 COUNT 1 STREAMS stream:orders 0
                List<MapRecord<String, Object, Object>> pendingList = stringRedisTemplate.opsForStream().read(...);
                
                if (pendingList == null || pendingList.isEmpty()) {
                    break; // PEL 空了,说明异常消息都处理完了,跳出内层循环,回去接着读新消息
                }
                
                // 6. 重新执行业务逻辑
                MapRecord<String, Object, Object> pendingRecord = pendingList.get(0);
                VoucherOrder pendingOrder = parse(pendingRecord);
                handleVoucherOrder(pendingOrder);
                
                // 7. 处理成功,补发 ACK!
                stringRedisTemplate.opsForStream().acknowledge("stream:orders", "g1", pendingRecord.getId());
                
            } catch (Exception innerE) {
                // 如果处理 PEL 依然报错,就休眠一会儿再重试,防止死循环把 CPU 打满
                Thread.sleep(20); 
            }
        }
    }
}

学习总结 (秒杀优化大结局)

至此,我们的秒杀下单架构演进彻底通关!我们经历了:

  1. 同步下单: 纯 MySQL 抗压,响应极慢,极其容易崩溃。
  2. 异步下单 (BlockingQueue): 引入内存队列,速度起飞,但面临 OOM 和 宕机丢数据 风险。
  3. 异步下单 (Redis Stream 单消费): 解决了持久化,但不方便集群化部署。
  4. 终极架构 (Redis Stream 消费者组): 完美实现了 异步削峰 + 集群负载均衡 + 宕机断点续传 + 强一致性 ACK 确认。这就是目前大型互联网公司最标准的轻量级高并发处理方案。