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

开发者小宇宙
2025-06-29 03:38
阅读 588

背景介绍

背景介绍

我在一家电商平台做后端开发,主要负责交易系统和库存系统的对接。平台随着业务发展,单体架构已经承载不了日益增长的流量和复杂度,于是我们开始逐步进行微服务拆分。

在一次促销活动上线前的压力测试中,我们发现了一个严重的问题:用户下单时,订单创建了但库存没扣减,或者反过来。

这在实际运营中是不能接受的——轻则造成超卖,重则影响用户体验、品牌信誉受损。而这种问题,背后的核心原因就是我们使用了多个独立服务(订单服务、库存服务),他们各自维护自己的数据库,这就涉及到了“分布式事务”。

这篇文章就从这个真实项目出发,聊聊我是怎么一步一步处理这个问题的,中间踩过的坑、学到的经验,希望对正在或即将面对类似挑战的同学有所帮助。


遇到的挑战

遇到的挑战

我们的核心问题是:

在用户提交订单时,需要同时完成两个操作:

  • 订单服务新增订单记录
  • 库存服务扣减对应商品库存

这两个服务各自使用不同的数据库,并且部署在不同的节点上。在并发高、网络不稳定的环境下,很容易出现以下几种情况:

  1. 下单成功但未扣库存:导致后续其他用户也能下单,超卖。
  2. 库存已扣但下单失败:商品被错误锁定,影响销售。
  3. 最终状态不确定:比如调用超时,两边都处于待定状态,后续难以恢复。

最开始我们用了最简单的方式处理:本地事务 + 顺序调用,即先插入订单记录,再调用库存服务 API 扣减库存。一旦其中一个步骤出错,就尝试回滚另一个。但这种方式在复杂场景下几乎不可靠。


我们的选择与解决方案

服务器部署方案-1

为了确保强一致性,我们考虑了几种常见的分布式事务方案,最终选定了 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

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝