分布式事务的那些事儿:踩过的坑、走过的路都在这里了
引子:为啥要写这么一篇?

我是一个五年经验的后端工程师,从最早在创业公司折腾单体应用,到后来进入中大型互联网企业负责核心业务模块的设计与开发,一路走来对“分布式系统”的复杂性和挑战深有体会。特别是分布式事务这玩意儿,刚开始我以为它就是“多个库一起commit/rollback”那么简单,结果实际工作中被它狠狠上了一课。
这篇东西不是教科书式的技术论文,也不是高大上的架构设计白皮书,而是想以一个开发者的真实经历为出发点,聊聊我在项目中碰到的分布式事务问题、当时的应对策略、踩过的坑和最终怎么把它搞定的心得。
这篇文章会结合我的一个真实项目场景展开——电商平台订单支付模块的重构。如果你也经历过类似的系统改造或服务拆分,相信你会找到共鸣。
项目背景:从单体到微服务的痛

去年我们公司决定把原来基于Spring MVC的单体电商系统逐步拆分成微服务架构。其中最核心的几个模块包括:
- 用户中心(用户管理)
- 商品服务(商品信息)
- 库存服务(库存扣减)
- 支付服务(第三方对接)
- 订单服务(下单、支付状态变更等)
这次的重点是订单支付流程,涉及多个服务之间的协同操作:
- 创建订单 -> 写入订单表
- 扣除用户积分(优惠券、积分抵现)
- 扣减商品库存
- 调用外部支付系统完成支付动作
- 修改订单状态为已支付
这些操作分散在不同服务中,数据也在各自的数据库里。而我们最开始只用了最简单粗暴的做法:先下订单,再调其他服务异步处理后续步骤。这样做最大的问题是:如果其中一个环节失败,整个流程就很难回滚了。
举个例子:
用户提交订单时系统正常创建了订单,然后调用库存服务扣减库存成功,但调用支付接口失败。这时候库存已经扣了,用户没支付成功,系统又没有机制自动恢复库存,这就导致了库存不准的问题。
这个问题在测试环境还可以靠人工修复,但到了生产环境,数据量一上来,根本没法手工处理。于是,我们不得不正视一个问题:我们需要一个可靠的方式来保证这一系列跨服务操作要么全部成功,要么全部失败。
真实挑战:面对分布式的无力感
一开始我们在团队内部讨论过几个方案:
- 本地事务 + 补偿机制:先做一次主操作,失败就回退前面的操作。
- 引入TCC模式:尝试搞出Try - Confirm - Cancel三个阶段的动作。
- 使用Saga模式:通过一系列本地事务加补偿操作组合实现。
- 引入Seata这类开源分布式事务框架:看看有没有成熟的组件能直接用。
这些听起来都挺靠谱的,但在实际落地时才发现每种方案都有各自的适用场景和局限性。
我们遇到的主要难点包括:
- 每个服务的数据模型差异很大,有的服务压根不支持“反向操作”;
- 服务之间存在依赖关系,补偿机制复杂;
- 并发环境下幂等性难以保障;
- 事务跨度长、链路复杂,日志跟踪和调试成本高。
最后我们决定采用一种混合方式:对于强一致性要求高的环节使用TCC,对于弱一致性要求的使用异步补偿机制。
解决方案:TCC + 补偿机制的混搭打法
技术选型:为什么选TCC?
我们之所以选择TCC而不是Saga或本地事务补偿,是因为:
- TCC可以在关键路径上提供比较可控的一致性;
- 在订单支付这种金额敏感的操作中,不能容忍中间状态长时间悬挂(如Saga可能出现的状态等待);
- 框架层面可以借助像ByteTCC或者Lcn这类轻量级TCC实现来快速搭建。
不过由于我们当时对TCC理解还不到位,直接引用了一个GitHub上的轻量级TCC框架(现已弃用),结果踩了不少坑。
我们是怎么设计的?
1. 对关键操作封装TCC接口
我们为每一个需要参与分布式事务的服务定义了三个方法:
public interface InventoryService {
boolean prepare(String businessKey); // Try阶段:预占库存
boolean commit(String businessKey); // Confirm阶段:正式扣减
boolean rollback(String businessKey); // Cancel阶段:释放预占库存
}
所有服务在接入的时候都需要实现这三个接口。
2. 主流程中使用TCC协调器
我们使用了一个轻量级的TCC调度器来协调各个服务间的调用顺序,并维护事务上下文。大致逻辑如下:
try {
// 开启事务
tcContext.begin();
// Step 1: 下单并锁定积分(用户服务)
userPointService.prepare(orderId);
// Step 2: 预占库存(库存服务)
inventoryService.prepare(orderId);
// Step 3: 创建订单(订单服务)
orderService.create(orderId);
// Step 4: 发起支付请求(支付服务)
paymentService.charge(orderId);
// 提交事务
tcContext.commit();
} catch (Exception e) {
log.error("分布式事务执行异常", e);
// 回滚整个事务
tcContext.rollback();
}
当然这段代码只是示意,实际的实现远比这个复杂得多。
3. 同步 vs 异步 + 定时补偿
并不是所有的操作都需要走TCC。比如:
- 支付完成后发送通知邮件、更新用户统计信息等属于弱一致性的操作;
- 这些我们采用了本地事务+消息队列+定时任务的组合拳。
例如,支付成功后将事件发往MQ,由下游服务消费后异步处理;同时后台还有一个定时任务定期检查未完成的任务进行补偿。
踩坑记:那些年我们一起翻过的车
坑1:幂等性没做好,补偿变成了二次错误
曾经有一次线上故障:用户一笔订单居然扣了两次库存。排查发现是因为Cancel操作重复执行了,而我们的rollback并没有检查是否已经释放过了。
教训:任何涉及到Cancel或Confirm的操作都要有幂等判断。建议:
- 使用唯一标识符(businessKey) + 状态字段(如:已提交 / 已取消);
- 或者用Redis记录当前事务ID的状态,防止重复提交。
坑2:TCC性能拖垮系统吞吐量
上线初期我们发现系统QPS严重下降,高峰期甚至出现了超时和服务雪崩的现象。
分析发现是TCC协调器每次都要串行调用多个服务的prepare、commit接口,并且中间还要持久化事务日志。
优化手段:
- 将TCC事务控制粒度缩小,非关键路径尽量用异步;
- 引入缓存机制减少Prepare阶段不必要的数据库访问;
- 异步刷盘事务日志,牺牲一定的可靠性换性能;
- 升级底层RPC框架,改为Netty传输 + 异步回调机制。
坑3:事务链过长导致“悬挂事务”
所谓“悬挂事务”,指的是某个节点卡住了,其他节点都等着,结果整个系统阻塞。
比如:某个服务因网络问题未响应,整个事务一直处于waiting状态。
解决方案:
- 设置合理的超时时间;
- 对于长时间处于挂起状态的事务,引入定时检查机制,主动触发回滚;
- 关键服务要有降级策略,确保系统整体可用性。
一些实用代码片段分享
以下是我们事务上下文管理类的一个简化版:
@Component
public class TcTransactionContext {
private static ThreadLocal<String> context = new ThreadLocal<>();
public void begin() {
String txId = UUID.randomUUID().toString();
context.set(txId);
// 存储到线程上下文中
}
public void commit() throws Exception {
String txId = context.get();
List<Participant> participants = TransactionManager.getParticipants(txId);
for (Participant p : participants) {
p.confirm(); // 调用confirm
}
}
public void rollback() {
String txId = context.get();
List<Participant> participants = TransactionManager.getParticipants(txId);
for (Participant p : participants) {
p.cancel(); // 调用cancel
}
}

public static String currentTxId() {
return context.get();
}
}
服务接口实现示例:
@Override
public boolean prepare(String businessKey) {
// 先冻结库存,记录冻结状态
return inventoryDao.freeze(businessKey);
}
@Override
public boolean commit(String businessKey) {
return inventoryDao.deduct(businessKey);
}
@Override
public boolean rollback(String businessKey) {
return inventoryDao.release(businessKey);
}
当然实际的DAO层还需要加入乐观锁、重试机制以及幂等判断,才能真正落地。
效果评估:系统稳定性显著提升
经过半年多的打磨和优化,新版本的支付流程上线后效果非常不错:
| 指标 | 上线前 | 上线后 |
|---|---|---|
| 支付成功率 | 97.2% | 99.8% |
| 数据一致性问题 | 每周20+ | 几乎为零 |
| 平均TPS | 800左右 | 提升至2500+ |
| 事务平均耗时 | 280ms | 降低至150ms以内 |
更关键的是,运维同学反馈说因为数据不一致导致的人工干预几乎消失,系统健壮性大大增强。
经验总结:给后端同学的一些建议
如果你也在做类似的事情,以下是我根据实践经验总结的一些小建议:
✅ 优先考虑业务场景的容错能力
并不是所有场景都需要用TCC或者其他严格一致性方案。比如日志、通知、打点等弱一致性业务完全可以用“最终一致+补偿”来解决。
✅ 不要迷信框架,一定要理解底层原理
我当时就是因为看到一个TCC框架文档看起来简单易懂,直接引入进来用,结果在线上出了不少问题。后来自己动手改了一套简化的TCC内核才稳定下来。
✅ 异步补偿机制也要设计好失败兜底方案
比如MQ发送失败怎么办?下游服务宕机怎么办?定时任务挂了会不会数据丢失?这些问题都要提前想到并留有回旋余地。
✅ 日志监控要到位
事务上下文ID必须贯穿整个调用链,每个服务的日志中都要带上txId,这样排障时才能快速定位问题发生在哪一步。
✅ 权衡性能与一致性
在分布式系统中,“鱼和熊掌不可兼得”的情况太常见了。你要清楚你的业务到底能不能接受一定概率的数据“延迟一致”。
结语:别怕复杂的系统,关键是要有章法
分布式事务这玩意儿说实话真的挺难搞的。尤其是当系统规模一大,服务数量一多,各种边界条件和异常情况就层出不穷。
但从我的经历来看,只要你能做到以下几点:
- 明确需求,合理划分一致性边界;
- 掌握常用技术方案的核心思想;
- 设计良好的降级、补偿和监控机制;
- 有足够的耐心去踩坑、填坑、写文档;
那么再复杂的系统也可以慢慢理顺,最终形成一套属于自己的“分布式事务治理体系”。
希望我的实战经验对你有所帮助。也欢迎留言交流你们在实际项目中遇到的分布式事务难题,咱们一起踩坑、一起成长 🚀
本文作者:一位正在搬砖路上坚持输出的Java程序员,走过路过不要吝啬点赞评论~

评论 0