目录
[1. Topic/Group/Tag 命名与设计规范(落地级)](#1. Topic/Group/Tag 命名与设计规范(落地级))
[1.1 Topic 命名规范(强制)](#1.1 Topic 命名规范(强制))
[1.2 Group 命名规范(强制)](#1.2 Group 命名规范(强制))
[1.3 Tag 设计规范(核心)](#1.3 Tag 设计规范(核心))
[1.4 队列(Queue)规划](#1.4 队列(Queue)规划)
[2. 架构分层(结合无人售货柜场景)](#2. 架构分层(结合无人售货柜场景))
[1. 全局配置(application.yml,Stream 方式)](#1. 全局配置(application.yml,Stream 方式))
[2. 关键参数取值依据(必看)](#2. 关键参数取值依据(必看))
[3. 延迟级别映射表(重要)](#3. 延迟级别映射表(重要))
[三、消息生产:确保 "发得出、发得对、可追溯"](#三、消息生产:确保 “发得出、发得对、可追溯”)
[1. 消息体设计规范(落地级)](#1. 消息体设计规范(落地级))
[2. 发送策略选择(按场景选型)](#2. 发送策略选择(按场景选型))
[3. 关键优化点](#3. 关键优化点)
[3.1 消息 Key 设置(强制)](#3.1 消息 Key 设置(强制))
[3.2 批量发送优化(高频场景必用)](#3.2 批量发送优化(高频场景必用))
[3.3 发送失败处理(核心)](#3.3 发送失败处理(核心))
[四、消息消费:确保 "收得到、处理对、不重复"](#四、消息消费:确保 “收得到、处理对、不重复”)
[1. 消费模式选择](#1. 消费模式选择)
[2. 幂等性保障(强制)](#2. 幂等性保障(强制))
[2.1 幂等方案选型(按场景)](#2.1 幂等方案选型(按场景))
[2.2 代码示例(Redis + 数据库双保障)](#2.2 代码示例(Redis + 数据库双保障))
[3. 消费失败处理(核心)](#3. 消费失败处理(核心))
[3.1 重试策略(非瞬时故障延迟重试)](#3.1 重试策略(非瞬时故障延迟重试))
[3.2 死信队列处理(必配)](#3.2 死信队列处理(必配))
[4. 顺序消费(特殊场景)](#4. 顺序消费(特殊场景))
[1. 事务消息流程(核心)](#1. 事务消息流程(核心))
[2. 代码实现(落地级)](#2. 代码实现(落地级))
[2.1 事务生产者](#2.1 事务生产者)
[2.2 事务消息消费者(订单服务)](#2.2 事务消息消费者(订单服务))
[六、性能优化:从代码到 Broker 的全链路调优](#六、性能优化:从代码到 Broker 的全链路调优)
[1. 生产者优化](#1. 生产者优化)
[2. 消费者优化](#2. 消费者优化)
[3. Broker 优化(运维层面)](#3. Broker 优化(运维层面))
[1. 核心监控指标(必看)](#1. 核心监控指标(必看))
[2. 监控工具选型](#2. 监控工具选型)
[3. 消息轨迹(必开)](#3. 消息轨迹(必开))
本文从架构设计、配置调优、生产消费、可靠性保障、性能优化、运维监控、问题排查7 个维度深度展开 RocketMQ 在 Spring Cloud Alibaba 生态中的最佳实践,结合无人售货柜等实际业务场景,给出可落地的规范、代码示例和调优依据。
一、架构设计:从源头规避核心问题
架构设计是基础,不合理的设计会导致后期运维成本指数级上升,核心围绕 "业务隔离、可扩展、易运维" 展开。
1. Topic/Group/Tag 命名与设计规范(落地级)
核心原则
- 语义化:通过名称直接识别业务域、环境、功能;
- 唯一性:避免不同业务复用 Topic/Group 导致消息串流;
- 可扩展:预留业务扩展空间,避免过度拆分 / 合并。
1.1 Topic 命名规范(强制)
| 层级 | 格式 | 说明 | 示例 |
|---|---|---|---|
| 业务域 | 小写英文,短横线分隔 | 核心业务模块(如订单、支付、柜机) | order、pay、cabinet |
| 功能模块 | 小写英文 | 业务域下的子模块 | create、status、command |
| 环境标识(可选) | dev/test/prod | 多环境隔离(无共享 Broker 时可省略) | prod |
| 类型后缀(可选) | event/command/data | 区分消息类型(事件 / 指令 / 数据) | event |
最终格式 :{业务域}-{功能模块}-{环境标识}-{类型后缀}(环境和类型可按需裁剪)
- 正确示例:
order-create-prod-event(生产环境订单创建事件)、cabinet-command-dev(测试环境柜机指令); - 错误示例:
topic1(无语义)、order_pay_cabinet(多业务混合)。
1.2 Group 命名规范(强制)
Group 分为生产者组和消费者组,核心绑定 "服务实例 + 功能 + 环境":
| 类型 | 格式 | 示例 |
|---|---|---|
| 生产者组 | {服务名}-producer-{环境}-group |
order-service-producer-prod-group |
| 消费者组 | {服务名}-consumer-{功能}-{环境}-group |
cabinet-service-consumer-command-prod-group |
关键禁忌:
- 禁止多个服务共用一个消费者组:会导致消息被随机分配到不同服务实例,业务逻辑混乱;
- 禁止消费者组与生产者组同名:易造成监控混淆,且不符合 RocketMQ 设计逻辑。
1.3 Tag 设计规范(核心)
Tag 是 Topic 内的 "二级分类",用于消息过滤,设计需满足:
- 按业务事件拆分 :一个 Tag 对应一类原子事件,例如:
- Topic:
cabinet-status-prod,Tag:heartbeat(心跳)、stock(库存)、fault(故障); - Topic:
order-create-prod,Tag:success(创建成功)、fail(创建失败)。
- Topic:
- 数量控制:单个 Topic 的 Tag 数≤20 个,过多会导致 Broker 过滤性能下降;
- 语义统一:同一 Topic 内的 Tag 语义维度一致(如都表示 "事件结果" 或 "设备状态")。
1.4 队列(Queue)规划
Queue 是 RocketMQ 的并行消费单元,规划需结合消费能力:
- 单个 Topic 的 Queue 数 = 消费者实例数 × 单实例消费线程数 × 1.2(预留 20% 扩展);
- 例如:柜机状态消费服务部署 4 实例,单实例 10 个消费线程,Queue 数 = 4×10×1.2=48;
- 顺序消息场景:需保证 "同一业务 ID(如订单号)的消息进入同一 Queue",此时 Queue 数可等于消费者实例数(避免多实例竞争同一 Queue)。
2. 架构分层(结合无人售货柜场景)
plaintext
安卓柜机(MQTT) → 柜机服务(MQTT客户端 + RocketMQ生产者) → RocketMQ集群 → 微服务集群(消费者)
↓
微服务(生产者) → RocketMQ集群 → 柜机服务(RocketMQ消费者 + MQTT客户端) → 安卓柜机(MQTT)
分层职责(强制)
- 设备接入层(柜机服务) :
- 唯一负责 MQTT 与 RocketMQ 的协议转换;
- 对设备消息做前置校验(如设备 ID 合法性、消息格式);
- 缓存离线设备指令(避免 RocketMQ 消息丢失)。
- 消息中转层(RocketMQ) :
- 微服务间仅通过 RocketMQ 异步通信,禁止直连;
- 核心业务(支付、订单)使用事务消息,非核心(日志、状态)使用普通消息。
- 业务处理层(微服务) :
- 仅消费自身业务相关的 Topic,生产的消息需携带完整元数据(设备 ID、订单 ID、时间戳)。
二、配置调优:参数决定稳定性与性能
Spring Cloud Alibaba 集成 RocketMQ 有两种方式(Stream Binder / 原生 Starter),以下是生产环境级的配置调优,附参数含义和取值依据。
1. 全局配置(application.yml,Stream 方式)
yaml
spring:
application:
name: cabinet-service # 服务名,用于动态绑定Group
cloud:
stream:
# 核心:避免不同环境/服务的默认通道冲突
default-binder: rocketmq
rocketmq:
binder:
# 生产环境配置集群,逗号分隔,避免单点
name-server: rocketmq-nameserver1:9876,rocketmq-nameserver2:9876
# 生产者全局配置
producer:
group: ${spring.application.name}-producer-${spring.profiles.active}-group
retry-times-when-send-failed: 3 # 同步发送失败重试次数(建议3次,过多易导致重复)
retry-times-when-send-async-failed: 3 # 异步发送失败重试次数
send-message-timeout: 5000 # 发送超时(核心业务5s,非核心3s)
compress-message-body-threshold: 4096 # 消息体>4KB自动压缩(节省网络/存储)
max-message-size: 4194304 # 最大消息体4MB(默认4MB,需与Broker配置一致)
# 消费者全局配置
consumer:
group: ${spring.application.name}-consumer-${spring.profiles.active}-group
broadcast: false # 默认集群消费(广播仅用于配置推送等场景)
consume-thread-max: 20 # 最大消费线程数(CPU核数×2+1,如8核设17)
consume-thread-min: 5 # 最小消费线程数(避免线程频繁创建销毁)
delay-level-when-next-consume: 2 # 消费失败后延迟重试级别(2=5s,见延迟级别表)
max-reconsume-times: 5 # 最大重试次数(超过进入死信队列)
# 通道绑定(按业务拆分)
bindings:
# 柜机状态上报消费通道
cabinetStatusInput:
destination: cabinet-status-${spring.profiles.active}
content-type: application/json # 统一JSON格式,避免序列化问题
group: ${spring.application.name}-consumer-status-${spring.profiles.active}-group
consumer:
max-attempts: 5 # 消费重试次数(与全局max-reconsume-times一致)
concurrency: 5-10 # 消费线程数范围(动态调整)
batch-mode: true # 开启批量消费(高频消息必开)
batch-size: 32 # 批量消费大小(建议16-64,过大易导致消费超时)
# 仅消费指定Tag
properties:
rocketmq_consumer_TAGS: heartbeat,stock
# 柜机指令下发生产通道
cabinetCommandOutput:
destination: cabinet-command-${spring.profiles.active}
content-type: application/json
producer:
sync: true # 指令下发需同步确认,避免丢失
send-message-timeout: 8000 # 指令下发超时延长至8s
2. 关键参数取值依据(必看)
| 参数 | 取值范围 | 取值依据 |
|---|---|---|
| send-message-timeout | 3000-10000ms | 核心业务(支付、指令)5-8s,非核心(日志、状态)3s;网络差的环境可适当延长 |
| retry-times-when-send-failed | 2-3 次 | 超过 3 次仍失败,大概率是 Broker 故障,重试无意义,需人工介入 |
| consume-thread-max | CPU 核数 ×2+1 | 避免线程过多导致上下文切换,过少导致消费能力不足 |
| batch-size | 16-64 | 单条消息越小,批量越大(如 1KB 消息设 64,100KB 消息设 16) |
| max-reconsume-times | 3-5 次 | 重试次数过多易导致消息堆积,5 次失败基本可判定为业务异常,需进入死信队列 |
3. 延迟级别映射表(重要)
RocketMQ 的延迟消息通过 "级别" 配置,而非直接指定时间,生产环境需熟记:
| 级别 | 延迟时间 | 级别 | 延迟时间 | 级别 | 延迟时间 |
|---|---|---|---|---|---|
| 1 | 1s | 7 | 1m | 13 | 30m |
| 2 | 5s | 8 | 2m | 14 | 1h |
| 3 | 10s | 9 | 3m | 15 | 2h |
| 4 | 30s | 10 | 4m | 16 | 3h |
| 5 | 1m | 11 | 5m | 17 | 4h |
| 6 | 1m | 12 | 10m | 18 | 5h |
三、消息生产:确保 "发得出、发得对、可追溯"
生产端是消息可靠性的第一道防线,核心解决 "丢失、重复、乱序" 问题。
1. 消息体设计规范(落地级)
必含元数据(强制)
| 字段 | 类型 | 说明 |
|---|---|---|
| bizId | String | 业务唯一 ID(订单号、设备 ID + 指令 ID),用于幂等、追溯 |
| timestamp | Long | 消息生产时间戳(毫秒),用于排查延迟问题 |
| version | String | 消息版本(如 v1.0),用于兼容不同版本的消息格式 |
| source | String | 消息来源(服务名 + 实例 ID),用于定位消息生产方 |
| traceId | String | 链路追踪 ID(集成 Sleuth/SkyWalking),用于全链路追踪 |
示例(柜机指令消息)
java
运行
@Data
@Builder
public class CabinetCommandDTO {
// 业务元数据
private String bizId; // 格式:设备ID+指令ID,如DEVICE001-OPEN_DOOR-123456
private Long timestamp;
private String version = "v1.0";
private String source;
private String traceId;
// 业务数据
private String deviceId;
private String commandType; // OPEN_DOOR、CHECK_STOCK
private String productId;
private String orderId;
}
2. 发送策略选择(按场景选型)
| 发送方式 | 适用场景 | 代码示例 |
|---|---|---|
| 同步发送 | 核心业务(支付、指令、订单) | rocketMQTemplate.syncSend(topic, message, 5000); |
| 异步发送 | 非核心但需确认(状态通知) | rocketMQTemplate.asyncSend(topic, message, new SendCallback() { ... }); |
| 单向发送 | 日志、埋点等无需确认的场景 | rocketMQTemplate.sendOneWay(topic, message); |
| 批量发送 | 高频小消息(柜机心跳) | rocketMQTemplate.syncSendBatch(topic, messageList); |
| 事务消息 | 分布式事务(支付 + 订单) | 见下文 "分布式事务" 小节 |
3. 关键优化点
3.1 消息 Key 设置(强制)
消息 Key 是 RocketMQ 中定位消息的核心标识,必须设置为业务唯一 ID:
java
运行
// Stream方式
Message<CabinetCommandDTO> message = MessageBuilder
.withPayload(commandDTO)
.setHeader(RocketMQHeaders.KEYS, commandDTO.getBizId()) // 绑定业务唯一ID
.setHeader(RocketMQHeaders.TAGS, "OPEN_DOOR") // 设置Tag
.build();
// 原生方式
Message<String> msg = new Message<>(topic, "OPEN_DOOR", commandDTO.getBizId().getBytes(), JSON.toJSONBytes(commandDTO));
3.2 批量发送优化(高频场景必用)
柜机心跳、状态上报等高频小消息,批量发送可减少网络请求数,提升吞吐量:
java
运行
@Service
public class CabinetStatusService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
// 批量缓存,每100条或500ms发送一次
private final List<Message> statusMessageCache = new ArrayList<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
// 初始化:每500ms触发批量发送
@PostConstruct
public void init() {
scheduler.scheduleAtFixedRate(this::sendBatchStatus, 0, 500, TimeUnit.MILLISECONDS);
}
public void addStatusToBatch(CabinetStatusDTO statusDTO) {
synchronized (statusMessageCache) {
Message<String> message = MessageBuilder
.withPayload(JSON.toJSONString(statusDTO))
.setHeader(RocketMQHeaders.KEYS, statusDTO.getBizId())
.build();
statusMessageCache.add(message);
// 达到批量阈值,立即发送
if (statusMessageCache.size() >= 32) {
sendBatchStatus();
}
}
}
private void sendBatchStatus() {
synchronized (statusMessageCache) {
if (statusMessageCache.isEmpty()) {
return;
}
try {
rocketMQTemplate.syncSendBatch("cabinet-status-prod", new ArrayList<>(statusMessageCache));
statusMessageCache.clear();
} catch (Exception e) {
log.error("批量发送状态消息失败", e);
// 失败后不清除缓存,下次重试
}
}
}
}
3.3 发送失败处理(核心)
同步发送失败后,禁止直接抛出异常,需执行 "本地落库 + 定时补偿":
java
运行
@Service
public class CommandSendService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private CommandSendLogMapper logMapper; // 本地消息日志表
public void sendCommand(CabinetCommandDTO commandDTO) {
// 1. 先落本地日志(事务)
CommandSendLogDO log = CommandSendLogDO.builder()
.bizId(commandDTO.getBizId())
.messageBody(JSON.toJSONString(commandDTO))
.topic("cabinet-command-prod")
.status("PENDING") // 待发送
.build();
logMapper.insert(log);
// 2. 发送RocketMQ消息
try {
SendResult result = rocketMQTemplate.syncSend(
"cabinet-command-prod:OPEN_DOOR",
commandDTO,
8000
);
if (result.getSendStatus() == SendStatus.SEND_OK) {
// 3. 发送成功,更新日志状态
logMapper.updateStatus(log.getId(), "SUCCESS");
} else {
logMapper.updateStatus(log.getId(), "FAILED");
}
} catch (Exception e) {
log.error("发送指令失败", e);
logMapper.updateStatus(log.getId(), "FAILED");
}
}
// 定时补偿:每1分钟扫描失败/待发送的消息重试
@Scheduled(fixedRate = 60000)
public void compensateFailedMessages() {
List<CommandSendLogDO> failedLogs = logMapper.listByStatus("PENDING", "FAILED");
for (CommandSendLogDO log : failedLogs) {
try {
CabinetCommandDTO commandDTO = JSON.parseObject(log.getMessageBody(), CabinetCommandDTO.class);
SendResult result = rocketMQTemplate.syncSend(log.getTopic(), commandDTO, 8000);
if (result.getSendStatus() == SendStatus.SEND_OK) {
logMapper.updateStatus(log.getId(), "SUCCESS");
}
} catch (Exception e) {
log.error("补偿消息失败,bizId:{}", log.getBizId(), e);
// 超过3次补偿失败,标记为需人工介入
if (log.getRetryCount() >= 3) {
logMapper.updateStatus(log.getId(), "MANUAL");
} else {
logMapper.incrementRetryCount(log.getId());
}
}
}
}
}
四、消息消费:确保 "收得到、处理对、不重复"
消费端是消息可靠性的最后一道防线,核心解决 "重复消费、消费阻塞、消息堆积" 问题。
1. 消费模式选择
| 模式 | 适用场景 | 配置方式 |
|---|---|---|
| 集群消费 | 绝大多数业务(分摊消费) | messageModel = MessageModel.CLUSTERING(默认) |
| 广播消费 | 配置推送、全量通知 | messageModel = MessageModel.BROADCASTING |
| 并发消费 | 无顺序要求的业务(状态上报) | consumeMode = ConsumeMode.CONCURRENTLY(默认) |
| 顺序消费 | 有顺序要求的业务(订单操作) | consumeMode = ConsumeMode.ORDERLY + 单线程消费 |
2. 幂等性保障(强制)
RocketMQ 仅保证 "至少一次消费",重复消费是必然现象,必须在业务层实现幂等:
2.1 幂等方案选型(按场景)
| 方案 | 适用场景 | 实现难度 | 性能 |
|---|---|---|---|
| Redis 分布式锁 + 状态 | 高频读写(柜机指令) | 低 | 高 |
| 数据库唯一索引 | 数据写入(订单创建) | 低 | 中 |
| 本地缓存 + 过期时间 | 无持久化要求(状态查询) | 低 | 极高 |
| 消息表状态标记 | 核心业务(支付结果) | 中 | 中 |
2.2 代码示例(Redis + 数据库双保障)
java
运行
@Component
@RocketMQMessageListener(
topic = "cabinet-command-prod",
consumerGroup = "cabinet-service-consumer-command-prod-group",
selectorExpression = "OPEN_DOOR",
consumeMode = ConsumeMode.CONCURRENTLY,
consumeThreadMax = 10
)
public class CabinetCommandConsumer implements RocketMQListener<CabinetCommandDTO> {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private CabinetCommandRecordMapper recordMapper; // 指令执行记录表
@Override
public void onMessage(CabinetCommandDTO commandDTO) {
String bizId = commandDTO.getBizId();
String redisKey = "consume:command:" + bizId;
// 1. Redis分布式锁(防止并发消费)
Boolean locked = redisTemplate.opsForValue().setIfAbsent(redisKey, "1", 5, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(locked)) {
log.info("指令已处理,bizId:{}", bizId);
return;
}
try {
// 2. 数据库查询是否已处理(持久化保障)
CabinetCommandRecordDO record = recordMapper.selectByBizId(bizId);
if (record != null && "SUCCESS".equals(record.getStatus())) {
log.info("指令已执行成功,bizId:{}", bizId);
return;
}
// 3. 执行业务逻辑(下发指令到柜机)
boolean executeSuccess = cabinetDeviceService.sendCommandToDevice(commandDTO);
// 4. 更新数据库状态
if (record == null) {
record = new CabinetCommandRecordDO();
record.setBizId(bizId);
record.setDeviceId(commandDTO.getDeviceId());
record.setCommandType(commandDTO.getCommandType());
record.setStatus(executeSuccess ? "SUCCESS" : "FAILED");
recordMapper.insert(record);
} else {
recordMapper.updateStatus(bizId, executeSuccess ? "SUCCESS" : "FAILED");
}
if (!executeSuccess) {
// 执行失败,抛出异常触发重试
throw new RuntimeException("指令执行失败,bizId:" + bizId);
}
} finally {
// 释放锁
redisTemplate.delete(redisKey);
}
}
}
3. 消费失败处理(核心)
3.1 重试策略(非瞬时故障延迟重试)
消费失败后,避免立即重试(如下游服务不可用),需设置延迟重试:
java
运行
// 原生方式:自定义重试策略
@Component
@RocketMQMessageListener(topic = "cabinet-command-prod", consumerGroup = "xxx-group")
public class CustomRetryConsumer implements RocketMQListener<MessageExt>, RocketMQPushConsumerLifecycleListener {
@Override
public void prepareStart(DefaultMQPushConsumer consumer) {
// 设置重试延迟级别(失败后5s重试)
consumer.setDelayLevelWhenNextConsume(2);
// 设置最大重试次数
consumer.setMaxReconsumeTimes(5);
}
@Override
public void onMessage(MessageExt msg) {
String body = new String(msg.getBody());
CabinetCommandDTO commandDTO = JSON.parseObject(body, CabinetCommandDTO.class);
// 业务逻辑:若检测到是下游服务不可用,抛出异常触发延迟重试
if (!cabinetDeviceService.isAvailable(commandDTO.getDeviceId())) {
throw new RuntimeException("设备离线,延迟重试");
}
}
}
3.2 死信队列处理(必配)
超过最大重试次数的消息会进入死信队列(DLQ),需单独消费处理:
java
运行
// 死信队列消费者(Topic格式:%DLQ%+消费者组名)
@Component
@RocketMQMessageListener(
topic = "%DLQ%cabinet-service-consumer-command-prod-group",
consumerGroup = "cabinet-service-consumer-dlq-prod-group"
)
public class DlqCommandConsumer implements RocketMQListener<MessageExt> {
@Autowired
private DlqMessageRecordMapper dlqRecordMapper;
@Override
public void onMessage(MessageExt msg) {
String body = new String(msg.getBody());
String msgId = msg.getMsgId();
String originTopic = msg.getProperty("ORIGIN_TOPIC");
// 1. 记录死信消息到数据库
DlqMessageRecordDO record = new DlqMessageRecordDO();
record.setMsgId(msgId);
record.setOriginTopic(originTopic);
record.setMessageBody(body);
record.setStatus("UNPROCESSED");
dlqRecordMapper.insert(record);
// 2. 告警通知(钉钉/短信)
alertService.sendDingTalkAlert("死信队列新增消息,msgId:" + msgId + ",内容:" + body);
// 3. 人工介入处理后,可手动重发或标记为已处理
}
}
4. 顺序消费(特殊场景)
无人售货柜中 "同一订单的扣款→出货→库存更新" 需顺序执行,实现方式:
java
运行
// 生产者:同一订单ID的消息发送到同一Queue
public void sendOrderMessage(OrderDTO orderDTO) {
rocketMQTemplate.syncSend(
"order-process-prod",
orderDTO,
new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
String orderId = (String) arg;
// 按订单ID哈希取模,确保同一订单的消息进入同一Queue
int index = Math.abs(orderId.hashCode()) % mqs.size();
return mqs.get(index);
}
},
orderDTO.getOrderId() // 传递订单ID作为参数
);
}
// 消费者:顺序消费(单线程)
@Component
@RocketMQMessageListener(
topic = "order-process-prod",
consumerGroup = "order-service-consumer-process-prod-group",
consumeMode = ConsumeMode.ORDERLY,
consumeThreadMax = 1 // 顺序消费必须单线程
)
public class OrderProcessConsumer implements RocketMQListener<OrderDTO> {
@Override
public void onMessage(OrderDTO orderDTO) {
// 按顺序执行:扣款→出货→库存更新
payService.deduct(orderDTO);
cabinetCommandService.sendOpenDoorCommand(orderDTO);
stockService.updateStock(orderDTO);
}
}
五、分布式事务:解决跨服务一致性问题
无人售货柜中 "支付扣款 + 订单更新 + 柜机出货" 是典型的分布式事务场景,需使用 RocketMQ 事务消息。
1. 事务消息流程(核心)
plaintext
1. 支付服务发送"半事务消息"到RocketMQ(不可消费);
2. 支付服务执行本地事务(扣款);
3. 扣款成功→提交消息(可消费),扣款失败→回滚消息(删除);
4. 若步骤2超时,RocketMQ主动回查支付服务的事务状态;
5. 订单/柜机服务消费消息,执行后续逻辑。
2. 代码实现(落地级)
2.1 事务生产者
java
运行
@Service
public class PayTransactionProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private PayService payService;
public void sendPayTransactionMessage(PayDTO payDTO) {
// 1. 构建半事务消息
String transactionId = UUID.randomUUID().toString();
Message<PayDTO> message = MessageBuilder
.withPayload(payDTO)
.setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId)
.setHeader(RocketMQHeaders.KEYS, payDTO.getOrderId())
.build();
// 2. 发送半事务消息
rocketMQTemplate.sendMessageInTransaction(
"pay-transaction-prod-group", // 事务生产者组
"pay-result-prod:success", // Topic:Tag
message,
payDTO // 传递业务参数到本地事务
);
}
// 3. 事务监听器(核心)
@RocketMQTransactionListener(txProducerGroup = "pay-transaction-prod-group")
public class PayTransactionListener implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
PayDTO payDTO = (PayDTO) arg;
try {
// 执行本地事务:扣款
boolean deductSuccess = payService.deduct(payDTO);
if (deductSuccess) {
// 提交消息
return RocketMQLocalTransactionState.COMMIT;
} else {
// 回滚消息
return RocketMQLocalTransactionState.ROLLBACK;
}
} catch (Exception e) {
log.error("执行本地扣款事务失败", e);
// 未知状态,触发回查
return RocketMQLocalTransactionState.UNKNOWN;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// 回查本地事务状态:查询扣款记录
String orderId = msg.getHeaders().get(RocketMQHeaders.KEYS, String.class);
PayRecordDO payRecord = payService.getPayRecordByOrderId(orderId);
if (payRecord == null) {
return RocketMQLocalTransactionState.ROLLBACK;
}
return payRecord.getStatus() == 1 ? RocketMQLocalTransactionState.COMMIT : RocketMQLocalTransactionState.ROLLBACK;
}
}
}
2.2 事务消息消费者(订单服务)
java
运行
@Component
@RocketMQMessageListener(
topic = "pay-result-prod",
consumerGroup = "order-service-consumer-pay-prod-group",
selectorExpression = "success"
)
public class PayResultConsumer implements RocketMQListener<PayDTO> {
@Autowired
private OrderService orderService;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Override
public void onMessage(PayDTO payDTO) {
// 1. 更新订单状态(幂等处理)
orderService.updateOrderStatus(payDTO.getOrderId(), "PAID");
// 2. 发送出货指令到柜机
CabinetCommandDTO commandDTO = CabinetCommandDTO.builder()
.bizId(payDTO.getOrderId() + "-OPEN_DOOR")
.deviceId(payDTO.getDeviceId())
.commandType("OPEN_DOOR")
.orderId(payDTO.getOrderId())
.build();
rocketMQTemplate.syncSend("cabinet-command-prod:OPEN_DOOR", commandDTO);
}
}
六、性能优化:从代码到 Broker 的全链路调优
1. 生产者优化
- 批量发送:高频小消息必须批量(如柜机心跳,批量大小 16-64);
- 异步发送:非核心消息使用异步发送,避免阻塞主线程;
- 消息压缩:开启 4KB 阈值压缩,减少网络传输量;
- 避免创建过多生产者实例:Spring 中 RocketMQTemplate 是单例,无需手动创建。
2. 消费者优化
-
批量消费:开启批量消费(batch-mode=true),提升吞吐量;
-
耗时操作异步化 :消费逻辑中耗时操作(如调用外部接口)提交到线程池:
java
运行
@Override public void onMessage(CabinetStatusDTO statusDTO) { // 异步处理耗时逻辑 executorService.submit(() -> { remoteStockService.updateStock(statusDTO); }); } -
消费线程调优:根据 CPU 核数调整线程数,避免线程上下文切换;
-
预取消息数调整:消费者预取消息数(consumeMessageBatchMaxSize)设为批量大小,减少拉取次数。
3. Broker 优化(运维层面)
- 存储介质:生产环境使用 SSD 硬盘,提升刷盘速度;
- 刷盘策略:核心业务开启同步刷盘(SYNC_FLUSH),非核心开启异步刷盘(ASYNC_FLUSH);
- 主从架构:部署主从集群(SYNC_MASTER),确保消息不丢失;
- 队列数调整:单个 Topic 的 Queue 数等于消费者实例数 × 消费线程数,充分利用并行性;
- 磁盘清理:配置消息过期时间(默认 72 小时),避免磁盘占满。
七、运维监控:提前发现问题,快速定位根因
1. 核心监控指标(必看)
| 维度 | 关键指标 | 告警阈值 |
|---|---|---|
| 生产者 | 发送成功率 | <99.9% |
| 生产者 | 发送延迟 | P99>100ms |
| 消费者 | 消费堆积量 | >1000 条 |
| 消费者 | 消费成功率 | <99.9% |
| 消费者 | 消费延迟 | P99>500ms |
| Broker | 消息刷盘延迟 | >50ms |
| Broker | 主从同步延迟 | >100ms |
| Broker | 磁盘使用率 | >80% |
2. 监控工具选型
- RocketMQ Dashboard:官方监控工具,查看 Topic、Group、消费进度、消息轨迹;
- Prometheus + Grafana:接入 RocketMQ Exporter,配置可视化面板和告警;
- Spring Boot Admin:监控微服务内 RocketMQ 客户端状态;
- SkyWalking/Pinpoint:全链路追踪,定位消息生产 / 消费的延迟节点。
3. 消息轨迹(必开)
开启消息轨迹,可追踪消息从生产到消费的全链路:
yaml
rocketmq:
producer:
enable-msg-trace: true
customized-trace-topic: RMQ_SYS_TRACE_TOPIC
八、常见问题与根因分析
| 问题现象 | 常见根因 | 解决方案 |
|---|---|---|
| 消息重复消费 | 网络抖动、消费者宕机、手动重试 | 业务层实现幂等性(Redis / 数据库) |
| 消息堆积 | 消费线程数不足、消费逻辑耗时、批量大小过小 | 增加消费线程数、异步处理耗时逻辑、调大批量大小 |
| 消息丢失 | 生产者异步发送未回调、Broker 异步刷盘、消费者未 ACK | 核心业务同步发送、Broker 同步刷盘、消费完成后再 ACK |
| 顺序消息乱序 | Queue 数设置不合理、多线程消费 | 同一业务 ID 绑定同一 Queue、顺序消费设为单线程 |
| 事务消息回查失败 | 事务监听器与生产者组不匹配、回查方法抛出异常 | 确保组名一致、回查方法捕获所有异常、幂等实现 |
| 消费线程阻塞 | 消费逻辑死循环、调用外部接口超时 | 增加线程超时时间、异步处理、监控线程状态 |
总结
RocketMQ 在 Spring Cloud Alibaba 生态中的最佳实践,核心是 "规范设计、可靠传输、高效消费、全面监控":
- 架构层:通过语义化的 Topic/Group/Tag 设计实现业务隔离;
- 配置层:根据业务场景调优参数,核心业务优先保障可靠性;
- 生产层:消息落库 + 补偿机制,确保 "发得出、发得对";
- 消费层:幂等 + 延迟重试 + 死信队列,确保 "收得到、处理对";
- 监控层:全维度监控指标,提前发现问题,快速定位根因。
结合无人售货柜的业务场景,以上实践可直接落地,支撑高并发、高可靠的设备通信和微服务协作。