Rocketmq

问题点

1、广播消息,集群消息区别

2、广播消息是否是很少用到,比如刷新每个pod缓存

3、上面说的消费组,和消费者,与每个服务关系,如何配置的,比如物流状态变更,订单中心和wms两个服务都需要感知,两个服务配置不同的消费组吗,多个pod节点,就算多个消费者,

4、实际应用场景,配置及原理

5、消息中间件类型(这里只有 canal 消费用 kafka,其余都是 rocketmq) 怎么知道的

6、如何生产和消费消息的,代码实现

7、mq消费源码

1、广播消息,集群消息区别

一、核心概念(通俗解释)

模式 核心定义 通俗理解
集群消息(Clustering) 默认模式,同个消费组的多个消费者只会有一个收到消息,消息被消费后就从队列中移除 类似 "抢红包":多个消费者抢同一份消息,谁抢到谁处理,处理完就没了
广播消息(Broadcast) 同个消费组的所有消费者都会收到同一条消息,每个消费者都要处理一次 类似 "广播通知":所有消费者都能收到消息,各自处理,互不影响

二、核心差异对比(关键维度)

对比维度 集群消息(默认) 广播消息
投递目标 消费组内随机一个消费者 消费组内所有消费者
消息重试 支持(消费失败会重新投递) 不支持(消费失败仅记录日志,不重试)
消费位点 消费组级别(所有消费者共享进度) 消费者级别(每个消费者独立维护进度)
死信队列 支持 不支持
顺序消费 支持 不支持
适用场景 任务分发、负载均衡(如订单处理、库存扣减) 配置同步、通知推送(如全量机器更新配置)

三、代码示例(生产者 + 消费者,快速落地)

1. 生产者(无需区分模式,仅发送消息)

java

运行

复制代码
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;

public class MQProducer {
    public static void main(String[] args) throws Exception {
        // 1. 创建生产者,指定生产组
        DefaultMQProducer producer = new DefaultMQProducer("PRODUCER_GROUP");
        // 2. 设置NameServer地址
        producer.setNamesrvAddr("127.0.0.1:9876");
        // 3. 启动生产者
        producer.start();

        // 4. 发送消息(集群/广播由消费者决定,生产者无需关心)
        String topic = "TEST_TOPIC";
        String tags = "TEST_TAG";
        String body = "这是一条测试消息";
        Message message = new Message(topic, tags, body.getBytes());
        producer.send(message);

        // 5. 关闭生产者
        producer.shutdown();
    }
}
2. 消费者 - 集群模式(默认,无需额外配置)

java

运行

复制代码
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

public class ClusteringConsumer {
    public static void main(String[] args) throws Exception {
        // 1. 创建消费者,指定消费组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CONSUMER_GROUP");
        // 2. 设置NameServer地址
        consumer.setNamesrvAddr("127.0.0.1:9876");
        // 3. 订阅主题(默认集群模式,无需额外配置)
        consumer.subscribe("TEST_TOPIC", "TEST_TAG");

        // 4. 注册消息监听器
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            for (MessageExt msg : msgs) {
                System.out.println("集群模式消费消息:" + new String(msg.getBody()) + ",消费者IP:" + msg.getBornHost());
            }
            // 消费成功返回CONSUME_SUCCESS,失败返回RECONSUME_LATER(会重试)
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        // 5. 启动消费者
        consumer.start();
        System.out.println("集群模式消费者启动成功");
    }
}
3. 消费者 - 广播模式(需手动设置模式)

java

运行

复制代码
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;

public class BroadcastConsumer {
    public static void main(String[] args) throws Exception {
        // 1. 创建消费者,指定消费组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CONSUMER_GROUP");
        // 2. 设置NameServer地址
        consumer.setNamesrvAddr("127.0.0.1:9876");
        // 3. 关键:设置为广播模式
        consumer.setMessageModel(org.apache.rocketmq.common.protocol.heartbeat.MessageModel.BROADCASTING);
        // 可选:设置从最新位点开始消费(广播模式建议)
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        // 4. 订阅主题
        consumer.subscribe("TEST_TOPIC", "TEST_TAG");

        // 5. 注册监听器(广播模式消费失败不重试,需自行处理)
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            for (MessageExt msg : msgs) {
                System.out.println("广播模式消费消息:" + new String(msg.getBody()) + ",消费者IP:" + msg.getBornHost());
            }
            // 广播模式下,RECONSUME_LATER也不会重试,仅返回CONSUME_SUCCESS即可
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        // 6. 启动消费者
        consumer.start();
        System.out.println("广播模式消费者启动成功");
    }
}

四、关键避坑要点(新手必看)

  1. 广播模式不重试:消费失败后不会重新投递,若需重试,需在消费逻辑中自行实现(如本地重试、写入死信表);
  2. 消费组必须一致:只有同个消费组的消费者,才会触发集群 / 广播的投递规则;不同消费组互不影响;
  3. 广播模式不支持顺序消费:若业务要求顺序,必须用集群模式;
  4. 广播模式位点独立 :每个消费者维护自己的消费进度,重启后不会重复消费(除非设置CONSUME_FROM_FIRST_OFFSET);
  5. 集群模式负载均衡:RocketMQ 会将主题的队列平均分配给消费组内的消费者,队列数决定了最大并发消费数(如 4 个队列最多 4 个消费者同时消费)。

五、典型使用场景

模式 场景示例 原因
集群消息 订单支付回调、库存扣减、物流状态更新 只需一个消费者处理,避免重复操作,支持重试保证可靠性
广播消息 全量机器更新配置、全量服务推送通知、日志收集 所有机器都需要收到消息,无需重试(配置更新失败可下次同步)

2、广播消息是否是很少用到,比如刷新每个pod缓存

1. 广播消息的使用频率

结论:广播消息确实远不如集群消息常用(占比大概 10% 以内),但在特定场景是 "刚需",无法被集群消息替代。

2. 核心使用场景(就是你说的「刷新每个 Pod 缓存」)
典型场景 为什么用广播? 示例
全量 Pod 刷新本地缓存 每个 Pod 都有独立的本地缓存(如配置、字典数据),需要全量同步,不能只刷新一个 Pod 商品价格配置更新、风控规则更新、地区编码字典更新
全量服务推送通知 所有实例都需要感知并执行本地操作 服务优雅停机通知、全量日志级别调整、全量节点清理临时文件
无状态服务的全量数据同步 每个实例都需要拿到相同数据做本地处理 日志采集 Agent(每个 Agent 都要收集同一条系统通知)
3. 非必要不用广播的原因(也是它用得少的核心)
  • 无重试、无死信队列,可靠性比集群消息低;
  • 消息会被投递到消费组内所有消费者,增加 Broker 和网络开销;
  • 若消费逻辑耗时久,会导致所有 Pod 同时执行耗时操作,引发集群压力(如全量 Pod 同时刷新缓存可能打满数据库)。

3、上面说的消费组,和消费者,与每个服务关系,如何配置的,比如物流状态变更,订单中心和wms两个服务都需要感知,两个服务配置不同的消费组吗,多个pod节点,就算多个消费者

二、消费组、消费者、服务的关系 & 实操配置(核心重点)

先明确核心定义,再结合你的 "物流状态变更" 场景讲配置:

1. 核心概念对应关系(先理清术语)
术语 定义 和服务 / Pod 的对应关系
消费组(Consumer Group) 逻辑上的 "消费集群标识",RocketMQ 按消费组维度管理消费进度、负载均衡、投递规则 「一个服务」对应「一个消费组」(如订单中心服务→ORDER_CENTER_CONSUMER_GROUP,WMS 服务→WMS_CONSUMER_GROUP
消费者(Consumer) 消费组的具体实例(代码层面的DefaultMQPushConsumer对象) 「一个 Pod」对应「一个消费者实例」(一个服务部署 5 个 Pod = 消费组下有 5 个消费者)
服务 业务层面的微服务(如订单中心、WMS、物流中心) 不同服务必须用不同消费组;同服务多 Pod 共享同一个消费组
2. 实操场景:物流状态变更,订单中心 + WMS 都要感知
场景描述
  • 消息生产者:物流系统发送「物流状态变更」消息到LOGISTICS_STATUS_TOPIC
  • 消息消费者:订单中心需要更新订单物流状态,WMS 需要更新库存履约状态;
  • 部署形态:订单中心部署 3 个 Pod,WMS 部署 4 个 Pod。
正确配置方案(核心:不同服务用不同消费组,同服务多 Pod 用同一个消费组)
服务 消费组配置 消费者数量 消息模式 配置代码关键项
订单中心 ORDER_CENTER_LOGISTICS_GROUP(唯一) 3 个(3 个 Pod 各 1 个) 集群模式(默认) new DefaultMQPushConsumer("ORDER_CENTER_LOGISTICS_GROUP")
WMS WMS_LOGISTICS_GROUP(唯一) 4 个(4 个 Pod 各 1 个) 集群模式(默认) new DefaultMQPushConsumer("WMS_LOGISTICS_GROUP")
配置代码示例(关键部分)
① 订单中心消费者(3 个 Pod,同消费组)

java

运行

复制代码
// 订单中心每个Pod的消费者代码(所有Pod配置完全一致)
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ORDER_CENTER_LOGISTICS_GROUP");
consumer.setNamesrvAddr("127.0.0.1:9876");
// 订阅物流状态变更主题
consumer.subscribe("LOGISTICS_STATUS_TOPIC", "*");
// 集群模式(默认,无需额外配置)
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
    // 处理订单物流状态更新逻辑
    updateOrderLogisticsStatus(msgs);
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
② WMS 消费者(4 个 Pod,同消费组)

java

运行

复制代码
// WMS每个Pod的消费者代码(所有Pod配置完全一致)
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("WMS_LOGISTICS_GROUP");
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.subscribe("LOGISTICS_STATUS_TOPIC", "*");
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
    // 处理WMS库存履约状态更新逻辑
    updateWmsStockStatus(msgs);
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
最终效果(符合业务预期)
  1. 物流系统发送 1 条「物流状态变更」消息到LOGISTICS_STATUS_TOPIC
  2. 订单中心的 3 个 Pod(同消费组):只有 1 个 Pod 收到消息并处理(集群模式负载均衡),避免订单重复更新;
  3. WMS 的 4 个 Pod(同消费组):只有 1 个 Pod 收到消息并处理,避免 WMS 重复操作;
  4. 订单中心和 WMS 属于不同消费组,互不干扰,都能感知到这条消息。
3. 关键配置原则(避坑核心)
原则 实操要求 反例(错误配置)
不同服务→不同消费组 订单中心和 WMS 必须用不同的消费组名称 两个服务都用LOGISTICS_CONSUMER_GROUP → 会导致订单中心的 Pod 和 WMS 的 Pod 抢消息,业务逻辑混乱
同服务多 Pod→同一消费组 订单中心的 3 个 Pod 都用ORDER_CENTER_LOGISTICS_GROUP 订单中心每个 Pod 用不同消费组 → 每个 Pod 都会收到消息(等同于广播),导致订单重复更新
消费组名称全局唯一 消费组名称建议带服务前缀(如ORDER_CENTER_XXX 不同业务用相同消费组名 → 消费进度混乱,消息重复 / 丢失
4. 补充:如果需要 "同服务所有 Pod 都感知消息"(如订单中心全量 Pod 刷新缓存)

此时订单中心的消费组需要设置为广播模式,配置如下:

java

运行

复制代码
// 订单中心广播模式配置(所有Pod都收到消息)
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ORDER_CENTER_CACHE_GROUP");
consumer.setNamesrvAddr("127.0.0.1:9876");
// 关键:设置为广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.subscribe("ORDER_CACHE_REFRESH_TOPIC", "*");
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
    // 每个Pod都执行缓存刷新
    refreshLocalCache(msgs);
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();

总结

  1. 广播消息的使用:确实用得少,但刷新 Pod 缓存、全量配置同步等场景是刚需,不可替代;
  2. 消费组配置核心
    • 不同服务→不同消费组(如订单中心和 WMS),确保各自独立消费,互不干扰;
    • 同服务多 Pod→同一消费组,集群模式下实现负载均衡(仅一个 Pod 消费),广播模式下全量 Pod 消费;
  3. 物流状态变更场景:订单中心和 WMS 配置不同消费组,各自的多 Pod 共享自身消费组,用默认集群模式即可。

4、实际应用场景,配置及原理

结合你提供的服务 1(商户服务)服务 2(WMS 服务) 的 RocketMQ 配置,我会从「DeliveryStatusChangeMessage 消息的消费链路、核心配置含义、消费规则」三个维度拆解,让你清晰看到这条消息是如何被两个服务消费的:

一、先定位核心配置(DeliveryStatusChangeMessage 相关)

1. 服务 1(商户服务)的配置

yaml

复制代码
# 1. 函数定义(消费入口):包含merchantDeliveryStatusChangeMessage
cloud:
  function:
    definition: ...;merchantDeliveryStatusChangeMessage;...
  stream:
    # 2. RocketMQ消费者过滤配置
    rocketmq:
      bindings:
        merchantDeliveryStatusChangeMessage-in-0:
          consumer:
            tags: DeliveryStatusChangeMessage # 只消费tag为该值的消息
    # 3. Topic和消费组配置
    bindings:
      merchantDeliveryStatusChangeMessage-in-0:
        destination: ${rocketmq.custom.namespace}%TOPIC-EXPRESS-PUSH # 订阅的Topic
        group: ${rocketmq.custom.namespace}%CID-MERCHANT-DELIVERY-STATUS-CHANGE-MESSAGE # 消费组名称
2. 服务 2(WMS 服务)的配置

yaml

复制代码
# 1. 函数定义(消费入口):包含deliveryStatusChangeMessage
cloud:
  function:
    definition: ...;deliveryStatusChangeMessage;...
  stream:
    # 2. RocketMQ消费者过滤配置
    rocketmq:
      bindings:
        deliveryStatusChangeMessage-in-0:
          consumer:
            # broadcasting: true # 广播模式默认关闭(集群模式)
            tags: DeliveryStatusChangeMessage # 只消费tag为该值的消息
    # 3. Topic和消费组配置
    bindings:
      deliveryStatusChangeMessage-in-0:
        destination: ${rocketmq.custom.namespace}%TOPIC-EXPRESS-PUSH # 订阅同一个Topic
        group: ${rocketmq.custom.namespace}%CID-WMS-DELIVERY_STATUS_CHANGE # 不同的消费组

二、DeliveryStatusChangeMessage 消费链路完整分析

1. 核心消费规则(最关键)
维度 配置值 实际消费行为
订阅 Topic 两个服务都订阅 TOPIC-EXPRESS-PUSH 生产者发送到该 Topic 的 DeliveryStatusChangeMessage,两个服务都能感知
消费组 服务 1:CID-MERCHANT-xxx服务 2:CID-WMS-xxx 两个服务属于不同消费组,互不干扰,各自独立消费这条消息
消息过滤 都配置 tags: DeliveryStatusChangeMessage 只消费该 Topic 下 tag 为 DeliveryStatusChangeMessage 的消息(过滤其他 tag)
消费模式 都默认关闭 broadcasting(集群模式) 每个服务的多 Pod 中,仅一个 Pod 消费这条消息(负载均衡)
2. 完整消费流程(举例:服务 1 部署 3 个 Pod,服务 2 部署 4 个 Pod)
  1. 生产者发送一条「配送状态变更」消息:
    • Topic:{命名空间}%TOPIC-EXPRESS-PUSH
    • Tag:DeliveryStatusChangeMessage
    • 内容:比如 "订单 123 的配送状态从「待配送」变为「已送达」";
  2. RocketMQ Broker 收到消息后,分发给两个消费组:
    • 服务 1 消费组(CID-MERCHANT-xxx):3 个 Pod 中随机 1 个 Pod 抢到消息,执行「商户侧配送状态更新」逻辑;
    • 服务 2 消费组(CID-WMS-xxx):4 个 Pod 中随机 1 个 Pod 抢到消息,执行「WMS 侧库存 / 履约状态更新」逻辑;
  3. 两个服务的消费进度独立维护,一个服务消费失败 / 重试,不影响另一个服务。

三、所有核心配置项含义(逐字段拆解)

1. 顶层核心配置
配置项 示例值 含义
cloud.function.definition merchantDeliveryStatusChangeMessage 定义 Spring Cloud Stream 的消费函数入口(对应代码中@Bean定义的函数名,是消息消费的业务逻辑入口)
cloud.stream.dynamicDestinationCacheSize 20(仅服务 1) 动态目的地缓存大小(优化性能,对消费逻辑无影响)
2. RocketMQ 专属配置(rocketmq.bindings.xxx
配置项 示例值 含义
consumer.tags DeliveryStatusChangeMessage 消息过滤:只消费对应 Topic 下该 tag 的消息(多个 tag 用 ` 分隔,如tag1 tag2`)
consumer.broadcasting true/false(默认 false) 消费模式:false = 集群模式(多 Pod 抢消息)true = 广播模式(多 Pod 都消费)
consumer.sql (服务 2 注释掉的配置) 更复杂的过滤规则(如TAGS = 'xxx' AND a > 10),优先级高于 tags
3. 绑定配置(bindings.xxx
配置项 示例值 含义
destination ${rocketmq.custom.namespace}%TOPIC-EXPRESS-PUSH 订阅的 Topic 名称(%是命名空间分隔符,避免不同环境 Topic 重名)
group ${rocketmq.custom.namespace}%CID-MERCHANT-xxx 消费组名称(全局唯一,RocketMQ 按消费组维护消费进度)
binder kafka(服务 1 的 canalProductConsumer) 消息中间件类型(这里只有 canal 消费用 kafka,其余都是 rocketmq)

四、关键配置亮点 & 避坑点(结合实际生产)

1. 配置亮点(符合最佳实践)
  • 消费组命名规范:消费组带服务前缀(MERCHANT/WMS),全局唯一,避免冲突;
  • Tag 过滤精准:每个消费函数只订阅自身需要的 tag,减少无效消息推送;
  • 不同服务不同消费组:订单中心和 WMS 独立消费,互不干扰,保证业务隔离;
  • 默认集群模式:配送状态变更只需一个 Pod 处理,避免重复更新业务数据。
2. 潜在避坑点(需注意)
  • 若误将两个服务配置为同一消费组:服务 1 的 Pod 和服务 2 的 Pod 会抢消息,导致一方处理不到消息,业务逻辑混乱;
  • 若开启broadcasting: true:服务的所有 Pod 都会处理配送状态变更,可能导致重复更新订单 / 库存;
  • Topic 命名中的%是自定义分隔符:需确保生产者发送消息时 Topic 名称格式一致,否则消费不到。

总结

  1. DeliveryStatusChangeMessage 消费核心
    • 两个服务订阅同一个 Topic(TOPIC-EXPRESS-PUSH)、过滤同一个 Tag,但使用不同消费组,实现 "各自独立、负载均衡" 的消费;
    • 服务 1(商户)处理商户侧配送状态更新,服务 2(WMS)处理仓储履约状态更新,业务隔离且都能感知消息。
  2. 核心配置含义
    • function.definition:消费业务逻辑入口;
    • tags:消息过滤规则;
    • destination:订阅的 Topic;
    • group:消费组(不同服务不同组,同服务多 Pod 同组);
    • broadcasting:集群 / 广播模式开关(默认集群)。

5、消息中间件类型(这里只有 canal 消费用 kafka,其余都是 rocketmq) 怎么知道的

我直接从你自己贴的配置里圈出来给你看:


1)只有这个消费者显式写了 binder: kafka

服务 1 配置里的 canalProductConsumer

yaml

复制代码
canalProductConsumer-in-0:
  destination: xswc_merchant_pro
  group: CID-MERCHANT-PRODUCT-BINLOG-PROD
  binder: kafka   # 👈 就是这里!
  • binder: kafka 意思:这个消费者用 Kafka 中间件,不用默认的 RocketMQ。

2)其他所有消费者 都没有写 binder

比如你看:

yaml

复制代码
logisticsMessage-in-0:
  destination: ${rocketmq.custom.namespace}%TOPIC-EXPRESS-PUSH
  group: ${rocketmq.custom.namespace}%CID-MERCHANT-LOGISTICS-MESSAGE-CALLBACK
  # 没有 binder: kafka

yaml

复制代码
deliveryStatusChangeMessage-in-0:
  destination: ${rocketmq.custom.namespace}%TOPIC-EXPRESS-PUSH
  group: ${rocketmq.custom.namespace}%CID-WMS-DELIVERY_STATUS_CHANGE
  # 没有 binder: kafka

规则:

  • 不写 binder → 默认使用全局默认消息中间件(你项目里是 RocketMQ)
  • 写了 binder: kafka → 强制走 Kafka

3)你们项目的默认规则(行业通用)

你们项目的 application.yml 里一定有类似这样的全局默认配置

yaml

复制代码
spring:
  cloud:
    stream:
      defaultBinder: rocketmq   # 👈 默认用RocketMQ

所以:

  • 99% 消费者 → 默认 RocketMQ
  • 只有显式写 binder: kafka → 才走 Kafka

6、如何生产和消费消息的,代码实现

结合你提供的服务 2(WMS)服务 1(商户) 的消费代码,我会从「代码逻辑、消费规则、核心设计思路」三个维度拆解,重点解释代码中 "广播消费" 注释的含义、Redis 缓存锁的作用,以及和之前配置的对应关系:

一、代码核心逻辑拆解(逐行解析)

1. 通用结构(两个消费函数一致)

java

运行

复制代码
// 1. 定义Spring Cloud Stream消费函数(对应配置里的function.definition)
@Bean("deliveryStatusChangeMessage") // 服务2的函数名
@Bean("merchantDeliveryStatusChangeMessage") // 服务1的函数名
public Consumer<Message<DeliveryStatusChangeMessage>> 函数名() {
    return message -> {
        // 2. 提取消息体(DeliveryStatusChangeMessage是物流状态变更的业务DTO)
        DeliveryStatusChangeMessage event = message.getPayload();
        
        // 3. 生成Redis缓存Key(核心:防重复消费)
        String cacheKey = RedisKeyUtil.generate(前缀, event.keys());
        
        // 4. 核心处理逻辑(通过rocketMQBuilder.process封装)
        rocketMQBuilder.process(cacheKey, message, msg -> {
            // 日志打印
            log.info("物流状态变更消息 msg={}", JSONUtil.toJsonStr(msg));
            // 业务处理(各自调用自己的BizService)
            业务服务.deliveryStatusChange(msg);
        });
    };
}
2. 关键代码逐行解释
代码片段 核心作用 细节说明
@Bean("xxxMessage") 消费函数注册 对应配置中cloud.function.definition里的函数名,是 Spring Cloud Stream 的消费入口,Broker 推送消息后会调用这个 Bean
Message<DeliveryStatusChangeMessage> 消息载体 Spring Cloud Stream 封装的消息对象,getPayload()获取实际的业务消息体(物流状态变更 DTO)
RedisKeyUtil.generate(..., event.keys()) 生成唯一缓存 Key 基于消息的业务主键(如订单号、物流单号)生成 Redis Key,用于幂等控制(避免重复消费)
rocketMQBuilder.process(cacheKey, message, 业务逻辑) 封装消费逻辑 核心封装了「Redis 分布式锁 + 消费逻辑 + 异常处理」,是项目自定义的消费工具类
fulfillOrderBizService.deliveryStatusChange(msg)``orderFulfillBizService.deliveryStatusChange(msg) 业务处理 各自调用服务内的业务逻辑:服务 2(WMS):更新仓储履约状态服务 1(商户):更新商户侧订单物流状态

二、核心设计思路(重点解释注释和 Redis 的作用)

1. 注释 "因为是广播消费,避免影响别的消费组" 的真实含义

⚠️ 关键纠正 :代码注释写的 "广播消费"≠ RocketMQ 的广播模式(broadcasting: true),而是业务层面的 "防干扰" 设计:

  • 从之前的配置看,两个服务都是集群模式(broadcasting 默认 false),并非真正的广播消费;
  • 注释的核心意图:不同消费组(商户 / WMS)独立处理消息,通过 Redis 锁避免自身消费组内重复处理,同时不影响其他消费组
2. Redis 缓存 Key 的核心作用(幂等控制)

java

运行

复制代码
String cacheKey = RedisKeyUtil.generate(前缀, event.keys());
  • 为什么需要 :即使是集群模式,也可能因网络抖动、Broker 重试等原因导致同一条消息被多次推送给消费组
  • 实现逻辑
    1. rocketMQBuilder.process内部会先尝试获取 Redis 锁(基于 cacheKey);
    2. 获取锁成功 → 执行业务逻辑,执行完成后释放锁(或设置过期时间);
    3. 获取锁失败 → 说明该消息已被消费组内其他 Pod 处理过,直接跳过,避免重复执行业务;
  • 前缀区分
    • 服务 1(商户):MerchantRedisKey.ROOT_KEY → 商户侧独立的 Redis Key 前缀,和 WMS 不冲突;
    • 服务 2(WMS):RedisKeyConsts.ROOT → WMS 侧前缀;✅ 效果:两个消费组的 Redis 锁互不干扰,各自保证自身的幂等性。
3. rocketMQBuilder.process的封装逻辑(推测)

结合代码和注释,这个工具类大概率封装了以下核心逻辑(伪代码):

java

运行

复制代码
public void process(String cacheKey, Message<?> message, Consumer<?> consumer) {
    // 1. 获取Redis分布式锁(设置过期时间,避免死锁)
    boolean lock = redisTemplate.opsForValue().setIfAbsent(cacheKey, "LOCK", 5, TimeUnit.MINUTES);
    if (!lock) {
        log.warn("消息已处理,cacheKey={}", cacheKey);
        return;
    }
    try {
        // 2. 执行业务逻辑
        consumer.accept(message.getPayload());
    } catch (Exception e) {
        // 3. 异常处理(日志+可选重试)
        log.error("消息处理失败", e);
        throw e; // 抛出异常让Stream重试(集群模式下会重新推送)
    } finally {
        // 4. 释放锁(可选,也可依赖过期时间)
        redisTemplate.delete(cacheKey);
    }
}

三、代码和配置的对应关系(关键闭环)

配置项 代码中的体现 实际效果
function.definition中的函数名 @Bean("deliveryStatusChangeMessage") 配置和代码的消费入口一一对应,Broker 推送消息后调用该 Bean
tags: DeliveryStatusChangeMessage 代码中Message<DeliveryStatusChangeMessage> 只有 Tag 匹配的消息才会进入该函数,类型安全
不同消费组 代码中不同的 Redis 前缀 + 不同的 BizService 两个消费组独立处理消息,业务逻辑隔离
集群模式(默认) Redis 锁控制消费组内仅一次处理 消费组内多个 Pod 抢锁,最终只有一个 Pod 执行业务逻辑

四、核心亮点 & 潜在风险

1. 设计亮点(符合生产级规范)
  • 幂等性保障:通过 Redis 分布式锁避免重复消费,解决集群模式下的消息重试问题;
  • 业务隔离:两个服务各自封装业务逻辑(fulfillOrderBizService/orderFulfillBizService),代码解耦;
  • 日志完整:打印消息内容和时间,便于问题排查;
  • 缓存 Key 区分前缀:避免不同消费组的 Redis 锁冲突。
2. 潜在风险(需要注意)
  • Redis 锁过期时间:若业务逻辑执行时间超过锁过期时间,可能导致重复消费(需确保过期时间 > 业务最大耗时);
  • 异常处理 :代码中未显式捕获异常,若deliveryStatusChange抛异常,会导致 Stream 重试(集群模式下会推送给消费组内其他 Pod);
  • 注释误导:"广播消费" 的注释和实际配置(集群模式)不符,易造成后续维护误解(建议修改注释为 "集群消费,Redis 锁防重复处理")。

总结

  1. 核心逻辑
    • 两个服务通过不同的消费函数接收同一类消息,各自生成独立的 Redis 锁 Key,保证消费组内幂等处理;
    • 代码中的 "广播消费" 是注释误解,实际是集群模式 + Redis 锁实现 "消费组内仅一次处理",同时不影响其他消费组;
  2. 关键设计
    • Redis 锁是核心:解决集群模式下的重复消费问题;
    • 业务隔离:不同服务调用各自的 BizService,处理自身业务逻辑;
  3. 配置 & 代码闭环
    • 配置中的函数名对应代码中的 @Bean 名称,Tag 过滤对应消息类型,消费组对应 Redis 前缀,形成完整的消费链路。

7、mq消费源码

java 复制代码
 public <T extends BaseEvent> void process(final String key, final Message<T> message, final Consumer<T> function) {
        String cacheKey = RedisKeyUtil.generate(REDIS_REPEAT_PREFIX_KEY, key);
        KeyInfo keyInfo = KeyInfo.builder()
            .prefix(REDIS_REPEAT_PREFIX_KEY)
            .keys(new String[]{key})
            .waitTime(3)
            .timeUnit(TimeUnit.SECONDS)
            .build();
        // 获取消息体
        T event = message.getPayload();
        try {
            // 获取链路追踪id
            String traceId = getHeaderValue(message, RocketMQConsts.Header.TRACE_ID);
            MDC.put(Constants.MDC_TRACE_ID, StringUtils.isBlank(traceId) ? key : traceId);
            // 设置调度tag环境
            String env = getHeaderValue(message, RocketMQConsts.Header.ENV);
            RequestThread.addParam(Constants.ENV, env);
            //对相同key的消息加锁,保证相同的消息是串行消费
            lockService.lock(keyInfo, () -> {
                //判断该消息是否已处理
                if (redisCache.exists(cacheKey)) {
                    log.info("[MQ消息-已处理]--[{}]:{}", key, ReflectUtils.processObject(event));
                    return null;
                }
                log.info("[MQ消息-开始处理]--[{}]:{}", key, ReflectUtils.processObject(event));
                function.accept(event);
                log.info("[MQ消息-处理完成]--{}", key);
                //缓存处理结果一小时,一个小时内不重复消费
                redisCache.setCacheObject(cacheKey, DateUtil.now(), 60, TimeUnit.MINUTES);
                return null;
            });
        } catch (LockException e) {
            //加锁失败,说明有线程正在消费
            log.warn("[MQ消息-正在处理]--[{}]:{}", key, ReflectUtils.processObject(event));
            throw e;
        } catch (Exception e) {
            log.error("[MQ消息-处理异常]--[{}]:{}", key, event, e);
            throw e;
        } finally {
            MDC.clear();
            RequestThread.clear();
        }
    }

这份 process 方法是生产级的 MQ 消息消费核心工具类 ,封装了「分布式锁、幂等控制、链路追踪、异常处理」等关键能力,我会从执行流程、核心设计、关键细节、避坑点四个维度拆解,让你彻底理解每一行的作用:

一、核心执行流程(按代码顺序)

我用流程图先梳理整体逻辑,再逐段解析:

复制代码
graph TD
    A[入口:接收缓存Key+消息+业务函数] --> B[生成最终Redis防重Key]
    B --> C[构建分布式锁参数(KeyInfo)]
    C --> D[提取消息体+链路追踪ID+环境变量]
    D --> E[加分布式锁(串行消费相同Key消息)]
    E --> F{加锁成功?}
    F -- 失败 --> G[抛LockException,记录"正在处理"日志]
    F -- 成功 --> H{Redis中是否存在防重Key?}
    H -- 存在 --> I[记录"已处理"日志,直接返回(幂等)]
    H -- 不存在 --> J[执行业务消费逻辑(function.accept)]
    J --> K[记录"处理完成"日志,缓存防重Key(1小时)]
    J --> L[业务异常?]
    L -- 是 --> M[记录异常日志,抛异常(触发MQ重试)]
    L -- 否 --> K
    G --> N[finally:清理MDC/ThreadLocal]
    I --> N
    K --> N
    M --> N

二、逐段解析核心代码逻辑

1. 入参定义(泛型约束 + 核心入参)

java

运行

复制代码
public <T extends BaseEvent> void process(final String key, final Message<T> message, final Consumer<T> function)
  • <T extends BaseEvent>:泛型约束,确保消息体是业务事件基类(统一规范所有 MQ 消息体);
  • key:业务唯一标识(如订单号 / 物流单号,用于生成防重 Key 和锁 Key);
  • message:Spring Cloud Stream 封装的 MQ 消息(含消息体 + Header);
  • function:业务消费逻辑(Lambda 表达式,解耦工具类和业务代码)。
2. 防重 Key + 锁参数构建

java

运行

复制代码
// 生成最终的Redis防重Key(前缀+业务Key,避免不同业务Key冲突)
String cacheKey = RedisKeyUtil.generate(REDIS_REPEAT_PREFIX_KEY, key);
// 构建分布式锁参数:前缀、Key、等待时间3秒、时间单位秒
KeyInfo keyInfo = KeyInfo.builder()
    .prefix(REDIS_REPEAT_PREFIX_KEY)
    .keys(new String[]{key})
    .waitTime(3)
    .timeUnit(TimeUnit.SECONDS)
    .build();
  • REDIS_REPEAT_PREFIX_KEY:全局防重前缀(如mq:repeat:),避免和其他 Redis Key 冲突;
  • KeyInfo:分布式锁的参数封装,waitTime=3秒表示 "抢锁最多等 3 秒,超时抛 LockException"。
3. 上下文初始化(链路追踪 + 环境变量)

java

运行

复制代码
// 提取链路追踪ID(从MQ Header中获取),放入MDC(日志打印用)
String traceId = getHeaderValue(message, RocketMQConsts.Header.TRACE_ID);
MDC.put(Constants.MDC_TRACE_ID, StringUtils.isBlank(traceId) ? key : traceId);
// 提取环境变量(如dev/test/prod),放入ThreadLocal(业务逻辑可能用到)
String env = getHeaderValue(message, RocketMQConsts.Header.ENV);
RequestThread.addParam(Constants.ENV, env);
  • 链路追踪:MDC 放入 traceId 后,该线程的所有日志都会打印 traceId,便于排查跨服务问题;
  • 环境隔离:从 MQ Header 取 env,避免不同环境的消息串用配置(如 dev 消息不处理 prod 数据)。
4. 分布式锁 + 幂等控制(核心中的核心)

java

运行

复制代码
lockService.lock(keyInfo, () -> {
    // 幂等校验:判断该消息是否已处理(防重复消费)
    if (redisCache.exists(cacheKey)) {
        log.info("[MQ消息-已处理]--[{}]:{}", key, ReflectUtils.processObject(event));
        return null;
    }
    // 执行业务消费逻辑
    log.info("[MQ消息-开始处理]--[{}]:{}", key, ReflectUtils.processObject(event));
    function.accept(event);
    log.info("[MQ消息-处理完成]--{}", key);
    // 缓存防重Key,1小时内不重复消费(过期自动清理)
    redisCache.setCacheObject(cacheKey, DateUtil.now(), 60, TimeUnit.MINUTES);
    return null;
});
  • lockService.lock :分布式锁核心逻辑(大概率是 Redis 的 SET NX EX 实现),作用是:✅ 相同业务 Key 的消息串行消费(避免并发处理导致数据不一致);✅ 消费组内多个 Pod 抢锁,最终只有一个 Pod 能执行逻辑(集群模式下的负载均衡 + 幂等);
  • redisCache.exists(cacheKey):最终的幂等兜底 ------ 即使锁失效,也能通过防重 Key 判断消息是否已处理;
  • 缓存 1 小时:足够覆盖 MQ 的重试周期(一般 MQ 重试间隔不会超过 1 小时),过期自动清理避免 RedisKey 堆积。
5. 异常处理(分场景)

java

运行

复制代码
// 场景1:加锁失败(其他Pod正在处理该消息)
catch (LockException e) {
    log.warn("[MQ消息-正在处理]--[{}]:{}", key, ReflectUtils.processObject(event));
    throw e; // 抛异常触发MQ重试(集群模式下推给其他Pod)
}
// 场景2:业务逻辑异常(消费失败)
catch (Exception e) {
    log.error("[MQ消息-处理异常]--[{}]:{}", key, event, e);
    throw e; // 抛异常触发MQ重试
}
// 场景3:最终清理(无论成功/失败都执行)
finally {
    MDC.clear(); // 清理MDC,避免线程复用导致traceId串用
    RequestThread.clear(); // 清理ThreadLocal,避免内存泄漏
}
  • LockException:抢锁失败不代表消费失败,只是 "正在处理",抛异常让 MQ 重试(其他 Pod 可能抢到锁);
  • 业务异常:消费逻辑出错(如数据库异常),抛异常触发 MQ 重试,保证消息最终能消费成功;
  • finally 清理:MDC/ThreadLocal 都是线程级别的,必须清理,否则线程池复用会导致数据串用。

三、核心设计亮点(生产级规范)

亮点 解决的问题
泛型约束 <T extends BaseEvent> 统一所有 MQ 消息体的规范,避免类型混乱
链路追踪(MDC+traceId) 跨服务排查问题时,可通过 traceId 串联所有日志
分布式锁 + 双重幂等(锁 + 防重 Key) 既保证相同 Key 消息串行消费,又防重复消费
环境变量隔离(ThreadLocal+env) 避免不同环境的消息串用配置 / 数据
异常分层处理(LockException / 通用 Exception) 区分 "正在处理" 和 "处理失败",重试逻辑更精准
finally 清理上下文 避免线程池复用导致的 MDC/ThreadLocal 泄漏

四、潜在风险 & 优化建议(避坑关键)

1. 风险点
  • 锁等待时间过短waitTime=3秒,若业务高峰期锁竞争激烈,可能频繁抛 LockException,导致 MQ 重试次数过多;
  • 防重 Key 过期时间固定:1 小时过期,若 MQ 重试周期超过 1 小时(如配置了 2 小时重试),可能导致重复消费;
  • 未处理锁超时:若锁的过期时间 <业务执行时间,锁失效后其他 Pod 会重新处理消息(需确保锁过期时间> 业务最大耗时);
  • 无死信机制:业务异常会无限重试,若消息本身有问题(如数据错误),会导致重复报错(需加重试次数限制,超过则入死信)。
2. 优化建议

java

运行

复制代码
// 1. 锁参数配置化(避免硬编码)
@Value("${mq.lock.wait-time:3}")
private long lockWaitTime;
@Value("${mq.repeat.expire-minutes:60}")
private long repeatExpireMinutes;

// 2. 锁过期时间动态计算(基于业务最大耗时)
KeyInfo keyInfo = KeyInfo.builder()
    .prefix(REDIS_REPEAT_PREFIX_KEY)
    .keys(new String[]{key})
    .waitTime(lockWaitTime)
    .leaseTime(businessMaxTime * 2) // 锁过期时间=业务最大耗时*2
    .timeUnit(TimeUnit.SECONDS)
    .build();

// 3. 加重试次数限制(避免无限重试)
int retryCount = Integer.parseInt(getHeaderValue(message, RocketMQConsts.Header.RETRY_COUNT, "0"));
if (retryCount > 3) {
    log.error("[MQ消息-重试超限]--[{}], 入死信", key);
    // 发送到死信队列
    deadLetterProducer.send(event);
    return;
}

总结

  1. 核心逻辑 :该方法是 MQ 消费的 "通用骨架"------ 通过分布式锁保证串行消费防重 Key 保证幂等链路追踪保证可观测异常处理保证可靠性,业务逻辑只需传入 Lambda 表达式即可,解耦性极强;
  2. 关键设计
    • 锁 + 防重 Key 的 "双重幂等" 是核心,解决了集群模式下的重复消费 / 并发问题;
    • 上下文(MDC/ThreadLocal)的初始化和清理,保证了日志和环境的隔离;
  3. 使用场景 :适用于所有需要 "串行消费、幂等处理、可追溯" 的 MQ 消费场景(如物流状态变更、订单状态更新),也是你之前看到的两个服务消费DeliveryStatusChangeMessage的底层保障。

如果需要基于这个工具类,优化你之前的物流状态变更消费代码(如加死信、配置化参数),我可以给出完整的修改示例。

相关推荐
无心水2 小时前
【OpenClaw:认知启蒙】3、OpenClaw Agent执行循环(Lobster)深度拆解:从意图识别到工具调用的全流程
java·人工智能·系统架构·自动化
天空属于哈夫克32 小时前
Java 企微机器人开发:高性能、可扩展的私域自动化集成方案
java·机器人·企业微信
golang学习记2 小时前
IDEA 2026.1 EAP 4 发布:git操作更加丝滑了!
java·git·intellij-idea
一知半解仙2 小时前
AI加持下的IDEA让Java开发更高效
java·人工智能·intellij-idea
XiYang-DING2 小时前
【Java SE】Java访问修饰符总结
java·开发语言
拾贰_C2 小时前
【idea | knife4j | springboot2/3|接上篇|终篇】knife4j版本号与spring boot版本不兼容问题(细节问题)
java·spring boot·intellij-idea
甲枫叶2 小时前
【openclaw】我用 OpenClaw 自动化了这些工作
java·python·自动化·ai编程
6+h2 小时前
【Spring】Mapper层常用注解详解
java·后端·spring
予枫的编程笔记2 小时前
【面试专栏|Java并发编程】Thread类核心方法全解:start和run的区别
java·thread·java多线程·java面试·start方法·wait notify·sleep方法