分布式事务解决方案:一次真实项目中的“生死较量”

CI掉线了
2025-06-17 16:31
阅读 408

引言

记得去年年底,我们公司准备上线一个新业务线:一套面向供应链管理的订单结算系统。整个系统涉及到多个微服务模块,比如订单服务、库存服务、支付中心和结算中心。这些模块各自维护着自己独立的数据库,而且数据一致性要求极高。

当时我作为后端主程负责这块系统的核心开发,其中一个最棘手的问题就是——分布式事务。在高并发下单场景下,如何保证多个服务的数据要么一起成功,要么一起失败?一开始我们天真地用了本地事务+消息队列的方式,结果一压测就翻车了,各种状态不一致、资金异常、订单残留……简直惨不忍睹。

这篇文章想分享一下我们在实际项目中一步步摸索出来的分布式事务最佳实践方案,包括遇到的坑、踩过的雷,以及最终落地的效果。希望对你们有所帮助。


问题描述:多服务之间的数据一致性难题

在这个项目中,核心流程是这样:

用户提交订单 -> 减少库存 -> 创建订单 -> 发起支付 -> 支付完成后触发结算 -> 更新订单状态为已结算。

这六个步骤分布在四个不同的微服务里,并且每个服务都有自己的数据库。例如:

  • 订单服务操作订单表
  • 库存服务扣减库存记录
  • 支付中心发起支付请求
  • 结算中心处理分账与对账逻辑

起初我们尝试用本地事务包裹住调用链路,比如订单创建成功后再调库存服务接口减库存,如果失败则回滚本地事务。但这种做法在异步调用或网络不可靠时根本扛不住。

举个例子:订单创建成功,但减库存失败,这时候前端已经提示用户下单成功,后续又得通过补偿机制手动干预。用户体验很差,运维成本也很高。

更复杂的情况是,当支付中心返回支付成功后,结算中心还没来得及处理,系统就崩溃了,导致资金流对不上。这种情况,在金融类系统中是绝对不能容忍的。


解决方案:Seata + Saga 模式解决长周期事务

经过多次技术选型和讨论,我们最终决定引入 Seata(一款由阿里开源的分布式事务中间件),并采用其中的 Saga 模式 来应对这个复杂的业务场景。

Saga 模式适用于:

  • 长周期事务(比如支付完成后几天才进行结算)
  • 多服务之间需要保证最终一致性的场景
  • 可容忍短暂不一致,但最终必须达成一致

我们的设计思路如下:

  1. 把整个业务流程拆解成多个原子动作(Action)
  2. 每个 Action 对应一个子服务的接口调用(如 createOrder、deductStock 等)
  3. 若某个 Action 执行失败,则根据业务定义一系列对应的补偿 Action(如 cancelOrder、increaseStock)
  4. 利用 Seata 的 Saga 模式自动编排整个流程,并支持人工介入查看状态和重试

负载均衡配置-1

这样一来,即使某一步骤失败,系统也能自动执行对应的补偿操作,避免出现“脏数据”。


代码实践:关键配置和实现细节

接下来我分享一些关键代码和配置,都是在生产环境中验证过的内容。

1. 添加 Seata 依赖(Spring Boot 项目)

<!-- seata starter -->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.6.1</version>
</dependency>

<!-- saga 模块需要额外添加 -->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-saga-starter</artifactId>
    <version>1.6.1</version>
</dependency>

2. Saga 编排文件定义(JSON格式)

我们通过 JSON 文件定义 Saga 的事务流程:

{
  "name": "orderBusinessWorkflow",
  "startState": "CreateOrderState",
  "states": {
    "CreateOrderState": {
      "type": "action",
      "action": "createOrderAction",
      "compensationAction": "cancelOrderAction",
      "next": "DeductStockState"
    },
    "DeductStockState": {
      "type": "action",
      "action": "deductStockAction",
      "compensationAction": "increaseStockAction",
      "next": "PaymentState"
    },
    "PaymentState": {
      "type": "action",
      "action": "initiatePaymentAction",
      "compensationAction": "refundPaymentAction",
      "next": "SettlementState"
    },
    "SettlementState": {
      "type": "action",
      "action": "doSettlementAction",
      "compensationAction": "reverseSettlementAction",
      "end": true
    }
  }
}

这个 JSON 描述了一个完整的事务链条。每个状态对应一个具体的 Action 方法和对应的补偿方法。

3. 编写 Action 实现类

我们以 createOrderAction 为例:

@Component
public class CreateOrderAction {

    @Autowired
    private OrderService orderService;

    public Map<String, Object> doAction(Map<String, Object> context) throws Exception {
        OrderDTO orderDTO = (OrderDTO) context.get("orderDTO");
        String orderId = orderService.createOrder(orderDTO);
        context.put("orderId", orderId); // 用于后续补偿逻辑
        return context;
    }

    public Map<String, Object> compensate(Map<String, Object> context) throws Exception {
        String orderId = (String) context.get("orderId");
        orderService.cancelOrder(orderId);
        return context;
    }
}

每个 Action 必须同时实现 doActioncompensate 方法,并注册到 Spring 容器中。

4. 启动 Saga 事务

启动事务只需要一行代码:

Map<String, Object> context = new HashMap<>();
context.put("orderDTO", orderDTO);

SagaMachineEngine sagaMachineEngine = applicationContext.getBean(SagaMachineEngine.class);
sagaMachineEngine.startWithBusinessKey("orderBusinessWorkflow", "ORDER_123456", context);

这里的 businessKey 是业务唯一标识,方便日志追踪和后续补偿。


踩坑经验:Seata 并不是万能的

虽然最终我们成功上线了这套方案,但过程中也遇到了不少坑,这里总结几个典型的教训:

1. Saga 模式的局限性

Saga 并不提供真正的 ACID 事务,它只能保证最终一致性。所以在某些极端场景下,可能出现短时间内的状态不一致。比如支付完成但结算还没开始,这时用户看到的状态会有点“延迟”。为此我们在前端做了一些兜底逻辑,比如显示“待确认”状态。

2. Action 参数要尽量简单

我们最开始把一些复杂的对象传入到 context 中,结果在序列化时出错。建议只传递必要的基础类型字段,比如 id、金额等,复杂结构可以放到数据库中临时存储。

3. 网络超时和重试机制要考虑清楚

Seata 默认是不会重试的,所以我们要手动加上 fallback 机制。我们结合了 Hystrix 做了降级处理,确保在网络波动时不会无限等待。

4. 补偿逻辑要幂等!

这是最容易被忽视的一点。补偿动作有可能因为网络抖动等原因被多次调用,如果不做幂等处理,可能会导致负数库存、重复退款等问题。为此我们在每个 Action 中加了一个 redis 分布式锁 + 请求编号校验。


效果总结:从混乱走向稳定

这套 Saga + Seata 的分布式事务方案上线后,我们的整体事务成功率提升了 98% 以上,异常订单几乎清零。特别是之前那种“支付成功但订单没生成”的现象彻底消失,系统稳定性大幅提升。

另外,我们也从中积累了一套可视化的 Saga 状态追踪平台,可以通过 Seata 自带的日志系统快速定位事务失败原因。这对运维同学来说简直是福音。

最重要的是,老板再也不骂我们“交易流水对不上”了😂。


经验分享:给同行们的一些建议

结合这次经历,我有几点心得想送给正在处理类似问题的你:

1. 不要一开始就追求完美的强一致性

在分布式系统中,强一致性往往是以牺牲性能和可用性为代价的。很多时候“最终一致性 + 补偿逻辑”才是更合理的策略。特别是金融类系统,只要保障最终账平即可。

2. 技术方案要贴合业务场景

比如你的业务有没有长周期流程?是否允许短时间不一致?这些都会直接影响你选择哪种事务模式(AT、TCC、Saga、XA)。不要盲目照搬,适合才是最好的。

3. 日志和可观测性至关重要

任何分布式事务系统都离不开完善的日志体系和监控手段。建议尽早集成 ELK 或者 Prometheus,方便排查问题。你可以通过 Saga 提供的 REST API 查看每个事务的当前状态和执行路径。

4. 幂等性设计一定要提前考虑

不仅是分布式事务,在整个后端架构中,幂等性都应该是一个基本的设计原则。尤其是对外部接口的调用,务必要做到“同一个请求无论执行多少次结果都一样”。

5. 分布式事务只是“最后的选择”

如果你的系统还能通过聚合服务或者合并数据库的方式来规避分布式事务,那一定优先这么做。毕竟再多的框架都无法完全弥补架构上的不合理设计。


写在结尾

这次项目让我深刻意识到:在分布式的世界里,没有银弹,只有不断权衡、不断演进的架构。

也许再过一年,我们会换成其他事务模型;也许会引入 Event Sourcing 或 CQRS 进一步提升系统的扩展性和可维护性。但我始终相信一句话:系统不是写完就结束,而是不断打磨、优化、迭代的过程

如果你也在面对分布式事务的困扰,不妨试试 Seata 的 Saga 模式。它不是最优雅的方案,但在实际落地中真的靠谱。

愿你在后端世界的征途中,越走越稳。共勉!

评论 0

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