分布式事务解决方案:从踩坑到落地的实战经验分享
分布式系统发展至今,早已不再是高并发项目的“可选项”,而是一个绕不开的话题。在我参与的一个电商系统重构项目中,我深刻体会到了这一点。我们原本是一个单一服务架构,所有交易、库存、物流都在一个库里面跑。后来随着业务增长,不得不进行微服务拆分——订单、支付、库存各自成独立服务,数据也开始分离。
这时候问题就来了:当用户下单付款时,订单要生成,库存需要扣减,支付要完成。这三个操作分布在三个服务里,用的是三个数据库实例。一旦某个环节失败(比如支付成功但库存不足),整个流程就必须回滚——这就是典型的分布式事务场景。
这篇文章讲的就是我们在解决这个需求过程中踩过的坑、总结出的最佳实践,以及最终实现的效果。
项目背景与挑战

我们的项目目标是构建一个支持日均百万级订单量的新一代电商平台。原有系统虽然稳定,但扩展性差、部署复杂、故障隔离能力弱。为了应对这些问题,我们决定采用微服务架构,并将核心模块拆分为:
- 订单中心:负责订单创建、状态变更等
- 库存中心:处理商品库存增减、锁定等
- 支付中心:与第三方支付平台对接,完成支付流程
每个服务中心都有自己的数据库,通过 RESTful API 或 gRPC 相互调用。
一开始我们尝试在业务逻辑中手动做补偿机制,比如先扣库存,再创建订单,最后调用支付。如果其中一步失败,就反向调用前一步接口做撤销。结果上线后没几天,就开始出现数据不一致的问题:
- 用户支付成功了,但订单没建出来;
- 订单创建了,库存扣减了,但支付失败了导致没人退款;
- 消息队列消费失败导致回调丢失,系统永远卡在中间状态...
这些都属于典型的分布式事务问题,传统的本地事务(ACID)无法解决跨服务的数据一致性问题,必须引入一种有效的事务控制机制。
解决方案设计与技术选型

为了解决这个问题,我们调研了几种主流的分布式事务方案:
| 方案 | 特点 | 是否适合 |
|---|---|---|
| 两阶段提交(2PC) | 强一致性,但性能差,存在单点故障风险 | ❌ 不适合互联网高并发场景 |
| 三阶段提交(3PC) | 改进版,解决了部分阻塞问题 | ❌ 实现复杂,依然依赖协调者 |
| TCC(Try-Confirm-Cancel) | 异步、柔性事务,需自己写补偿逻辑 | ✅ 合适,但开发成本高 |
| Saga模式 | 长事务编排模型,易于理解 | ✅ 可行,但状态机维护复杂 |
| 消息队列 + 最终一致性 | 通过事务消息异步处理 | ✅ 轻量,但对业务侵入性强 |
| Seata框架 | 基于 AT 模式的分布式事务中间件 | ✅ 推荐,但初期版本稳定性存疑 |

考虑到我们团队的技术栈是 Spring Cloud + MyBatis + MySQL + RabbitMQ,同时追求一定的易用性和开发效率,最终我们选择了结合使用 TCC 模式 + Seata 的 AT 模式 + 半消息机制 来构建多层防护体系。
我们做了如下的架构设计:
+-------------------+
| 下单请求 |
+---------+---------+
|
v
+---------+---------+
| 下单服务入口 |
| - 参数校验 |
| - 写入预订单状态 |
+---------+---------+
|
v
+---------+---------+ +------------------+
| 库存中心 |<-->| Try: 锁定库存 |
+---------+---------+ +------------------+
|
v
+---------+---------+ +------------------+
| 支付中心 |<-->| Try: 冻结金额 |
+---------+---------+ +------------------+
|
v
+---------+---------+
| 真实下单事件触发 |
| - 发送 RabbitMQ |
+---------+---------+
|
v
+---------+---------+
| 订单中心 |
| - 创建真实订单 |
| - 状态置为已支付 |
+---------+---------+
整体流程如下:
- 用户发起下单请求;
- 下单服务写入一个“预订单”(状态为待支付);
- 先去库存服务
Try一下,比如锁定某个商品的库存; - 再去支付中心冻结用户的账户余额(相当于预留资源);
- 如果两个操作都成功,就发一条消息给订单服务,正式创建订单;
- 此时如果任何一个步骤失败,则进入 Cancel 阶段;
- 如果订单创建成功,则通知其他服务 Confirm 提交;
- 所有资源最终释放,流程结束。
这种模式的优势是:
- 没有阻塞等待,性能更高;
- 可以根据业务场景灵活控制回滚粒度;
- 可以通过日志和重试机制自动修复大部分异常;
- 对原有服务结构侵入较小。
代码实现与关键配置

下面是基于 Spring Boot 和 Seata 的一个简化示例代码片段:
1. 定义全局事务注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GlobalTransactional {
}
2. 在下单接口中启动全局事务
@RestController
@RequestMapping("/order")
public class OrderController {
@GlobalTransactional
@PostMapping("/create")
public Response createOrder(@RequestBody CreateOrderRequest req) {
// 1. 写入预订单
orderService.createPreOrder(req);
// 2. 调用库存 Try
inventoryClient.lockInventory(req.getProductId(), req.getCount());
// 3. 调用支付 Try
paymentClient.freezeBalance(req.getUserId(), req.getTotalPrice());
// 4. 发起确认或取消
if (lockSuccess && freezeSuccess) {
sendCreateRealOrderEvent(req);
} else {
rollback();
}
return Response.success();
}
}
3. 在各服务中实现 Try/Confirm/Cancel 方法
以库存服务为例:
@Service
public class InventoryServiceImpl implements InventoryService {
@Override
public void lockInventory(String productId, int count) throws Exception {
// 减库存前检查是否足够
if (currentStock < count) {
throw new NotEnoughStockException();
}
// 更新库存表,status = locked
inventoryMapper.updateStock(productId, count, "locked");
}
@Override
public void confirmLock(String orderId) {
// 把锁库存的状态改成已成交
inventoryMapper.confirmLock(orderId);
}
@Override
public void cancelLock(String orderId) {
// 回退之前锁定的库存
inventoryMapper.releaseLock(orderId);
}
}
4. RabbitMQ 消费端保证最终一致性
对于某些场景,我们采用“半消息机制”,即订单创建成功之后才真正扣款/发货,确保即使 MQ 消费失败也不会引发脏数据:
@RabbitListener(queues = "real_order_created")
public void handleOrderCreated(Message message, Channel channel) throws IOException {
try {
OrderDTO dto = parse(message.getBody());
paymentService.deduct(dto.getUserId(), dto.getTotalPrice());
deliveryService.schedule(dto.getOrderNo());
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
log.error("消费失败", e);
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true); // 重新放回队列
}
}
踩坑经历与解决方案
这一套流程看似合理,但在实际开发中我们还是踩了不少坑:
❗️ 1. 幂等性处理不到位
我们最开始没有统一定义幂等 ID,在网络抖动导致重复请求时,出现了重复下单问题。例如用户点击下单多次或者支付超时重试,会导致多个订单被创建。
解决办法:统一要求每次请求带上
requestId,并在服务侧记录该 ID 是否已经处理过。
❗️ 2. Confirm/Cancel 未正确执行顺序
早期我们曾遇到 Confirm 比 Try 更早到达的情况(因为异步网络问题),导致 Confirm 操作找不到对应上下文。
解决办法:使用唯一的事务 ID 绑定整条链路,服务端判断状态后再决定执行哪一步。
❗️ 3. Seata 配置不当引发死锁
我们一开始启用了 Seata 的 AT 模式来做事务控制,但由于没有设置好 undo_log 表索引,导致在大批量操作下频繁加锁,系统响应变慢甚至不可用。
解决办法:优化 Seata 存储引擎配置,增加合理的索引,避免长事务拖慢性能。
❗️ 4. 重试机制不合理导致无限循环
Cancel 阶段如果没有做好重试策略,可能会因为下游服务短暂不可用而导致流程中断。
解决办法:使用指数退避算法重试;超过一定次数后转人工处理,避免系统雪崩。
实施效果与收益
这套方案上线半年多以来,系统运行平稳,主要指标如下:
- 日均处理订单量约 90W 单;
- 整体分布式事务成功率保持在 99.97% 以上;
- 数据一致性错误率下降至几乎为零;
- 运维告警大幅减少,故障定位更清晰;
- 新服务接入成本降低,只需实现 Try/Confirm/Cancel 三个方法即可加入事务链路。
更为重要的是,由于采用了“柔性事务”的思路,系统的容错能力和伸缩性大大提升。即使是高峰期的流量冲击,也能快速恢复,不再出现以往那种“一笔订单卡住全站”的问题。
经验总结与建议
如果你也在做类似的分布式事务设计,这里是我总结的一些经验:
🔑 1. 根据业务需求选择合适的事务模型
并不是每个场景都需要强一致性。比如优惠券发放可以接受一定程度的最终一致性,但支付和库存一定要精确控制。
🧭 2. 一切都要围绕“幂等”设计
无论是接口调用还是 MQ 消费,都得考虑幂等处理,否则一定会出问题。尤其是在分布式环境下。
🛠 3. 技术不是万能的,架构设计更重要
Seata 很强大,但如果业务没有良好的分层、状态管理、日志追踪,光靠技术框架也救不了你。要让事务可控、可查、可修复。
📈 4. 多做压测,尤其关注极端情况
我们曾在灰度环境中用 JMeter 模拟了 10W 并发请求,发现了不少隐藏问题。提前暴露比线上爆炸更好。
🧩 5. 组合使用多种机制才是王道
不要试图用一种方案解决所有问题。像我们这样组合使用 TCC + 半消息 + Seata,其实是一种更加稳健的做法。
尾声:一次深夜排查的小故事
还记得一次凌晨两点,突然收到告警说有几个订单库存没回滚。我赶紧爬起来远程登录服务器,打开日志查看。
原来是某个 Cancel 请求没能及时送达库存服务,系统一直以为还在执行中。后来我发现是因为当时 Redis 缓存失效,导致 Cancel 请求的路由信息找错了节点。
那次事件后,我特别强调要在 Cancel 阶段加上兜底补偿机制。哪怕服务重启也能继续完成任务,而不是停留在半死不活的状态。
这也让我明白了一个道理:分布式事务不是写完就结束的事情,它是一场持久战,需要完善的监控、报警、自动修复机制做支撑。而这,才是真正保障系统稳定的底线。
希望这篇真实经历分享对你有所帮助。如果你也在做类似架构改造,欢迎留言交流。一起在分布式的世界里越走越稳!

评论 0