分布式事务解决方案:从踩坑到落地的实战分享
背景介绍:为什么我非得碰分布式事务这块硬骨头?

去年年底,我在一家做供应链系统的公司负责一个核心模块的重构。项目背景是这样的:我们原有的系统是单体架构,所有业务逻辑都跑在一个MySQL数据库上,用本地事务就能搞定订单、库存、支付等操作的一致性。但随着业务量的增长,以及微服务化的推进,系统被拆成了多个独立服务,分别部署在不同的应用节点和数据库实例上。
这时候问题来了——用户下一单,要扣库存、生成订单、冻结资金,这三项操作分布在三个服务中,如何保证要么一起成功,要么一起失败?
没错,这就是典型的 分布式事务 场景。
刚开始没经验,直接上了两阶段提交(2PC),结果上线一测,性能崩得连开发环境都撑不住。后来又试了TCC、Saga模式、最终一致性方案……一路踩坑下来,也积累了不少血泪教训。今天就结合真实项目经验,聊聊我是怎么一步一步解决这个问题的。
问题描述:分布式环境下一致性崩溃的真实场景

我们的核心业务流程大致如下:
- 用户下单
- 订单服务创建订单记录
- 库存服务减库存
- 支付服务冻结用户资金
- 所有步骤成功后,订单状态变为“已支付”
理想情况下,四个步骤应该原子性地完成。但在实际运行中,经常出现以下几种异常情况:
- 库存扣减成功,但支付失败,导致库存锁定但没人买单
- 订单创建后,库存接口超时,前端提示用户“处理中”,但用户查不到订单
- 幂等校验缺失导致重复处理,引发数据混乱
这些错误一旦发生,就会导致用户投诉、财务对账困难、客服压力剧增。最严重的一次,我们一个促销活动中库存出现了负数,技术组被拉着开了三轮复盘会……
所以,我们迫切需要一种既能保障数据一致性,又能兼顾性能和扩展性的分布式事务解决方案。
解决方案:选型与实践之路

初期尝试:2PC 的高成本之痛
我们一开始选择了基于Atomikos实现的JTA分布式事务,想着用XA协议来统一协调各资源参与者。听起来很美,但实际上带来几个致命的问题:
- 性能差:因为要两轮通信+阻塞等待,TPS跌了一半不止
- 数据库兼容性差:不是所有的数据库都支持XA(比如某些版本的MySQL)
- 容错能力弱:某个资源挂掉,整个事务链路都会阻塞
简单压测一下,我们就意识到这种强一致性的代价太高了。尤其是对于我们这种每天几万订单的系统来说,2PC根本扛不住流量。
于是我们开始转向其他方案。
最终采用:TCC + 异步消息补偿机制
在权衡各种方案之后,我们选择了 TCC(Try-Confirm-Cancel)模式 作为主框架,配合 RabbitMQ 或 Kafka 来做异步事件驱动。整体思路如下:
✅ TCC 实现流程:
Try 阶段:资源预留
- 订单服务创建待定订单
- 库存服务冻结指定库存
- 支付服务预授权金额
Confirm 阶段:执行业务动作
- 确认库存扣除
- 确认订单生效
- 确认资金正式划转
Cancel 阶段:回滚
- 如果任意一步失败,则触发Cancel操作释放资源
🔄 异步补偿机制:
通过消息队列将各个阶段的操作结果发布出去,并由消费端监听,进行后续确认或补偿。例如:
- 当支付确认失败时,库存服务监听到该消息后自动解锁库存
- 对于未完成的TCC事务,通过定时任务扫描数据库中的“待确认”状态,尝试重新 Confirm 或 Cancel
🔒 补充设计点:
- 每个服务都要提供幂等接口(比如根据唯一订单ID判断是否已经执行过Confirm/Cancle)
- 使用Redis缓存事务ID避免重复处理
- 日志追踪必须完整,以便排查失败原因
关键代码片段:TCC 核心逻辑示例

以下是我们订单服务在 Try 阶段的核心伪代码:
public class OrderService {
@Transactional
public String tryOrder(OrderDTO orderDTO) {
// 1. 创建订单,状态为"待确认"
Order order = new Order();
order.setStatus("PENDING");
order.setUserId(orderDTO.getUserId());
orderRepository.save(order);

// 2. 发送 Try 成功事件
messageProducer.send("ORDER_TRY_SUCCESS", order.getId());
return order.getId(); // 返回事务ID用于后续确认/取消
}
@Transactional
public void confirmOrder(String orderId) {
Order order = orderRepository.findById(orderId);
if (order.getStatus().equals("PENDING")) {
order.setStatus("CONFIRMED");
orderRepository.update(order);
messageProducer.send("ORDER_CONFIRMED", orderId);
}
}
@Transactional
public void cancelOrder(String orderId) {
Order order = orderRepository.findById(orderId);
if (order.getStatus().equals("PENDING")) {
order.setStatus("CANCELLED");
orderRepository.update(order);
messageProducer.send("ORDER_CANCELLED", orderId);
}
}
}
而库存服务则监听 ORDER_TRY_SUCCESS 事件并执行对应的 Try 操作。
类似地,每个服务都需要具备 Try/Confirm/Cancel 三种行为,并且要支持幂等和重试。
踩坑经验分享:那些深夜调试教会我的事
坑点一:没有幂等处理,同一笔订单被多次Confirm
我们最初没有在Confirm方法里加幂等判断,结果因网络延迟导致消息重复消费,同一个订单被反复Confirm,资金被重复扣款。
解决方案:
- 在Confirm方法开头检查是否已经处理过当前事务ID
- 使用Redis存储执行记录,设置合理TTL防止堆积
if (redis.exists("confirmed_order:" + orderId)) {
log.warn("订单 {} 已确认,跳过重复处理", orderId);
return;
}
redis.setex("confirmed_order:" + orderId, 24 * 3600, "1");
坑点二:Cancel逻辑不完善,导致死锁资源无法释放
在一次压测中,发现大量库存处于“冻结”状态,但是并没有对应订单生成。原因是Cancel方法由于数据库连接池耗尽未能及时执行。
解决方案:
- Cancel操作不能依赖远程调用,要尽量使用本地化手段快速释放资源
- 加入定时任务定期清理“卡住”的事务
坑点三:日志信息不全,定位问题像盲人摸象
早期日志只记录事务ID,没有上下文关联,出了问题只能靠人工翻表查找。
改进措施:
- 统一埋点跟踪ID,贯穿整个事务生命周期
- 接口参数、结果返回值全记录
- 使用ELK集中日志平台 + Kibana 查询分析
效果总结:系统稳定性提升,运维复杂度可控
经过三个月的打磨和灰度上线测试,我们这套基于TCC + 异步消息补偿机制的分布式事务方案表现非常稳定:
| 指标 | 上线前 | 上线后 |
|---|---|---|
| 事务成功率 | 87% | 99.2% |
| 数据不一致率 | 0.5% | < 0.01% |
| 单笔交易平均耗时 | 320ms | 180ms |
| 运维报警次数 | 每天 5~6 次 | 基本归零 |
尤其在大促活动期间,整个系统面对高峰流量表现出色,没有再出现之前那种“库存负数”、“订单丢失”的问题。
给你的建议:如何优雅应对分布式事务难题?
如果你也在面临分布式事务的挑战,这里是我总结的一些经验和建议,希望能帮你少走些弯路:
1. 优先考虑最终一致性方案
除非你真的对数据一致性要求极高(比如银行转账),否则不要上来就搞强一致的2PC或3PC。这类方案性能差、复杂度高,很容易拖垮系统。
2. TCC 是相对成熟的折中选择
它虽然开发量大,但可以很好地控制事务边界和失败兜底逻辑。适合交易类场景。
3. 幂等性和重试机制要前置规划
接口设计初期就必须把幂等问题考虑进去,而不是等到上线后再补救。否则你会像我一样,在凌晨三点debug一条重复的消息消费。
4. 异步消息机制 + 可视化监控是关键
借助消息队列做解耦,加上完善的日志追踪和后台看板,可以大大降低运维成本。
5. 不要忽略人为兜底策略
即使系统设计得再严密,也难免会有一些极端情况漏网。建议保留手动回滚工具或者自动化巡检脚本,关键时刻能救命。
结语:分布式事务没有银弹,只有合适的选择
写到这里,我已经不记得和TCC打了多少场“持久战”。有时候也会想,要是当初一开始就不用微服务多好,但现实往往不允许我们这样逃避问题。
其实,分布式事务从来不是一个单纯的技术问题,它更像是一道综合题:你需要懂得数据库原理、掌握异步编程思想、理解业务规则、还要有良好的容错意识。更重要的是,你要有足够的耐心去面对它的复杂性。
希望这篇文章能给你一些启发,也欢迎你在评论区交流你们团队在处理分布式事务方面的实践经验。
毕竟,一个人走得快,一群人走得远。我们一起慢慢变强吧!

评论 0