分布式事务解决方案:一次真实项目中的最佳实践分享

Prompt造梦师
2025-06-29 09:34
阅读 489

背景介绍:为什么分布式事务如此重要

背景介绍:为什么分布式事务如此重要

2021年,我参与了一个电商平台的重构项目,目标是将原本单体架构的服务拆分为多个微服务,以提升系统的扩展性和维护性。我们按业务领域划分了用户中心、订单中心、库存中心、支付中心等子系统,分别部署在不同的服务器上。

一开始一切都挺顺利的,直到我们上线第一个完整业务流程——下单操作的时候,问题就来了。

下单过程包括三个核心动作:

  • 扣减库存(调用库存服务)
  • 创建订单(调用订单服务)
  • 调用第三方支付接口完成支付(调用支付服务)

这三步如果其中任意一个失败,整个流程都应该回滚。但因为是跨服务调用,本地事务已经无法覆盖这些操作了。于是我们遇到了一个典型的分布式事务场景

这个问题困扰了我们将近两周时间,最后通过引入一套成熟的方案才得以解决。今天我想把这些经验拿出来和大家分享一下,希望能帮助到同样面临这类问题的朋友。


遇到的挑战:不是所有异常都能被 catch 住

遇到的挑战:不是所有异常都能被 catch 住

最初我们尝试使用最简单的“伪事务”方式来处理,比如:

try {
    inventoryService.decreaseStock(productId, quantity);
    orderService.createOrder(userId, productId, quantity);
    paymentService.charge(amount);

} catch (Exception e) {
    // 记录日志并补偿
    logger.warn("Order failed, need manual compensation");
}

但实际上,这种方式根本扛不住生产环境的复杂情况。例如:

  • 支付成功了,但网络超时导致我们以为失败了,这时候要不要退款?
  • 订单创建成功,但下游服务调用失败,如何清理数据?
  • 日志写入和调用不同步,出现状态不一致怎么定位?

我们还试过同步重试机制,结果发现一旦某个环节频繁出错,就会导致系统雪崩。甚至有一次,为了快速恢复数据一致性,我们手动写了几十条 SQL 来补数据……

那次之后我们就意识到:不能靠蛮力解决问题,必须找到一种既稳定又可控的分布式事务方案。


我们选择的方案:Seata + Saga 模式实战

我们选择的方案:Seata + Saga 模式实战

我们在技术选型阶段评估了几种主流的分布式事务方案:

方案 优点 缺点 使用难度
两阶段提交(2PC) 理论完备 性能差,存在单点故障风险 较高
TCC(Try-Confirm-Cancel) 可控性强,适合关键业务 开发成本高,需要预留反向逻辑
Saga 模式 流程清晰,支持长周期业务 不保证 ACID,需关注补偿失败
最大努力通知 实现简单 几乎不保证一致性

结合我们系统的实际情况(电商下单链路不算特别复杂、允许一定异步补偿、对开发效率有要求),最终决定采用 Seata 的 Saga 模式

Saga 模式的工作机制简述

Saga 是一种经典的分布式事务模型,核心思想是:每一步都有对应的补偿操作,如果某步失败,则依次执行前面步骤的逆向操作来进行回滚。

举个例子,下单业务可以定义如下状态机:

start -> decreaseInventory(Try) -> createOrder(Try) -> chargePayment(Try)
                                 ↑                          ↓
                                 └───────rollbackToCreateOrder──┘

每一步执行完成后,会记录当前状态到状态机引擎中。Seata 的 Saga 模式利用 JSON 定义状态流转图,并交由 StateMachineEngine 去驱动整个事务的执行与补偿。


代码实践:从状态图配置到服务整合

第一步:定义状态流转图

我们使用 JSON 描述每个节点的行为及其补偿方法:

{
  "name": "orderSaga",
  "states": [
    {
      "name": "decreaseInventory",
      "type": "SERVICE_TASK",
      "serviceName": "inventoryService",
      "serviceMethod": "tryDecreaseInventory",
      "compensateState": "cancelInventory"
    },
    {
      "name": "cancelInventory",
      "type": "COMPENSATE"
    },
    {
      "name": "createOrder",
      "type": "SERVICE_TASK",
      "serviceName": "orderService",
      "serviceMethod": "tryCreateOrder",
      "compensateState": "deleteOrder"
    },
    {
      "name": "deleteOrder",
      "type": "COMPENSATE"
    },
    {
      "name": "chargePayment",
      "type": "SERVICE_TASK",
      "serviceName": "paymentService",
      "serviceMethod": "tryCharge",
      "compensateState": "refund"
    },
    {
      "name": "refund",
      "type": "COMPENSATE"
    }
  ],
  "startState": "decreaseInventory",
  "endStates": ["success", "failure"]
}

负载均衡配置-1

这个文件上传到 Seata Server 以后,就可以通过唯一标识符(比如 stateMachineName)触发执行。

第二步:编写服务接口

inventoryService 为例:

@Component
public class InventoryService {

    @SagaAction(compensable = true)
    public void tryDecreaseInventory(@ContextVariable(name = "productId") Long productId,
                                     @ContextVariable(name = "quantity") Integer quantity) {
        // 减库存逻辑,这里是伪代码示例
        int updated = inventoryRepository.decreaseStock(productId, quantity);
        if (updated == 0) {
            throw new RuntimeException("库存不足");
        }
    }

    public void cancelInventory(@ContextVariable(name = "productId") Long productId,
                                @ContextVariable(name = "quantity") Integer quantity) {
        // 补偿动作:加回库存
        inventoryRepository.increaseStock(productId, quantity);
    }
}

这里需要注意的是:

  • @SagaAction 注解用于标记该方法是 saga 模式中的可执行节点;
  • 方法入参需加 @ContextVariable 注解以便传递上下文;
  • 各个节点之间的状态和参数都会被 Seata 自动持久化记录。

第三步:触发状态机执行

在下单入口处,调用 Seata 提供的状态机 API 触发执行:

StateMachineEngine engine = SpringUtil.getBean(StateMachineEngine.class);

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

// 触发执行
StateInstance result = engine.startWithBusinessKey("orderSaga", null, null, context);

踩坑经历:那些深夜里调试出来的经验

API接口文档-2

说到底,再好的工具也不能避免我们在实际开发中踩坑。以下是几个比较典型的问题及解决方案:

问题一:状态机执行卡住了,不知道进展如何

我们初期上线的时候遇到一个诡异现象:有些订单状态长时间停留在“进行中”,后台看不到任何异常日志。

后来排查发现,Seata 的状态机会把中间状态记录到数据库中,但如果执行过程中抛出未捕获的异常,会导致状态停滞。因此我们做了几点改进:

  • 引入全局异常拦截器统一捕获异常并打日志;
  • 在 Saga 执行前添加唯一 ID 关联业务单据;
  • 在数据库增加定时任务定期清理超时状态机实例。

问题二:补偿函数执行失败,没有自动重试机制

有时候补偿函数也会失败,比如网络抖动导致的支付回滚失败。

我们的应对策略是:

  • 对补偿函数也加入重试机制(例如 3 次指数退避);
  • 引入独立的消息队列作为容灾通道,当主流程失败时,把订单 ID 投递到 MQ 中进行二次补偿;
  • 设置报警机制,一旦超过阈值立即告警,人工介入。

问题三:状态图 JSON 格式错误难排查

Seata 加载状态图的时候并不会严格校验格式,有时候少个逗号或者字段拼写错误,会导致运行时报错,排查起来非常麻烦。

为此,我们做了一个小工具脚本,在 CI 阶段提前校验所有的 .json 文件格式是否符合规范。后来还集成到了 Git Hook 中,大大减少了因格式问题导致的发布事故。


实施效果与收益总结

自从这套 Saga 分布式事务方案正式上线后,整体稳定性得到了显著提升:

  • 系统下单流程成功率从原来的 92% 提升到了 99.7%
  • 数据不一致率大幅下降,从每天几十笔减少到几乎为零
  • 运维同学再也不用半夜爬起来“修数据”了 😂
  • 接口响应速度保持在合理范围内(控制在 300ms 内)

此外,这套方案还为我们后续的其他业务模块提供了一个标准模板,比如退货流程、积分兑换等场景都复用了类似的架构,节省了不少开发成本。


给读者的一些建议与注意事项

如果你现在正准备或已经在做分布式事务相关的功能,以下是我个人的一些经验和建议:

  1. 不要一开始就追求完美,先解决80%的问题再说
    Saga 模式虽然不是强一致性方案,但它足够成熟且具备良好的灵活性。对于大多数业务场景而言,这是性价比最高的选择。

  2. 始终保留补偿路径的幂等性设计
    补偿操作可能多次执行,一定要加幂等判断。比如支付退款接口,必须检查是否已退过款。

  3. 不要只依赖框架,要有兜底机制
    即使用了 Seata,也要结合消息队列、定时补偿任务等方式形成闭环。毕竟线上环境千变万化,多一层保险总没错。

  4. 日志要详细,追踪要方便
    每次 Saga 执行都要带上请求 ID 或订单号,方便定位问题。推荐使用 MDC + 日志平台实现全链路追踪。

  5. 监控必不可少
    监控补偿失败次数、状态机失败率、执行耗时等指标,及时预警,做到心中有数。


结语:技术方案的本质是服务于业务

这篇文章并不是单纯地讲一个技术方案,而是想告诉大家:在真实的工程实践中,技术从来都不是孤立存在的。它必须服务于业务,兼顾运维体验和团队能力。

我们之所以选择 Saga 模式,是因为它在当时最适合我们的业务现状;如果换一个更复杂的场景,也许我们会选择 TCC,或者未来随着 Flink、Event Sourcing 技术的成熟,再升级为基于事件驱动的一致性方案。

最重要的是:不要盲目追新,适合自己才是最好

希望我的这段经历能够帮到你。如果你在实践中也遇到过类似问题,欢迎留言交流,我们一起成长!


如需获取完整的 Seata Saga 示例代码,可以在评论区留下邮箱,我会整理发送。

评论 0

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