分布式事务实战:从踩坑到掌控,我的经验分享
一、开篇:为什么我要写这个话题?

去年我在一家中型互联网公司参与了一个电商项目的重构,其中最让我头大的一个模块就是订单系统。它牵涉到多个微服务,比如库存服务、支付服务、用户服务和物流服务。这些服务彼此独立部署,使用各自数据库,但业务逻辑上又高度耦合。
有一次我们上线了一个促销活动,订单量瞬间飙涨,结果出现了一些匪夷所思的问题:
- 用户付了款,但是库存没扣;
- 库存扣了,但订单没有生成;
- 系统里出现了“脏数据”,退款也退不出去……
后来复盘发现,这些问题的根因都出在——分布式事务处理不当。
那段时间,我几乎是被这些事务问题“按在地上摩擦”,每天都在排查、日志分析、回滚数据、修 Bug……痛定思痛之后,我开始系统学习分布式事务的相关方案,并在项目中落地了一个相对稳定可靠的解决方案。今天就借这篇文章,把我踩过的坑、收获的经验分享出来,希望能帮到你。
二、真实场景下的问题描述

我们的订单服务采用的是 Spring Cloud + MyBatis + MySQL 架构,每个核心服务(如订单、支付、库存)都是独立微服务,通过 Dubbo 进行远程调用。整个流程大致如下:
- 用户下单 → 订单服务创建订单;
- 调用库存服务,冻结商品库存;
- 调用支付服务完成付款;
- 支付成功后,提交订单并减少库存;
- 发送消息通知物流服务。
理论上看起来没问题,但在高并发或网络波动时就会出乱子。例如:
- 支付成功后,订单状态更新失败;
- 某个服务宕机,导致事务处于中间态;
- 消息队列延迟,造成重复消费或丢失数据。
这些都是典型的分布式事务不一致的场景。
三、解决方案选型与设计思路
在调研了几种主流方案后,我决定采用 Seata + TCC + 异步消息补偿机制 结合的方式:
1. Seata 实现全局事务管理
我们引入了阿里开源的 Seata,实现基于 AT 模式的全局事务管理,适用于大多数场景下的一致性保障。
- 优点:对开发透明,无需改动原有业务代码太多;
- 缺点:AT 模式依赖于数据库本地事务,需要引入 Undo Log 表,在大并发情况下会带来一定性能损耗。
我们在订单服务中添加了 Seata 客户端,所有跨服务调用都会被纳入同一个全局事务中:
spring:
cloud:
alibaba:
seata:
tx-service-group: my_tx_group
并配置好了 TC(Transaction Coordinator)服务器,部署在 Kubernetes 上,作为中心协调器。
2. 针对库存等关键资源,使用 TCC 模式
由于库存操作非常频繁,且不能容忍任何误差,所以我们为库存服务单独实现了 TCC 模式:
- Try 阶段:检查并冻结库存;
- Confirm:正式扣减库存;
- Cancel:解冻库存。
这样即使后续服务失败,也能保证最终一致性。
举个例子,下面是 Try 接口的核心逻辑:
public boolean deductStockTry(String productId, int quantity) {
int available = stockRepository.getAvailable(productId);
if (available < quantity) {
return false;
}
// 冻结库存
stockRepository.freeze(productId, quantity);
return true;
}
Confirm 的实现:
public void deductStockConfirm(String productId, int quantity) {
stockRepository.deduct(productId, quantity);
}
Cancel 的实现:
public void deductStockCancel(String productId, int quantity) {
stockRepository.unfreeze(productId, quantity);
}
3. 异步补偿机制兜底
为了应对极端情况(如 Seata Server 崩溃),我们还引入了异步消息补偿机制:
- 所有事务操作完成后,发送一条事务状态变更的消息到 Kafka;
- 另一个消费者监听这个 Topic,如果发现某笔事务卡在中间状态超过一定时间,自动触发补偿逻辑。
这部分是系统的“保险丝”,虽然平时可能不会动,但关键时刻能救命。
四、关键代码实践
下面我贴一段 Seata 在订单服务中的调用示例,方便你更好地理解集成方式:
@GlobalTransactional
public void createOrderAndPay(OrderDTO orderDTO) {
// 1. 创建订单
Order order = orderService.createOrder(orderDTO);
try {
// 2. 调用库存服务冻结库存(需支持 Seata)
inventoryService.freezeInventory(order.getProductId(), order.getQuantity());
// 3. 调用支付服务扣款
paymentService.charge(order.getUserId(), order.getTotalPrice());
// 4. 提交订单
orderService.submitOrder(order.getId());
} catch (Exception e) {
// 异常时,Seata 会自动回滚
log.error("分布式事务执行失败,将进行回滚", e);
throw e;
}
}
可以看到,使用 Seata 后业务代码变化不大,只需要加上 @GlobalTransactional 注解即可。
而 TCC 模式则需要开发者自己实现 Try/Confirm/Cancel 接口,并注册到 Seata 中:
@Component
public class InventoryTccAction implements TccActionOnePhase {
@Autowired
private InventoryService inventoryService;
@TwoPhaseBusinessAction(name = "deductStock")
public boolean prepare(BusinessActionContext ctx) {
String productId = (String)ctx.getActionContext("productId");
int quantity = (Integer)ctx.getActionContext("quantity");
return inventoryService.freezeInventory(productId, quantity);
}
@Commit
public boolean commit(BusinessActionContext ctx) {
String productId = (String)ctx.getActionContext("productId");
int quantity = (Integer)ctx.getActionContext("quantity");
inventoryService.deductInventory(productId, quantity);
return true;
}
@Rollback
public boolean rollback(BusinessActionContext ctx) {
String productId = (String)ctx.getActionContext("productId");
int quantity = (Integer)ctx.getActionContext("quantity");
inventoryService.unfreezeInventory(productId, quantity);
return true;
}
}
五、踩坑经验分享
坑 1:Seata 和本地事务冲突
我们一开始为了提升接口性能,开启了本地事务和 Seata 共同工作,结果导致事务嵌套,出现了死锁和锁等待超时。
解决方法:
- 统一使用 Seata 来管理所有分布式事务;
- 关闭本地事务控制(如去掉 spring 的 @Transactional)。
坑 2:TCC 未幂等导致重复扣减
在一次测试中发现,库存多次被重复扣除,最后查出是因为 TCC 的 Commit 方法没有做好幂等处理。
解决方法:
- 在每个 Confirm/Cancel 方法里加入唯一事务 ID 判断;
- 使用 Redis 缓存事务 ID,防止重复执行。
public boolean commit(BusinessActionContext ctx) {
String txId = ctx.getXid();
if (redisTemplate.opsForValue().get("tx_" + txId) != null) {
return true; // 已经处理过
}
// 扣减库存逻辑...
redisTemplate.opsForValue().set("tx_" + txId, "done", 24, TimeUnit.HOURS);
return true;
}
坑 3:消息补偿机制失效
有一阵子因为 Kafka 消费积压,导致很多订单无法正常释放库存,甚至影响到了第二天的业务。
解决方法:
- 增加消费者的并发数;
- 设置合理的重试策略和监控报警;
- 加入定时任务兜底查询未处理完成的事务。
六、实施效果与收益
自打这套分布式事务体系上线后,系统稳定性明显提升:
| 指标 | 上线前 | 上线后 |
|---|---|---|
| 订单异常率 | 0.8% | 0.05% |
| 库存一致性 | 偶尔错乱 | 几乎完美 |
| 平均事务耗时 | 680ms | 420ms |
| 数据回滚次数 | 每天约 3~5 次 | 每周不到 1 次 |
更关键的是,现在每次促销活动再也不怕“崩”了,研发团队的焦虑指数也降了不少 😄。
七、我的经验和建议
做分布式事务不是一锤子买卖,也不是某个技术组件一加就能解决的事,它是架构设计、编码习惯、运维能力等多个维度的综合体现。
以下几点是我在这趟“长征”中学到的重要经验,分享给你:
✅ 技术选型要因地制宜
不要盲目追求某种“银弹”,像 Seata 这类工具虽然好用,但也存在性能瓶颈和复杂度。对于简单场景,也可以考虑用“本地事务表+定时对账”的方式来做兜底。
✅ 接口设计要统一标准
不管是 Dubbo 还是 REST,都要定义清晰的错误码、幂等标识、事务上下文传递机制,这对后续扩展至关重要。
✅ 异常处理要做得体面
无论采用哪种分布式事务方案,都无法做到 100% 成功,务必设置完善的补偿机制和异常记录手段。出了问题要能快速定位,而不是靠人肉排查日志。
✅ 日志和监控不能省
每一步事务的状态、请求参数、返回结果都要完整记录,最好接入链路追踪系统(比如 SkyWalking、Zipkin)。只有看得到,才能治得好。
✅ 性能也要兼顾
像 Seata 的 AT 模式,在写压力较大的系统里可能会成为瓶颈。可以结合业务特点做一些优化,比如只在必要路径启用事务,或者把部分非关键操作做成异步化。
八、小结一下
分布式事务从来都不是一件轻松的事情,它考验的是你的架构能力和工程素养。在我亲身经历的这次实践中,我深刻体会到:
“所谓成熟,不是不犯错,而是在一次次踩坑中找到了属于自己的节奏。”
如果你正在面对这类问题,或者正准备踏上这条路,希望我的这段经历能给你一点点帮助和信心。毕竟我们都是一边踩坑一边成长的程序员,不是吗?
欢迎留言交流,一起探讨你在分布式事务方面的实战经验!

评论 0