分布式事务实战:从“数据不一致”到“稳如老狗”
背景介绍

我在一家电商平台做后端开发,主要负责交易系统和库存系统的对接。平台随着业务发展,单体架构已经承载不了日益增长的流量和复杂度,于是我们开始逐步进行微服务拆分。
在一次促销活动上线前的压力测试中,我们发现了一个严重的问题:用户下单时,订单创建了但库存没扣减,或者反过来。
这在实际运营中是不能接受的——轻则造成超卖,重则影响用户体验、品牌信誉受损。而这种问题,背后的核心原因就是我们使用了多个独立服务(订单服务、库存服务),他们各自维护自己的数据库,这就涉及到了“分布式事务”。
这篇文章就从这个真实项目出发,聊聊我是怎么一步一步处理这个问题的,中间踩过的坑、学到的经验,希望对正在或即将面对类似挑战的同学有所帮助。
遇到的挑战

我们的核心问题是:
在用户提交订单时,需要同时完成两个操作:
- 订单服务新增订单记录
- 库存服务扣减对应商品库存
这两个服务各自使用不同的数据库,并且部署在不同的节点上。在并发高、网络不稳定的环境下,很容易出现以下几种情况:
- 下单成功但未扣库存:导致后续其他用户也能下单,超卖。
- 库存已扣但下单失败:商品被错误锁定,影响销售。
- 最终状态不确定:比如调用超时,两边都处于待定状态,后续难以恢复。
最开始我们用了最简单的方式处理:本地事务 + 顺序调用,即先插入订单记录,再调用库存服务 API 扣减库存。一旦其中一个步骤出错,就尝试回滚另一个。但这种方式在复杂场景下几乎不可靠。
我们的选择与解决方案

为了确保强一致性,我们考虑了几种常见的分布式事务方案,最终选定了 Saga 模式 + 异步补偿机制 的组合方式。
为什么选择 Saga?
- TCC(Try-Confirm-Cancel)虽然一致性更强,但实现复杂,对现有代码侵入性高。
- XA 协议性能差,不适合高并发。
- Seata 的 AT 模式当时还在试用阶段,团队经验不足,风险较高。
- 最终我们选了相对容易落地、扩展性强的 Saga 模式。
Saga 是一种基于补偿机制的长周期事务解决方案。它通过将整个流程拆分为一系列本地事务,每个事务完成后记录日志,若其中某一步失败,则执行对应的逆向操作来补偿。
我们的实现思路
我们将整个下单过程拆分为几个关键步骤:
| 步骤 | 描述 | 补偿动作 |
|---|---|---|
| Step 1 | 创建订单(本地事务) | 删除订单 |
| Step 2 | 扣减库存(远程调用库存服务) | 增加库存 |
整个过程通过一个状态机引擎控制流程流转,失败时触发回滚补偿。
同时,我们引入了以下几个关键设计:
- 事务日志表:记录每一步事务的状态,便于故障恢复。
- 幂等控制:保证多次请求不会重复扣减库存或重复创建订单。
- 异步补偿队列:当同步失败时,将任务扔进消息队列,由后台工作线程异步处理。
关键代码片段与设计细节
1. 状态定义
public enum TransactionStep {
CREATE_ORDER,
DEDUCT_STOCK,
CANCEL_ORDER,
REVERT_STOCK
}
2. Saga协调器伪代码
public class SagaCoordinator {
private OrderService orderService;
private InventoryService inventoryService;
private TransactionLogService transactionLogService;
public void executePlaceOrderSaga(String orderId, String productId, int quantity) {
try {
// Step 1: 创建订单
orderService.createOrder(orderId, productId, quantity);
transactionLogService.logStep(orderId, TransactionStep.CREATE_ORDER, true);
// Step 2: 扣减库存
boolean stockResult = inventoryService.deductStock(productId, quantity);
if (!stockResult) {
throw new RuntimeException("库存扣减失败");
}
transactionLogService.logStep(orderId, TransactionStep.DEDUCT_STOCK, true);
} catch (Exception e) {
// 触发补偿逻辑
handleRollback(orderId);
}
}
private void handleRollback(String orderId) {
List<TransactionLog> logs = transactionLogService.getLogsByOrderId(orderId);
for (TransactionLog log : logs) {
switch(log.getStep()) {
case DEDUCT_STOCK:
inventoryService.revertStock(log.getProductId(), log.getQuantity());
break;
case CREATE_ORDER:
orderService.cancelOrder(orderId);
break;
}
}
}
}
3. 数据库事务日志结构(简化)
CREATE TABLE transaction_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id VARCHAR(36),
step VARCHAR(50),
success BOOLEAN,
product_id VARCHAR(50),
quantity INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
开发与上线过程中遇到的坑
1. 幂等控制不到位导致重复扣减库存
初期没有做良好的幂等判断,结果一次超时重试导致同一个订单两次扣减库存。解决方法:
- 在库存服务中加入唯一ID校验(例如 orderId + productId)
- 使用 Redis 记录最近一段时间的已处理请求 ID
if (redis.exists("processed_" + orderId + "_" + productId)) {
return Result.success(); // 已处理
}
redis.setex("processed_" + orderId + "_" + productId, 24 * 60 * 60, "1");
2. 网络波动导致的补偿延迟
某个高峰期 Saga 协调器未能及时执行补偿逻辑,导致部分订单状态异常。后来引入 RabbitMQ 做异步回调:
- 将失败事务写入 MQ
- 异步 Worker 从 MQ 拉取任务并执行补偿
这样可以避免阻塞主线程,也增强了容错能力。
3. 事务日志丢失引发的“悬空事务”
早期由于没有落盘机制,事务日志存在内存中,发生异常重启后无法继续执行补偿流程。后来改为每次事务变动都落库,并增加定时扫描任务去补漏。
实施效果与收益
上线后,我们通过压测和监控验证了这套机制的有效性:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 下单成功率 | ~87% | ~99.95% |
| 超卖率 | ~0.1% | 接近于0 |
| 故障恢复时间 | 几乎无自动修复 | 通常 < 5分钟 |
| 日志可追踪性 | 不支持 | 可追溯每个步骤 |
最关键的是,系统在大促期间表现稳定,没有出现大规模的数据不一致问题,运维成本也明显降低。
经验总结与建议
结合这次经历,我有一些心得体会想分享给你:
✅ 技术选型要贴合业务需求
Saga 不是最强一致性方案,但它足够简单,在我们项目中是一种合适的妥协。不是所有场景都需要 100% 的 ACID,重点是找到“业务容忍”的平衡点。
✅ 幂等性必须贯穿始终
在任何需要调用外部接口的地方,都要加上幂等控制,哪怕只是临时性的缓存 ID,后期升级为更完善的机制也方便。
✅ 失败不是例外,而是常态
微服务之间通信永远会有失败可能,你的系统设计要能优雅地应对失败。Saga 中的补偿机制正是为这种“预期中的失败”设计的。
✅ 日志记录和可观察性非常重要
事务日志不仅是用来恢复的,更是定位问题的关键手段。推荐搭配 ELK、Prometheus 或者自研的日志追踪系统,做到“有据可查”。
✅ 监控+报警必不可少
我们在改造完成后加入了专门的事务监控模块,每当发现异常事务超过阈值时,系统会自动报警,甚至驱动自动修复流程。
写在最后的小感悟
说起来,这是我第一次真正意义上把“分布式事务”这个概念从理论变成现实。刚接触 Saga 的时候一头雾水,文档里的术语看得晕头转向。但当我一步步动手改代码、踩坑、调日志的时候,才真正理解了什么叫“分布式系统下的权衡”。
我想对刚开始接触微服务的你说一句:别怕复杂,别急着找“银弹”。 真正解决问题的能力,往往是在一次次重构、一点点优化中积累出来的。
希望这篇来自一线实战的真实分享,能在你前行的路上点亮一盏灯。如果你也有类似的故事,欢迎留言交流,互相学习。
技术这条路,我们一起走!

评论 0