分布式事务的实战经验分享:从踩坑到最佳实践
引言:为什么需要关注分布式事务?

2021年,我参与了一个电商平台重构项目,目标是将原本单体应用拆分为微服务架构。一开始我们非常乐观,以为把服务拆开来、用 RESTful 接口互相调用就完事了。但没过多久,业务上就遇到了一个棘手的问题:订单创建成功后,用户余额扣除失败怎么办? 或者反过来,余额扣了,订单却没创建成功。
这其实就是典型的分布式事务场景——两个独立的服务(订单中心和账户中心)之间需要保证数据一致性。在单体架构中,通过数据库事务就可以解决;但在微服务架构下,传统的本地事务再也撑不住了。
于是我开始深入研究各种分布式事务方案,在实践中踩了不少坑,也积累了不少经验。这篇文章我想结合亲身经历,聊聊在真实项目中如何解决分布式事务问题,以及我在实际落地过程中遇到的一些挑战和解决方案。
项目背景与挑战:电商平台的“一致性”噩梦

我们的电商平台最初是一个单体应用,所有的模块都在同一个代码库,运行在一个 JVM 实例中,使用的是 MySQL 单库。系统结构简单,维护成本低。但随着用户量上涨,单体应用逐渐暴露出诸多瓶颈:
- 部署频率高时容易相互影响
- 模块耦合严重,修改一处可能牵动全局
- 性能压力集中在某几个关键操作上,比如下单流程
于是团队决定进行微服务化改造,将原有功能模块拆分成独立服务:
- 用户服务(管理用户信息)
- 商品服务(商品上下架、库存)
- 订单服务(订单生命周期管理)
- 账户服务(用户的余额、积分等资产)
服务间通过 Dubbo + Nacos 实现远程调用。初期一切顺利,直到我们上线第一个核心业务流程:用户下单购买商品。
遇到的核心问题
一次完整的下单流程涉及:
- 扣减库存(商品服务)
- 创建订单(订单服务)
- 扣除用户余额(账户服务)
这三个步骤必须全部成功或全部失败。否则就会出现下面几种情况:
- 库存扣了,订单没建好 → 数据混乱
- 订单建好了,用户余额没扣 → 风险敞口
- 用户余额扣了,库存没扣 → 黑客攻击或资金异常
刚开始我们尝试用 try-catch 包裹整个流程,失败了再逐个补偿,但很快发现这种方式根本不靠谱:
- 失败回滚逻辑复杂,难以覆盖所有异常场景
- 服务间无法感知彼此状态,补偿逻辑极易出错
- 如果中间某个服务宕机,整个流程陷入死循环
更麻烦的是,线上出现了几次因为网络波动导致的数据不一致问题,最终只能靠人工介入处理,用户体验差,运维成本高。
这个问题让我们意识到:我们需要一套真正有效的分布式事务解决方案。
技术选型与实现思路:CAP 原则下的折中选择

为了解决这个问题,我们对市面上主流的分布式事务方案进行了调研和对比。主要考虑以下几个方面:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 两阶段提交(2PC) | 强一致性 | 单点故障、性能差 | 强一致性要求高、非高并发系统 |
| TCC(Try-Confirm-Cancel) | 灵活、支持高性能 | 开发成本高,需写大量补偿逻辑 | 高并发业务场景,如金融类系统 |
| Saga 模式 | 可扩展性好 | 业务侵入性较强,补偿逻辑较复杂 | 中高复杂度的长周期任务 |
| 最终一致性(MQ + 补偿) | 高性能,解耦 | 存在短暂不一致期 | 对一致性容忍度较高的场景 |

最终我们采用了 TCC 和 基于 MQ 的最终一致性两种方式结合,应对不同的业务场景。
我们的主方案:TCC + Seata
TCC(Try - Confirm - Cancel)是一种经典的分布式事务模式,适用于需要较高一致性和响应速度的场景。
我们选择了阿里开源的 Seata 来实现 TCC 的协调机制。Seata 提供了 AT(自动事务)、TCC、SAGA 等多种模式,适合微服务+Spring Boot 架构。
流程概览
以一个简单的下单流程为例:
TM(发起方) -> 启动全局事务
↓
RM1(商品服务) Try: 冻结库存
RM2(账户服务) Try: 冻结余额
RM3(订单服务) Try: 初始化订单,状态设为“未支付”
↓
全部 Try 成功 → Commit 所有 RM
↓
任意 Try 失败 → Rollback 所有 RM
这样可以保证多个服务之间的最终一致性。
核心设计点
- 每个服务都要实现
Try,Confirm,Cancel接口,并注册到 Seata - Try 方法需要幂等、可重试、不能破坏最终一致性
- Confirm/Canel 必须是无副作用的
- 使用 AOP 注解管理事务边界,开发体验接近本地事务
技术难点
- 如何优雅地实现幂等性?我们采用了“全局唯一 ID + DB 唯一索引”的方式。
- 重试策略如何设定?不同场景采用不同的重试次数,例如网络问题最多重试3次,逻辑错误直接放弃。
- 日志追踪尤为重要。每个请求都带上 XID(Seata 的全局事务 ID),方便后续排查。
代码实战:TCC 在 Spring Boot 中的应用示例

以下是一个简化版的下单流程实现,展示如何在 Spring Boot + Dubbo + Seata 中整合 TCC。
商品服务(冻结库存)
@LocalTCC
public interface InventoryService {
@TwoPhaseBusinessAction(name = "deductInventory")
boolean deductInventory(BusinessActionContext ctx);
@Commit
boolean commitInventory(BusinessActionContext ctx);
@Rollback
boolean rollbackInventory(BusinessActionContext ctx);
}
实现类略去具体逻辑,重点是在注解中标明这是一个 TCC 事务操作,由 Seata 自动协调。
账户服务(冻结余额)
@LocalTCC
public interface AccountService {
@TwoPhaseBusinessAction(name = "freezeBalance")
boolean freezeBalance(BusinessActionContext ctx);
@Commit
boolean commitBalance(BusinessActionContext ctx);
@Rollback
boolean rollbackBalance(BusinessActionContext ctx);
}
下单主流程(订单服务)
@GlobalTransactional // 开启全局事务
public Order createOrder(CreateOrderReq req) {
// 调用库存服务
inventoryService.deductInventory(...);
// 调用账户服务
accountService.freezeBalance(...);
// 创建订单
Order order = new Order();
order.setStatus("UNPAID");
return orderRepository.save(order);
}
当任意一步抛出异常,Seata 会自动触发所有已执行的 rollbackXXX 方法,保证数据一致性。
实战中的“坑”:那些让我半夜惊醒的教训
尽管 Seata 很强大,但在实际部署中我们也遇到不少问题。这里分享几个踩过的坑和对应的经验。
1. 网络超时导致的事务卡住
问题描述:
某个节点挂掉后事务一直“卡住”,后台日志没有明显报错,但系统处于半瘫痪状态。
原因分析:
TCC 依赖 RM(资源管理者)返回结果,如果某节点超时未响应,Seata 会等待默认超时时间(通常是 60s),然后才触发降级处理。
解决办法:
- 设置合理的超时时间(建议控制在 5~10s)
- 增加健康检查机制,在事务启动前先探测各服务可用性
- 加入熔断机制(Hystrix / Sentinel),防止雪崩效应
2. 幂等性没做好导致重复处理
问题描述:
由于网络波动,某些操作被重复执行多次,导致库存多扣或余额异常。
根本原因:
Try/Cancel 请求没有做幂等控制,同一个事务被多次提交。
解决办法:
- 利用唯一业务标识(如 userId + orderId)作为幂等键
- 将幂等记录写入 Redis,设置 TTL
- 重要操作加 DB 唯一键约束,避免重复执行
3. 全局锁竞争激烈导致性能下降
问题现象:
高并发下单时,接口响应变慢,甚至出现线程阻塞。
分析结论:
Seata 默认会对资源加全局锁,用于防止脏读。但在高并发场景下,频繁获取锁会影响性能。
优化措施:
- 合理划分事务粒度,尽量减少参与事务的服务数量
- 减少 Try 阶段的锁持有时间
- 升级到 Seata 新版本,利用其优化后的锁机制(如异步释放锁)
成果与收益:从“人肉修数据”到自动化治理
自从引入 Seata 并优化分布式事务流程之后,我们的系统稳定性有了显著提升:
- 数据一致性错误减少了约 98%
- 平均下单耗时从原来的 600ms 降低至 300ms 左右
- 运维不再需要每天手动修复数据,节省大量人力成本
更重要的是,我们的系统具备了更好的扩展能力,新业务模块接入成本更低,也更容易支撑未来更高频的交易场景。
给读者的几点实用建议
如果你也在面临分布式事务的难题,以下是我在工作中总结的一些经验,希望对你有所帮助:
✅ 优先选择适合你业务特性的方案
没有“最好的方案”,只有最合适的方案。像电商这种场景下,TCC 是个不错的选择;但对于一些容忍度更高的场景(如日志上报),使用消息队列+补偿机制更加轻量高效。
✅ 强调幂等设计
在分布式环境下,幂等就是生命线。无论你用什么方案,务必确保每个接口都能抗住重试的压力。
✅ 日志监控必不可少
建议将每个事务的 XID、操作链路记录下来,便于快速定位问题。可以用 APM 工具(如 SkyWalking)做全链路追踪。
✅ 不要怕引入“笨拙”的补偿机制
有时候看似复杂的补偿逻辑,其实是保障系统健壮性的最后一道防线。特别是在面对不可预料的极端场景时,宁可“笨一点”,也要“稳一点”。
✅ 保持开放心态
现在越来越多新的技术手段可以帮助我们更好地管理分布式事务,比如 Event Sourcing、CQRS、分布式数据库(如 TiDB、OceanBase)等。不要局限于当前的技术栈,根据业务发展灵活调整方案。
写在最后:分布式事务不是终点,而是旅程的一部分
回顾这段经历,我最大的感触是:分布式事务并不是“万能钥匙”,而是一种权衡的艺术。
它考验的不仅仅是你的技术广度,更是你对业务的理解深度。每一个决策背后,都是无数次的实验和踩坑,还有与产品、测试、运维等多个角色之间的反复沟通。
或许有一天,我们会彻底告别“分布式事务”的概念,用更好的架构来替代它。但在那一天来临之前,请珍惜每一次解决问题的经历——它们会成为你最宝贵的技术财富。
如果你也在微服务的路上狂奔,希望这篇文章能为你带来一点点启发和方向。
愿你在代码世界里少踩坑,多收获!

评论 0