分布式事务解决方案:最佳实践

慢慢写代码
2025-06-19 18:28
阅读 267

分布式事务解决方案:从踩坑到稳定落地的那些事

分布式事务解决方案:从踩坑到稳定落地的那些事

作为一名后端开发者,我在过去几年里一直在参与中大型系统的开发与架构优化。这些系统往往涉及多个服务模块,数据存储在不同的数据库实例甚至是不同类型的数据库中,这就不可避免地遇到了一个绕不过去的技术难题——分布式事务

今天我想结合最近参与的一个电商项目来聊聊我们是怎么一步步解决这个问题的。这中间我们踩了不少坑,也做了一些技术选型上的取舍。希望通过这篇文章,能给大家一些实际可行的思路和经验,少走些弯路。


项目背景:一次典型的跨服务下单场景

故事要从一个电商平台的核心流程说起——用户下单购买商品。这个场景看似简单,但背后其实涉及多个核心子系统之间的协作:

  • 订单中心负责创建订单
  • 库存中心负责扣减商品库存
  • 支付中心处理支付流程
  • 积分中心可能还要增加用户的积分

这几个系统都是独立部署、各自有各自的数据库,彼此之间通过 RPC 或 RESTful 接口调用交互。问题来了:如何保证在整个下单过程中,这些操作要么全部成功,要么全部失败?

我们最初的想法很朴素:用本地事务 + 手动补偿。也就是先下订单,然后依次调用库存和支付接口,一旦某一步失败,就回滚前面的操作。听起来没问题,但现实远比想象复杂得多。


真实挑战:不是所有失败都能及时感知

有一次压测时,我们发现当支付服务超时时,整个下单流程就会卡住,甚至出现“已发货未付款”的奇怪情况。更糟糕的是,在高并发下手动补偿根本追不上错误日志,导致最终一致性无法保障。

我们总结了几个关键痛点:

  1. 网络不稳定导致请求失败或延迟,服务间耦合严重
  2. 补偿逻辑需要人工实现,容易漏掉边缘情况
  3. 异步消息丢失导致状态不一致
  4. 事务边界不好控制,尤其在链路较长的情况下

这些问题告诉我们,手工管理分布式事务不是一个可持续的方向


技术选型思考:我们为什么选择 Saga 模式

面对这些问题,我们开始调研主流的分布式事务方案,包括两阶段提交(2PC)、TCC、Saga 模式、Seata 的 AT 模式等。

我们对比了几个方案的特点:

方案 特点 优点 缺点
2PC 强一致性 实现简单 性能差,存在单点故障风险
TCC Try - Confirm - Cancel 三阶段 控制灵活,性能好 业务改造成本高,代码侵入性强
Saga 本地事务 + 补偿机制 异步、高性能 需要设计补偿逻辑,可能不满足原子性
Seata AT 自动代理数据库事务 透明化事务管理 依赖数据库,不支持 NoSQL

最终我们选择了 Saga 模式,原因如下:

  • 我们的业务对一致性要求是最终一致性,而不是强一致性
  • 有些服务使用的是非关系型数据库(如 Redis 和 MongoDB),不支持 XA 协议
  • 业务模块已经相对独立,拆分清晰,便于实现补偿逻辑

方案设计与实现:基于事件驱动的 Saga 落地实践

我们采用的是一种基于消息队列的事件驱动方式,整体流程如下:

  1. 用户下单,订单服务开启本地事务生成订单
  2. 发送 OrderCreatedEvent 到 Kafka
  3. 库存服务监听该事件,尝试扣减库存:
    • 成功则发送 InventoryDeductedEvent
    • 失败则发送 InventoryFailedEvent
  4. 支付服务继续监听事件并执行支付
  5. 各个服务都保持状态机记录当前操作,并根据事件流转状态
  6. 如果某个环节失败,触发补偿动作(Cancel)

举个简单的例子,我们在订单服务中定义了一个状态字段:

public enum OrderStatus {
    CREATED, 
    INVENTORY_LOCKED, 
    PAID,
    CANCELLED;
}

每个事件都会推动状态流转:

if (event instanceof InventoryDeductedEvent) {
    order.setStatus(OrderStatus.INVENTORY_LOCKED);
} else if (event instanceof PaymentSuccessEvent) {
    order.setStatus(OrderStatus.PAID);
} else if (event instanceof InventoryFailedEvent || event instanceof PaymentFailedEvent) {
    order.setStatus(OrderStatus.CANCELLED);
    // 触发其他服务的 Cancel 操作
}

补偿逻辑我们也抽象成了标准接口:

public interface Compensator {
    void compensate(String businessId);
}

比如,订单取消时我们会调用库存服务的 API 来恢复库存:

@Override
public void compensate(String orderId) {
    log.info("Compensating inventory for order: {}", orderId);
    // 调用远程接口,或者发送命令到队列
    inventoryService.restoreStock(orderId);
}

我们还为补偿失败的情况加了一层重试机制和报警,确保不会漏掉任何异常。


关键代码片段分享

这里贴一下订单服务消费库存结果的消费者逻辑简化版:

@Component
@RequiredArgsConstructor
public class InventoryConsumer {

    private final OrderService orderService;

    @KafkaListener(topics = "inventory_result")
    public void handleInventoryResult(InventoryResultEvent event) {
        String orderId = event.getOrderId();
        switch (event.getType()) {
            case SUCCESS:
                orderService.updateStatusToInventoryLocked(orderId);
                // 继续发起支付操作
                break;
            case FAILED:
                orderService.handleOrderFailAndCancel(orderId);
                break;
        }
    }
}

以及补偿处理器的部分伪代码:

// 注册补偿器示例
compensatorManager.registerCompensator("order_cancellation", new OrderCancellationCompensator());

public class OrderCancellationCompensator implements Compensator {
    @Override
    public void compensate(String orderId) {
        log.warn("Start compensating order: {}", orderId);

        boolean success = false;
        int retryTimes = 3;
        while (!success && retryTimes-- > 0) {
            try {
                // 调用各服务取消订单相关资源
                inventoryService.recoverStock(orderId);
                paymentService.refund(orderId);
                pointsService.rollbackPoints(orderId);
                success = true;
            } catch (Exception e) {
                log.error("Compensation failed, retrying...", e);
                Thread.sleep(1000); // 简单重试
            }
        }

        if (!success) {
            alertService.sendCriticalAlert("Order compensation failed after multiple retries: " + orderId);
        }
    }
}

踩过的坑和教训总结

说实话,这条路并没有一开始想得那么顺利。我们在这个过程中也踩了不少坑:

坑一:消息重复消费和幂等问题

Kafka 的 At-Least-Once 模式会导致消息重复消费。我们在早期没意识到这点,导致同一个订单被多次扣减库存,甚至出现了负库存的乌龙事件。

解决方案:我们给每个业务操作加上了幂等 ID,并且在处理前先查询是否已经处理过。

if (deductionRecordRepository.existsByBusinessId(event.getOrderId())) {
    return; // 已处理,跳过
}

坑二:补偿链断裂

有时候某个服务因为宕机或网络抖动没有收到补偿通知,导致数据不一致。

解决方案:引入一个定时任务扫描异常订单,自动触发补偿;同时接入监控告警,第一时间发现问题。

坑三:状态机设计不当

最开始我们将所有状态都放在订单实体里,随着业务扩展,状态变得越来越多,难以维护。

解决方案:我们改为使用独立的状态机引擎(后来升级成 Spring StateMachine),解耦状态流转逻辑,提升可读性和可维护性。

坑四:异步处理丢失上下文信息

刚开始我们用 Kafka 发送事件,但部分服务拿到的 event 数据结构不一致,调试困难。

解决方案:统一事件结构,并引入 Avro 或 Protobuf 作为序列化协议,确保上下游的一致性。


落地效果和收益评估

这套 Saga 解决方案上线后,我们监控了几个月的数据:

  • 下单成功率提升了 7%
  • 平均处理延迟从 800ms 降低到 350ms
  • 系统吞吐量提高了近 2 倍
  • 最关键的是:数据一致性问题几乎彻底消失

更重要的是,我们的系统具备了良好的伸缩性和容错能力。即使某个服务短暂不可用,也不会影响整体流程,最多只是延迟,不会造成损失。


个人经验和建议

如果你也面临类似的分布式事务问题,我有几个建议供你参考:

✅ 优先明确业务一致性需求

不是所有场景都需要强一致性。弄清楚你的业务对数据一致性的容忍度,才能选对方案。

✅ 不要一开始就追求完美方案

分布式事务是一个复杂的领域,别试图一开始就找到“银弹”。可以从小规模业务入手,逐步演进。

✅ 尽早引入可观测性工具

像 Zipkin、Prometheus 这些工具一定要早点接入。出了问题才能快速定位是哪个服务卡住了。

✅ 日志要详细但不过载

关键节点打日志,最好带上 trace id,方便后续排查。但不要打太多无用的日志,反而影响性能。

✅ 用通用组件减少重复劳动

我们后来封装了一个 saga-starter 包,把状态管理、补偿逻辑抽象出来,其他服务只需配置即可复用。

✅ 时刻关注性能和可用性

尤其是在使用 Kafka 这样的异步管道时,注意分区设置、积压监控,避免雪崩。


写在最后:技术没有银弹,只有不断适应变化

分布式事务从来不是一道纯技术题,它涉及到业务流程设计、服务治理、运维保障等多个维度。在这个过程中,我们也不是一蹴而就,而是一步步在实践中摸索、修正和优化。

回顾这段旅程,我最大的感触就是:真正的工程落地从来不是靠一套完美的理论,而是通过一次次试错、踩坑、再调整出来的结果

如果你也在探索分布式事务的解法,不妨试试 Saga + 消息队列的组合。也许它不是最优解,但在大多数场景下,它确实够用了。

当然,技术总是在进步,未来我们也计划引入 Dapr 或者 Cloud Native 的新方案来进一步简化事务模型。但那又是另一个故事了。

祝你在构建高可靠系统的路上越走越稳。如果你有更好的实践经验,也欢迎一起交流!

评论 0

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