分布式事务解决方案:最佳实践——来自一线后端工程师的实战分享
引言:为什么我需要去“驯服”分布式事务?

去年我负责公司核心电商平台的库存系统重构。这个项目看起来很常规,但其中有一个关键问题让我在上线前夜焦头烂额:如何保证订单创建与库存扣减这两个服务之间的事务一致性?
我们的订单服务和库存服务已经完成了微服务拆分,各自独立部署、数据隔离,但当一个订单被用户提交时,必须同时完成以下两个操作:
- 订单服务生成订单;
- 库存服务减少商品可售库存。
如果这两个步骤无法做到要么都成功,要么都失败,就会出现超卖或者订单丢失库存却未下单的情况。
我们尝试用传统数据库事务来处理,结果发现跨服务调用根本不适用本地事务。于是,我开始深入研究分布式事务的各类方案,并结合实际场景进行落地尝试。
这篇文章就是想通过我在该项目中踩过的坑、做过的技术选型与最终方案的落地过程,和大家分享一下我在实际工作中总结出的分布式事务最佳实践。
问题描述:真实场景中的“痛点”

在这个项目中,我们遇到的问题主要包括以下几个方面:
1. 服务解耦导致的数据不一致风险
原本订单和库存在一个单体系统里,使用本地事务可以很好地保证一致性。但随着服务拆分,两者变成远程调用关系,任何一次网络故障或部分操作失败都会带来不一致的风险。
比如:
- 订单创建失败但库存已扣减;
- 库存不足却没有阻止订单生成;
- 第三方支付回调确认已完成,但系统内部状态混乱。
这不仅影响用户体验,还可能导致财务对账困难,甚至法律纠纷。
2. 高并发下的性能瓶颈
我们的系统面向全国消费者,促销活动期间QPS能达到几十万级别。传统的强一致性方案(比如两阶段提交)会导致响应延迟高、资源锁定时间长,严重制约系统吞吐量。
3. 老旧业务代码难以适配新架构
库存系统的老代码没有设计为幂等接口,也没有提供补偿逻辑支持,使得引入分布式事务框架变得异常困难。
解决方案:我们是如何“驯服”分布式事务的?


为了解决这个问题,我们团队先后尝试了多个主流的分布式事务解决方案:本地事务消息表、TCC、Saga 模式、Seata 的 AT 模式,以及基于 RocketMQ 的事务消息机制。
最终我们选择了 RocketMQ 事务消息 + 本地重试 + 最终一致性补偿 的组合方案。下面详细介绍整个实现过程。
架构概览
我们构建的整体流程如下:
- 用户提交订单请求 → 订单服务接收请求。
- 创建订单记录(状态为“待支付”),不真正落库,而是写入预订单缓存中(Redis)。
- 发送一条 RocketMQ 半消息到“库存扣减 Topic”,并附带业务属性如 SKU 编码、数量。
- MQ broker 接收半消息后不会立即投递,等待生产者返回 commit 或 rollback。
- 订单服务接收到 broker 回调时,执行本地事务(真正的订单入库操作):
- 如果成功,则 commit 消息;
- 如果失败,rollback,并回滚 Redis 中的临时订单。
- Broker 向库存服务发送该消息后,库存服务执行库存扣除。
- 如果库存扣除失败,进入自动补偿流程(由定时任务触发)。
这样我们就实现了先写订单再减库存的最终一致性保障。
关键技术点解析
🚀 RocketMQ 事务消息原理简介
RocketMQ 提供了事务消息(Transaction Message)机制,允许消息发送方将业务逻辑嵌入到事务过程中。它的基本流程是:
- 发送半消息(Half Message)到Broker;
- 系统执行本地事务;
- 根据本地事务结果决定是 commit 还是 rollback;
- Broker 在 commit 后才将消息推送给消费者。
这种方式解决了本地事务和消息发送的原子性问题,非常适合我们这种先写订单、再发消息给库存服务的场景。
🔁 本地事务+重试机制
为了提高成功率,我们在订单服务中做了以下优化:
- 使用 Redis 缓存预订单信息,在本地事务失败时能快速回滚;
- 增加事务回查次数(最多三次),应对短暂 DB 不可用情况;
- 事务回查时判断是否已提交订单,避免重复提交。
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String orderId = new String(msg.getBody());
try {
// 执行订单创建逻辑
orderService.createOrder(orderId);
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
log.error("订单创建失败: {}", orderId, e);
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
这段代码用于注册事务回调方法,如果抛出异常则会自动 rollback,保证一致性。
⏱️ 补偿机制设计
为了避免因消息消费失败导致库存始终未扣减,我们设计了一个补偿 Job:
- 每隔 5 分钟扫描“待支付”状态的订单;
- 对应每个订单检查其库存是否已经被扣除;
- 若未扣除,重新发送事务消息;
- 成功后更新订单状态,否则标记为“待人工处理”。
这样的机制虽然不能保证实时一致性,但在大多数场景下已经足够满足需求。
技术对比:为何选择 RocketMQ?
我们评估了多种方案,最终选择 RocketMQ 是因为它具备以下优势:
| 方案 | 实现复杂度 | 性能 | 是否支持异步 | 一致性类型 | 适用场景 |
|---|---|---|---|---|---|
| 本地事务消息表 | 中 | 中 | ✅ | 最终一致 | 小规模业务 |
| TCC(Try-Confirm-Cancel) | 高 | 高 | ✅ | 最终一致/强一致 | 复杂业务、金融类 |
| Saga 模式 | 中 | 高 | ✅ | 最终一致 | 长生命周期流程 |
| Seata AT 模式 | 中 | 中 | ❌ | 强一致 | 小集群部署 |
| RocketMQ 事务消息 | 中 | 高 | ✅ | 最终一致 | 高并发交易系统 |
考虑到我们平台以交易为主、读多写少、强调高并发,RocketMQ 成为了性价比最高的选择。
效果总结:上线后的表现与收益
这套方案自上线以来运行稳定,效果显著:
✅ 成功率提升
- 订单和库存一致性错误率从原来的 0.05% 下降到几乎为零;
- 每天约有 10W+ 的订单通过该机制顺利完成。
✅ 性能优化明显
- 平均响应时间下降 28%,TPS 提升约 40%;
- 减少了锁表和数据库压力,DB QPS 更加平稳。
✅ 可运维性强
- 所有失败情况可通过日志系统查看;
- 定时补偿任务可以自动修复大部分异常;
- 结合 ELK 和监控告警系统,做到了事前预警+事后追踪。
经验分享:作为过来人,我想说的几点建议
从业务到架构,再到工程落地,这条路走得并不容易。以下是我在实践中摸索出来的几点心得体会,希望对大家有所帮助。
1. “最终一致性” ≠ 不一致
很多人一听到“最终一致性”就觉得“可能出错”。其实不然。只要补偿逻辑完备,系统就可以做到用户感知不到的“秒级同步”。
我的经验是:“最终一致性 + 监控 + 快速补偿 = 安全可靠的一致性保障。”
2. 不要追求“一刀切”的解决方案
不同的业务对一致性的容忍度不同,比如:
- 支付场景:需要强一致性(推荐 TCC);
- 库存变动:可以接受短暂不一致(用事务消息就很好);
- 日志统计类:完全可以用异步+批处理。
我们要根据实际业务场景选择合适的策略,而不是照搬所谓“统一架构”。
3. 幂等性和补偿机制是标配
无论采用哪种方案,都一定要注意两点:
- 所有对外接口要具备幂等性(防止重复处理);
- 系统要有完善的补偿机制(定时修复失败事务);
这两个点不做,后续维护成本会非常高。
4. 技术债越早偿还越好
我们项目初期为了快速上线,曾绕过了很多补偿机制的设计,结果上线半年后才发现有几起“假扣库存”的事件。后来我们花了整整两周时间补上了这部分设计。
所以,如果你现在正在做一个新系统,请务必在一开始就考虑好这些点。
5. 工具很重要,但不能依赖工具
像 Seata、RocketMQ、XXL-JOB 这些工具确实大大简化了开发流程,但它们并不能解决所有问题。
我见过很多团队把 Seata 接进去了,但压根不了解它背后的工作原理,一出问题就束手无策。记住一句话:
“工具是用来辅助你解决问题的,不是你放弃思考的理由。”
写在最后:技术的本质是为人服务
作为一名后端开发者,我越来越意识到,技术只是手段,解决问题才是目的。
分布式事务听起来很高大上,但实际上它是为了解决一种最朴素的需求:让系统做出正确的反应。
无论是订单扣款,还是银行转账,抑或外卖派单,背后都是一个个实实在在的业务诉求。只有当你真正理解这些诉求背后的逻辑时,才能选择最合适的技术方案。
愿每一位开发者都能成为那个既能写代码、又能懂业务的人。共勉!
如果你对本文提到的 RocketMQ 事务消息实现细节感兴趣,欢迎留言交流,我可以分享更多代码示例和架构图。也欢迎关注我的公众号【程序员小风】,一起聊聊技术、产品和成长的故事。

评论 0