大宗商品销采运储系统・六层架构连载(公共中间件层)
上一篇(消息队列上篇),我们学习了消息队列基础模型、流转链路以及 RocketMQ 基础接入代码,实现业务事件异步投递。
但在大宗商品销采运储业务里,销售开单、库存扣减、财务记账这类核心业务,会遇到一个经典难题:
数据库事务和消息发送,如何保证原子性?
业务库事务提交成功,消息发送失败,下游业务收不到事件,单据断层;消息发送成功,数据库事务回滚,下游收到消息,业务却没有生成单据,造成数据不一致。
本篇聚焦两大核心:事务消息原理与落地、业务幂等设计,解决销采运储核心单据的数据一致性问题。
一句话定调:
事务消息解决「数据库和消息的原子性」,幂等解决「消息重复投递带来脏数据」。
一、什么是事务消息,业务场景边界
大宗商品业务场景举例:销售订单创建成功,需要发送消息触发仓储生成出库单。
理想状态:订单入库成功,消息一定发出;订单入库失败,消息一定不发出。
普通消息无法做到,这时就需要 RocketMQ 事务消息。
事务消息执行流程:
-
生产者发送半消息到 Broker,Broker 暂存消息,此时下游消费者无法消费这条消息。
-
Broker 收到半消息成功后,生产者执行本地数据库事务(新增销售订单、扣减预占库存)。
-
本地事务执行完成,生产者向 Broker 提交确认(Commit / Rollback)。
-
Commit:Broker 将消息转为可消费状态,下游消费者可以拉取处理。
-
Rollback:Broker 删除这条半消息,下游永远收不到。
-
-
如果网络中断、服务宕机,Broker 长时间没有收到确认,会主动发起事务回查。生产者收到回查请求,查询本地事务状态,再回复 Broker 提交或回滚。
边界说明:
事务消息保证最终一致性,不是强一致性。适合销采运储单据异步联动场景;不适合要求实时强一致的业务。

二、销采运储事务消息代码示例(Spring Boot RocketMQ)
pom.xml 依赖和上篇保持一致,下面是事务消息核心代码。
1. 事务监听器
@RocketMQTransactionListener
@Component
public class OrderTransactionListener implements RocketMQTransactionListener {
@Autowired
private OrderService orderService;
/**
* 执行本地事务
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
OrderDTO dto = JSON.parseObject(msg.getBody(), OrderDTO.class);
try {
// 执行本地事务:创建销售订单,预扣库存
orderService.createOrderAndLockStock(dto);
// 本地事务成功,通知 Broker 提交消息
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
// 本地事务失败,回滚消息
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
/**
* 事务回查接口,Broker 长时间未收到确认时调用
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
OrderDTO dto = JSON.parseObject(msg.getBody(), OrderDTO.class);
// 查询数据库订单状态,判断本地事务结果
boolean exist = orderService.checkOrderExist(dto.getOrderNo());
if (exist) {
return LocalTransactionState.COMMIT_MESSAGE;
}
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
2. 发送事务消息
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void createSaleOrder(OrderDTO dto) {
String topic = "supply_order_stock_topic";
Message<String> message = MessageBuilder.withPayload(JSON.toJSONString(dto)).build();
// 发送事务消息
rocketMQTemplate.sendMessageInTransaction(topic, message, dto);
}
3. 事务消息配置要点
rocketmq:name-server: 10.0.0.20:9876;10.0.0.21:9876;10.0.0.22:9876producer:group: supply-producer-groupsend-message-timeout: 3000retry-times-when-send-failed: 2
工程规范:
事务消息只用于核心单据场景,普通通知类事件不要使用,增加系统复杂度。
事务回查必须实现,不能省略,是宕机场景的兜底机制。
本地事务尽量轻量化,避免长时间锁库。
事务消息生产者组必须全局唯一,避免与其他生产者混淆。
三、业务幂等:解决消息至少一次投递带来的重复消费
RocketMQ 的投递保障是至少一次投递。网络抖动、消费成功后 ACK 丢失、服务重启重平衡,都会造成同一条消息重复投递。
放到销采运储业务,重复消费会导致:重复生成出库单、重复扣库存、重复生成财务应收单,属于严重线上故障。

3.1 三种幂等实现方案对比
| 方案 | 适用业务场景 | 优点 | 局限性 |
|---|---|---|---|
| 数据库唯一索引 | 订单、出库单、财务流水 | 实现简单,数据库层面强约束 | 只适合新增类单据,更新场景不适用 |
| 业务状态机判断 | 单据状态流转(待处理/已处理/完成) | 适配单据多次状态变更 | 需要维护单据状态,业务代码稍复杂 |
| 幂等表记录 | 跨服务、复杂事件场景 | 通用性最强,可记录消息 ID、业务单号 | 需要额外建表,存在存储开销 |
销采运储项目选型:
单据新增场景优先唯一索引;单据状态流转使用状态机;跨系统复杂事件使用独立幂等记录表。
3.2 幂等表建表 SQL 示例
CREATE TABLE `t_idempotent_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`business_no` varchar(64) NOT NULL COMMENT '业务单号',
`msg_id` varchar(64) NOT NULL COMMENT '消息ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_business_no` (`business_no`),
UNIQUE KEY `uk_msg_id` (`msg_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='幂等记录表';
3.3 幂等消费伪代码示例
@Override
public void onMessage(OrderStockDTO message) {
String orderNo = message.getOrderNo();
// 1. 查询幂等记录
IdempotentRecord record = idempotentMapper.selectByBusinessNo(orderNo);
if (record != null) {
// 已经处理过,直接返回,不执行业务
log.info("单据{}已处理,跳过本次消费", orderNo);
return;
}
// 2. 执行业务逻辑,生成出库单
stockService.createOutBoundOrder(message);
// 3. 写入幂等记录
IdempotentRecord newRecord = new IdempotentRecord();
newRecord.setBusinessNo(orderNo);
newRecord.setMsgId(message.getMsgId());
newRecord.setCreateTime(new Date());
idempotentMapper.insert(newRecord);
}
重要提醒:
幂等判断 + 业务处理 + 幂等记录写入,建议放在同一个数据库事务中,防止中间宕机导致幂等记录未落库。
四、本篇小结
本篇我们介绍了 RocketMQ 事务消息,解决数据库与消息发送的原子性;同时落地业务幂等方案,解决消息重复投递的脏数据风险。
事务消息保证事件“有没有发出去”,幂等保证事件“不会重复处理”,两者搭配,支撑大宗商品订单、库存、财务核心单据的异步流转。
记住一句话:
事务消息保发送,业务幂等保消费;两板斧配合,单据流转才不丢、不重。
但在生产环境,还会遇到消息堆积、重试风暴、死信消息、消息监控告警等运维难题,这些内容放在下篇。
下篇预告:消息队列架构设计(下篇):消息堆积、重试风暴、死信队列治理与集群监控告警实战。
289



被折叠的 条评论
为什么被折叠?



