RocketMQ 消费者模式

📖 前置阅读:本文假设读者已掌握 SpringBoot RocketMQ 的基本消费操作。如果还不熟悉,建议先阅读 SpringBoot RocketMQ 全操作指南

一、⚡ 问题切入:一条消息,谁来消费?

前面五篇的消费者代码都默认了一件事——一条消息只被一个消费者实例处理。但实际业务中:

  • 订单消息——只能被一个实例消费(一个订单不能被两个服务处理两次)
  • 配置刷新消息——所有实例都要收到(所有缓存节点刷新缓存)
  • 部分 Tag 的消息——只关心"订单创建",对"订单支付"不感兴趣
  • 消费不过来——10 个 Queue,2 个实例,怎么分?

这四大问题的答案都在这一篇里。

二、集群消费 vs 广播消费

2.1 集群消费(CLUSTERING)——默认模式

同一个 ConsumerGroup 内的所有实例共享消费一个 Topic 的消息——每条消息只被组内一个实例消费

@Component
@RocketMQMessageListener(
    topic = "order-topic",
    consumerGroup = "order-consumer-group",
    messageModel = MessageModel.CLUSTERING   // 集群模式(默认值,可以不写)
)
public class OrderClusteringListener
        implements RocketMQListener<OrderMessage> {

    @Override
    public void onMessage(OrderMessage msg) {
        System.out.printf("[实例A] 处理订单: orderId=%d%n", msg.getOrderId());
    }
}

部署 3 个实例,Topic 有 8 个 Queue:

OrderConsumerGroup (3 个实例,8 个 Queue)

实例A → Queue-0, Queue-1, Queue-2    ← 分到 3 个 Queue
实例B → Queue-3, Queue-4, Queue-5    ← 分到 3 个 Queue
实例C → Queue-6, Queue-7             ← 分到 2 个 Queue

Queue-0 内的消息 [msg1, msg2, msg3]
    → 全部由实例A消费(不会被实例B或C消费)

2.2 广播消费(BROADCASTING)

同一个 ConsumerGroup 内的所有实例都收到 Topic 的全部消息

@Component
@RocketMQMessageListener(
    topic = "config-refresh-topic",
    consumerGroup = "config-broadcast-group",
    messageModel = MessageModel.BROADCASTING  // 广播模式
)
public class ConfigRefreshListener
        implements RocketMQListener<String> {

    @Override
    public void onMessage(String configKey) {
        System.out.printf("[实例%s] 收到配置刷新通知: %s%n",
                instanceId(), configKey);
        // 每个实例独立刷新自己的本地缓存
        cacheManager.refresh(configKey);
    }
}
维度CLUSTERINGBROADCASTING
每条消息被消费次数组内只有 1 次组内每个实例 1 次
消费进度 (offset) 存储Broker 统一管理每个实例本地存储
Rebalance支持(实例增减时 Queue 重新分配)不支持
适用场景订单处理、库存扣减配置刷新、缓存清除、系统通知

⚠️ 新手提示:广播模式下消息不会重试——因为消费进度存在本地,Broker 不知道你的消费状态。广播消费失败后 RocketMQ 不会将消息转入 %RETRY% Topic,需要在本地自己做容错。

三、Push 模式 vs Pull 模式

3.1 默认是 Push——但本质是"长轮询 Pull"

RocketMQ 的 Push 模式并不是 Broker 主动往 Consumer 推消息——它实际上是长轮询 Pull

Consumer → "有消息吗?" → Broker → "有" / "没有,等着(hold 15s)"

长轮询的工作流程:
    1. Consumer 向 Broker 发拉取请求
    2. 如果队列有消息 → Broker 立即返回
    3. 如果队列没消息 → Broker hold 住请求 15 秒
    4. 15 秒内有新消息到达 → 立即返回
    5. 15 秒到了还没消息 → 返回空,Consumer 立即发下一个拉取请求

为什么不用真正的 Push? 真正的 Push 是 Broker 往 Consumer 推——Broker 需要维护每个 Consumer 的 TCP 连接状态,影响横向扩展。Pull 模式下 Consumer 掌控消费节奏——快了多拉、慢了少拉,Broker 只管响应拉取请求。

3.2 什么时候用 Pull

SpringBoot Starter 默认使用 Push(DefaultMQPushConsumer)。如果需要更精细的控制(如流量控制、批量消费),可以用 Pull:

@Service
public class PullConsumerService {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public List<OrderMessage> pullMessages() {
        // 手动拉取:从 order-topic 的 Queue-0 拉取最多 32 条,offset 从 0 开始
        List<OrderMessage> messages = rocketMQTemplate.receive(
                "order-topic:created",  // Topic:Tag
                OrderMessage.class
        );

        // Pull 模式下自己决定什么时候 ACK
        return messages;
    }
}
维度Push(长轮询 Pull)真正 Pull
使用@RocketMQMessageListenerrocketMQTemplate.receive
消费进度管理自动(Broker 维护 offset)手动管理 offset
流量控制较粗糙(线程数 + prefetch)精细(自己决定拉取速率)
适用场景99% 的业务场景需精细控制消费速率的场景

四、消息过滤

4.1 Tag 过滤 —— 最简单高效

Tag 过滤在Broker 端通过 ConsumeQueue 的 hash 字段执行——没匹配的消息根本不传输到 Consumer。

// 只收 Tag=paid
@RocketMQMessageListener(
    topic = "order-topic",
    consumerGroup = "order-paid-group",
    selectorExpression = "paid",
    selectorType = SelectorType.TAG
)

// 收 paid 或 cancelled
selectorExpression = "paid || cancelled"

4.2 SQL92 过滤 —— 按消息属性过滤

SQL92 过滤基于消息的用户属性(User Properties),需要 Broker 开启 enablePropertyFilter=true

// 发送端——设置消息属性
public void sendWithProperties(OrderMessage msg) {
    Message<String> message = MessageBuilder
            .withPayload(JSON.toJSONString(msg))
            .setHeader("region", "cn-north")    // ← 自定义属性
            .setHeader("amount", msg.getAmount().toString())
            .setHeader("vip", msg.isVip() ? "true" : "false")
            .build();
    rocketMQTemplate.syncSend("order-topic:created", message);
}

// 消费端——SQL92 过滤
@Component
@RocketMQMessageListener(
    topic = "order-topic",
    consumerGroup = "vip-north-order-group",
    selectorExpression = "region = 'cn-north' AND vip = 'true' AND amount > 1000",
    selectorType = SelectorType.SQL92
)
public class VipNorthOrderListener
        implements RocketMQListener<OrderMessage> {
    @Override
    public void onMessage(OrderMessage msg) {
        // 只收到华北区域的 VIP 用户且金额 > 1000 的订单
    }
}

4.3 Broker 端过滤 vs Consumer 端过滤

Broker 端过滤(Tag / SQL92)Consumer 端过滤(收到后判断)
网络传输不匹配的不传输全拉过来再判断
Consumer 压力只处理匹配的消息不匹配的也要接收+丢弃
过滤能力Tag(简单)SQL92(丰富)任意逻辑(Java 代码)
推荐✅ 优先使用只在 Tag 和 SQL92 都覆盖不了时才用

五、Rebalance —— Queue 怎么分配到消费者实例

5.1 Rebalance 的触发时机

时机发生了什么
消费者实例增加新实例加入 ConsumerGroup → Queue 重新分配
消费者实例减少(宕机/下线)老实例离开 → 它的 Queue 分给其他实例
Topic 的 Queue 数变化Queue 增加 → 重新分配(但 Queue 减少不触发 Rebalance)

5.2 分配策略

RocketMQ 默认使用平均分配策略AllocateMessageQueueAveragely):

Topic: order (8 个 Queue)
ConsumerGroup: order-group (3 个实例)

AllocateMessageQueueAveragely:
    实例 1 → [0, 1, 2]
    实例 2 → [3, 4, 5]
    实例 3 → [6, 7]

AllocateMessageQueueByMachineRoom(按机房分配):
    实例 1 (机房A) → [0, 2, 4, 6]   ← 分机房A的 Queue
    实例 2 (机房A) → [1, 3, 5, 7]
    实例 3 (机房B) → []               ← 机房B没Queue,闲着
// 自定义分配策略——不常用,但可以
@RocketMQMessageListener(
    topic = "order-topic",
    consumerGroup = "order-group",
    allocateMessageQueueStrategy = AllocateMessageQueueAveragely.class
)

5.3 Rebalance 的代价:消息可能重复

Rebalance 发生时,正在被处理的 offset 可能回退——导致消息被重复消费:

实例 A 正在处理 Queue-0 的 offset=100
↓ Rebalance 发生(实例 C 上线)
Queue-0 被分配给实例 C
实例 A 停止消费 Queue-0
实例 C 从上次提交的 offset=95 开始消费
→ offset 95~100 的消息被重复消费

这就是幂等必须做的原因——Rebalance 是正常操作(扩缩容时自动触发),无法避免。

六、消费进度(Offset)管理

集群消费模式下,消费进度存在Broker 上。每次 CONSUME_SUCCESS 之后,Consumer 定期提交 offset 到 Broker。

// 手动查看消费进度
docker exec rocketmq-broker sh mqadmin consumerProgress \
    -g order-consumer-group

// 手动重置消费进度——从头消费
docker exec rocketmq-broker sh mqadmin resetOffsetByTimestamp \
    -g order-consumer-group -t order-topic -s 0
进度存储集群消费广播消费
存储位置Broker本地文件
重启不影响是(Broker 维护)否(本地文件在容器重启后丢失)
Rebalance 后自动从 Broker 读取不适用

🎯 总结

  1. 集群消费 vs 广播消费:集群消费(默认)每条消息在组内只消费一次,offset 存在 Broker;广播消费每条消息组内所有实例都收到,offset 存本地,不支持重试。

  2. Push 本质是长轮询 Pull:Consumer 主动拉取,Broker hold 住请求等消息。真正的 Push 在 RocketMQ 中不存在——这保证了 Broker 的横向扩展能力。

  3. 过滤发生在 Broker 端:Tag 过滤基于 ConsumeQueue 的 hash 字段匹配,SQL92 过滤基于消息属性匹配——不匹配的消息根本不传输到 Consumer。过滤优先级:Tag > SQL92 > Consumer 端过滤。

  4. Rebalance 自动触发,带来重复消费风险:实例增减或 Queue 数变化时发生。必须做幂等——因为正在处理的消息可能被新实例重新拉取。

📖 下一步阅读:消费端的最后一层也讲完了。所有功能原理都了解了,接下来是部署上线——集群搭建、Dashboard 监控、JVM 调优、常见故障处理。继续阅读 生产环境部署与调优