Redis消息队列-基于Stream的消息队列-消费者组

一、前言:为什么消费者组是 Redis Stream 的灵魂?

在高并发系统中,单个消费者处理能力有限,必须支持多实例并行消费

而 Redis Stream 的 消费者组(Consumer Group) 正是为此而生!

它实现了:

组内竞争消费 (负载均衡)

组间广播消费 (多业务订阅)

消息状态跟踪 (Pending Entries)

失败自动重试(通过 Pending 机制)

本文将带你彻底掌握消费者组的核心原理与 Spring Boot 实战技巧


二、消费者组核心概念图解

复制代码
                    ┌───────────────┐
                    │   Stream      │
                    │  (消息日志)    │
                    └───────┬───────┘
                            │
        ┌───────────────────┼───────────────────┐
        ▼                   ▼                   ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Consumer Group│ │ Consumer Group│ │ Consumer Group│
│   order_group │ │ notify_group  │ │ audit_group   │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
        │                 │                 │
  ┌─────▼─────┐     ┌─────▼─────┐     ┌─────▼─────┐
  │ worker-1  │     │ sms-sender│     │ log-audit │
  ├───────────┤     ├───────────┤     ├───────────┤
  │ worker-2  │     │ email-svr │     │ db-sync   │
  └───────────┘     └───────────┘     └───────────┘

📌 关键规则

  • 同一组内的消费者竞争消费(一条消息只被一个 consumer 处理)
  • 不同组之间独立消费(每条消息被每个组各消费一次)

三、消费者组工作流程(含 ACK 与重试)

1️⃣ 创建消费者组

bash 复制代码
XGROUP CREATE mystream mygroup 0 MKSTREAM
  • 0 表示从第一条消息开始消费
  • MKSTREAM 自动创建 Stream(若不存在)

2️⃣ 消费者读取消息(组内)

bash 复制代码
XREADGROUP GROUP mygroup consumer-1 COUNT 1 STREAMS mystream >
  • > 表示只读未被组内任何消费者读过的新消息
  • 消息被读取后,进入 Pending Entries List(PEL)

3️⃣ 处理成功 → ACK

bash 复制代码
XACK mystream mygroup 1698765432123-0
  • 消息从 PEL 中移除
  • 不再被重复投递

4️⃣ 处理失败?→ 自动重试!

  • 若未 ACK,消息一直留在 PEL 中
  • 其他消费者可通过 XPENDING 发现积压消息,并重新消费:
bash 复制代码
# 查看组内所有 pending 消息
XPENDING mystream mygroup

# 某消费者主动认领超时消息(实现重试)
XCLAIM mystream mygroup consumer-2 60000 1698765432123-0

无需死信队列,原生支持失败重试!


四、Spring Boot 实战:多消费者协同处理订单

场景:订单创建后,需异步完成「扣库存」「发通知」「记审计日志」

→ 使用 三个消费者组,每组可部署多个实例


步骤 1:定义通用消息结构

java 复制代码
public class StreamMessage {
    private String messageId;   // Redis 消息 ID
    private String stream;      // Stream 名称
    private Map<String, String> payload;
    // ...
}

步骤 2:封装消费者组管理器

java 复制代码
@Component
public class StreamConsumerGroupManager {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 安全创建消费者组(幂等)
     */
    public void createGroup(String stream, String group) {
        try {
            redisTemplate.execute((RedisCallback<Void>) conn -> {
                conn.streamCommands().xGroupCreate(
                    stream.getBytes(),
                    group.getBytes(),
                    ReadOffset.from("0-0"),
                    true
                );
                return null;
            });
        } catch (Exception e) {
            if (!e.getMessage().contains("BUSYGROUP")) {
                throw new RuntimeException("创建消费者组失败", e);
            }
        }
    }

    /**
     * 从指定组读取消息
     */
    public List<MapRecord<String, String, String>> read(
            String stream, String group, String consumer, int count) {
        return redisTemplate.opsForStream().read(
            Consumer.from(group, consumer),
            StreamReadOptions.empty().count(count).block(Duration.ofSeconds(2)),
            StreamOffset.create(stream, ReadOffset.lastConsumed())
        );
    }

    /**
     * ACK 消息
     */
    public void ack(String stream, String group, String... ids) {
        redisTemplate.opsForStream().acknowledge(group, stream, ids);
    }

    /**
     * 获取 Pending 消息(用于监控/重试)
     */
    public PendingMessagesSummary pending(String stream, String group) {
        return redisTemplate.opsForStream().pending(stream, Consumer.from(group, "*"));
    }
}

步骤 3:实现通用消费者基类

java 复制代码
public abstract class AbstractStreamConsumer {

    @Autowired
    protected StreamConsumerGroupManager manager;

    protected final String stream;
    protected final String group;
    protected final String consumerName;

    public AbstractStreamConsumer(String stream, String group, String consumerName) {
        this.stream = stream;
        this.group = group;
        this.consumerName = consumerName;
    }

    @PostConstruct
    public void start() {
        manager.createGroup(stream, group);
        Thread thread = new Thread(this::consumeLoop);
        thread.setDaemon(true);
        thread.setName("stream-consumer-" + group + "-" + consumerName);
        thread.start();
    }

    private void consumeLoop() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                List<MapRecord<String, String, String>> records =
                    manager.read(stream, group, consumerName, 10);

                for (MapRecord<String, String, String> record : records) {
                    try {
                        handleMessage(record.getValue());
                        manager.ack(stream, group, record.getId().getValue());
                    } catch (Exception e) {
                        log.error("处理消息失败, id={}", record.getId(), e);
                        // 不 ACK,留待重试
                    }
                }

                if (records.isEmpty()) Thread.sleep(500);
            } catch (Exception e) {
                log.error("消费异常", e);
                try { Thread.sleep(1000); } catch (InterruptedException ie) { break; }
            }
        }
    }

    protected abstract void handleMessage(Map<String, String> message);
}

步骤 4:具体业务消费者实现

java 复制代码
// 扣库存消费者组
@Component
public class InventoryConsumer extends AbstractStreamConsumer {
    public InventoryConsumer() {
        super("order_stream", "inventory_group", "inv-worker-" + UUID.randomUUID().toString().substring(0, 8));
    }

    @Override
    protected void handleMessage(Map<String, String> msg) {
        Long orderId = Long.parseLong(msg.get("orderId"));
        inventoryService.decreaseStock(orderId);
    }
}

// 通知消费者组
@Component
public class NotifyConsumer extends AbstractStreamConsumer {
    public NotifyConsumer() {
        super("order_stream", "notify_group", "notify-worker-" + ...);
    }

    @Override
    protected void handleMessage(Map<String, String> msg) {
        String userId = msg.get("userId");
        notificationService.sendOrderSuccess(userId);
    }
}

优势

  • 每个业务逻辑独立部署、独立扩展
  • 一个组挂了,不影响其他组
  • 组内可水平扩展多个实例

五、消费者组高级技巧

🔧 1. 消费者命名建议

  • 使用 服务名-实例ID(如 order-service-8081
  • 便于监控和排查问题

🔧 2. Pending 消息监控

java 复制代码
// 定时任务:检查积压
@Scheduled(fixedRate = 30000)
public void checkPending() {
    PendingMessagesSummary pending = manager.pending("order_stream", "inventory_group");
    if (pending.getTotalPendingMessages() > 100) {
        alertService.sendAlert("库存组消息积压: " + pending.getTotalPendingMessages());
    }
}

🔧 3. 手动重试超时消息

java 复制代码
// XCLAIM:将长时间未 ACK 的消息转移给新消费者
List<ClaimRecords> claims = redisTemplate.opsForStream().claim(
    "order_stream",
    Consumer.from("inventory_group", "new-worker"),
    Duration.ofMinutes(5), // 超时5分钟
    "1698765432123-0"
);

六、消费者组 vs 传统队列对比

特性 Redis Stream 消费者组 RabbitMQ Queue Kafka Consumer Group
消息持久化
ACK 机制 ✅(offset 提交)
多组广播 ✅(天然支持) ❌(需 Exchange)
消息回溯 ✅(XRANGE)
运维复杂度 ⭐(仅 Redis) ⭐⭐⭐ ⭐⭐⭐⭐
适用规模 中小项目 中大型 超大规模

📌 结论中小项目首选 Redis Stream 消费者组!


七、结语:消费者组,让异步处理更智能

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
四七伵1 小时前
数据库必修课:MySQL金额字段用decimal还是bigint?
数据库·后端
diaya2 小时前
麒麟V10 x86系统安装mysql
数据库·mysql
LaughingZhu2 小时前
Product Hunt 每日热榜 | 2026-02-24
大数据·数据库·人工智能·经验分享·搜索引擎
QEasyCloud20223 小时前
WooCommerce 独立站系统集成技术方案
java·前端·数据库
数据知道4 小时前
MongoDB 数组查询专项:`$all`、`$elemMatch` 与精确匹配数组的使用场景
数据库·mongodb
柒.梧.4 小时前
Java位运算详解:原理、用法及实战场景(面试重点)
开发语言·数据库·python
callJJ4 小时前
深入浅出 MVCC —— 从零理解 MySQL 并发控制
数据库·mysql·面试·并发·mvcc
小杜的生信筆記4 小时前
生信技能技巧小知识,Linux多线程压缩/解压工具
linux·数据库·redis
Smoothcloud润云4 小时前
Google DeepMind 学习系列笔记(3):Design And Train Neural Networks
数据库·人工智能·笔记·深度学习·学习·数据分析·googlecloud