分布式事务解决方案:我的实战经验分享
开篇:为什么我要写这篇文章?

作为一个从业务系统开发逐渐转向微服务架构的后端工程师,我这些年参与过不少中小型系统的架构设计和核心模块开发。其中,最让我印象深刻的,是一次典型的“分布式事务”问题。
事情发生在一次重构旧系统、拆分单体应用为多个微服务的过程中。原本一个简单的库存扣减+订单创建的操作,在服务拆分后变成了跨两个服务(订单服务 & 库存服务)的操作,而这两个服务又分别依赖各自的数据库。结果上线没多久,就出现了“订单创建了但库存没扣”,或者更糟的情况,“库存扣了但订单没生成”的问题。
这背后其实就是分布式事务的难题。我当时带着团队踩了不少坑,也尝试过各种方案,最终找到了一条在我们业务场景下相对平衡的路子。于是,就有了这篇结合我实际经历的总结文章。
希望看完以后,你能少走弯路。
问题描述:从单体到微服务,事务失效了

项目背景是我们公司要做一次系统改造,把原来的一个电商平台的单体应用拆分成多个独立部署的服务。具体来说,包括:
- 订单服务
- 商品服务
- 库存服务
- 支付服务
这些服务都使用 Spring Boot + MySQL 的组合,并且通过 RPC 接口互相调用。
我们遇到的问题是:用户下单时需要同时完成两个操作:
- 创建订单
- 扣除商品库存
在单体应用中,这两个操作天然在一个本地事务里,要么一起成功,要么回滚。可拆成服务后,这两个操作分布在不同的服务中,数据也分散在不同数据库中,原有的事务机制失效了。
第一次上线后,我们就收到了几个奇怪的异常数据:
- 用户支付成功了,但库存没有扣减,出现超卖风险。
- 订单状态是“已提交”,但对应商品仍然显示“有库存”,导致客服频繁投诉。
这个问题必须解决,否则系统无法正常运营。
解决方案选择:从本地事务到TCC再到消息队列补偿
面对这种跨库、跨服务的事务问题,常见的解决方案包括:
- 两阶段提交(2PC)/三阶段提交(3PC)
- TCC(Try - Confirm - Cancel)
- Saga 模式
- 基于消息队列的异步补偿机制
- 本地事务表 + 异步处理
我们在讨论选型的时候,首先排除了 2PC 和 3PC,因为这类方案对数据库支持要求高,而且性能差,对于电商业务这种并发量较高的场景来说不太现实。
剩下的几种我们都考虑过。比如:
- TCC 理论上能保证一致性,但开发成本较高,每一个接口都需要实现 Try / Confirm / Cancel 三个方法;
- Saga 模式适合长流程、多步骤的场景(比如订票),但在订单这种短流程场景有点“杀鸡用牛刀”;
- 基于 RocketMQ 或 Kafka 的消息队列方式,在我们之前做过类似实践,有一定积累。
最后,我们决定采用一种混合策略:主流程使用本地事务 + 消息队列做后续补偿的方式来实现最终一致性的保障。
这个思路的核心是:
- 在订单服务先创建订单,并记录状态为“待确认”
- 同一本地事务中,插入一条待消费的消息到“事务消息表”
- 异步消费这条消息,远程调用库存服务进行库存扣减
- 如果扣减失败,定时任务会重新投递这条消息
这样既避免了强一致性带来的复杂性和性能开销,又能通过异步补偿来保证最终一致性。
实现细节:代码与架构如何配合工作
接下来分享一下我们这套方案的具体实现方式,涉及到几个关键点:
1. 数据库结构设计
我们新增了一个 order_message 表,用于保存订单创建后的待消费消息,结构如下:
CREATE TABLE order_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL COMMENT '对应的订单ID',
msg_body TEXT NOT NULL COMMENT '消息内容 JSON 格式',
status ENUM('WAIT', 'SENT', 'SUCCESS', 'FAILED') DEFAULT 'WAIT' COMMENT '消息状态',
retry_count INT DEFAULT 0 COMMENT '重试次数',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME ON UPDATE CURRENT_TIMESTAMP
);
这个表的设计目的是为了在创建订单的同时插入一条消息记录,利用本地事务保证订单创建和消息记录同时成功或失败。

2. 代码实现逻辑(Spring Boot + MyBatis)
订单服务中的简化伪代码如下:
@Transactional
public void createOrder(OrderDTO dto) {
// 创建订单
Order order = buildOrder(dto);
orderMapper.insert(order);
// 构建发送给库存服务的消息
InventoryDeductMessage msg = new InventoryDeductMessage();
msg.setOrderId(order.getId());
msg.setSkuId(dto.getSkuId());
msg.setCount(dto.getCount());
// 插入消息表,和订单创建保持在同一个本地事务
orderMessageService.save(msg);
}
这里的关键在于,插入订单和消息这两步是在一个本地事务中完成的,只要其中一步出错就会整体回滚,不会出现“订单没创建成功但消息已经发出去了”的情况。
3. 异步消息消费
我们使用的是 RocketMQ 作为消息中间件。异步消费者定时从数据库拉取状态为 WAIT 的消息并发送到 MQ:
@Scheduled(fixedDelay = 5000)
public void sendMessages() {
List<OrderMessage> messages = messageService.listWaitMessages();
for (OrderMessage msg : messages) {
try {
rocketMQTemplate.convertAndSend("INVENTORY_TOPIC", msg.getMsgBody());
msg.setStatus("SENT");
messageService.update(msg);
} catch (Exception e) {
log.warn("消息发送失败: {}", e.getMessage());
msg.setRetryCount(msg.getRetryCount() + 1);
if (msg.getRetryCount() > MAX_RETRY_COUNT) {
msg.setStatus("FAILED");
}
messageService.update(msg);
}
}
}
库存服务监听该 topic,执行真正的库存扣减动作。
4. 失败重试与补偿机制
我们还增加了一个定时任务用于扫描失败的消息并重新发送:
@Scheduled(fixedDelay = 60_000)
public void retryFailedMessages() {
List<OrderMessage> failed = messageService.listFailedMessages();
for (OrderMessage msg : failed) {
try {
rocketMQTemplate.convertAndSend("INVENTORY_TOPIC", msg.getMsgBody());
msg.setStatus("SENT");
msg.setRetryCount(0); // 重置重试计数器
messageService.update(msg);
} catch (Exception e) {
log.error("重试失败的消息依旧失败: {}", e.getMessage());
}
}
}
这样即使某一时刻网络抖动或库存服务不可用,也能通过不断重试来最终完成整个流程。
踩坑经验分享:那些年我掉过的坑
虽然这个方案最终跑起来了,但我们也不是一蹴而就的,期间遇到了很多坑,现在回头看看特别值得分享。
1. “双写”问题导致数据不一致
初期,我们在消息发送失败时没有及时标记状态,导致某些情况下消息重复发送或者漏发。例如:
- 发送成功但没更新状态 -> 下次调度再次发送同一消息
- 数据库异常导致事务回滚 -> 消息没发但订单已经生成
后来我们引入“乐观锁”更新状态字段的方式,确保只有当前状态是 WAIT 的才会被更新为 SENT。
2. 重试太多反而引起雪崩
一开始我们的最大重试次数设得太高(比如10次),在系统高峰期遇到消息积压,短时间内大量重试请求打到库存服务,直接把库存服务打挂了……
后来调整为最多重试3次,然后进入人工介入流程(比如告警 + 手动补单)。
3. 消费者幂等处理不到位
由于消息可能重复,如果库存服务未做好幂等控制,会导致重复扣减库存。我们在库存服务中增加了去重表(redis缓存最近一段时间内的订单号)来防止重复消费。
4. 日志监控缺失,排查困难
最初消息失败的原因只能靠日志去分析,效率很低。后来我们增加了监控看板,将消息状态流转可视化,大大提升了排障效率。
效果总结:上线后的收益和表现

这个方案上线后,我们跟踪了一段时间的订单链路和库存变动:
- 几乎杜绝了“订单创建但库存未扣减”的现象
- 系统吞吐量相比以前提高了约30%,因为库存扣减不再阻塞主流程
- 即使库存服务短暂不可用,也能通过消息队列和重试机制完成最终一致
运维同学反馈说这套机制上线后生产环境的异常订单数量大幅下降,客服工单减少了70%左右,老板满意,我也松了一口气 😅
经验分享:给你的几点建议
如果你也在面临分布式事务的问题,以下是我这几年踩坑后总结出来的几点建议:
1. 别一开始就追求“强一致性”
很多时候我们以为必须用 TCC 来保证强一致,其实对于多数业务场景(尤其是电商类),最终一致性 + 一定的补偿机制是可以接受的,而且实施起来更容易。
2. 本地事务表是个好东西
不要小看那个小小的事务消息表,它其实是很多异步处理的基础。在不能使用消息事务的情况下,本地事务表可以很好地承担“桥梁”角色。
3. 幂等性是分布式系统设计的生命线之一
不管你是用 TCC 还是消息队列,都要记得给每个接口加上幂等校验(比如唯一订单 ID 或 UUID)。否则一旦消息重复,后果很严重。
4. 监控比你想象的重要得多
一个优秀的分布式事务方案,必须配套一套完善的监控体系,比如:
- 消息状态统计图
- 失败消息告警
- 自动补偿失败提示
没有监控的系统就像瞎子开车。
5. 技术方案要匹配业务场景
并不是所有业务都需要复杂的分布式事务方案。比如有些系统可以通过合并 DB 或服务边界的方式规避跨库事务,也是一种解法。技术方案永远服务于业务目标。
结语:别怕分布式事务,理解本质才是王道
回头看这次分布式事务的实践,我觉得最大的收获不是技术方案本身,而是对分布式系统中“一致性”这个命题有了更深的理解。
事务的本质,就是“状态的变化过程是否可控”。而所谓的“分布式事务”,不过是当你的变化跨越了多个服务、数据库甚至数据中心之后,依然能够维持这种可控的一种努力。
这条路很长,但我相信,只要我们理解了业务、掌握了方法、愿意去踩坑,就能越走越稳。
希望这篇文章能帮到你。如果你有类似的实战案例,欢迎留言交流 🤝

评论 0