分布式事务解决方案:我的实战经验分享

高志华
2025-06-25 17:09
阅读 347

引言

引言

作为一名后端架构师,我在多个项目中都碰到过“分布式事务”这个老大难问题。尤其是在近几年微服务架构成为主流的背景下,系统拆得越来越细,但随之而来的是数据一致性保障的难题。今天我想结合自己亲身经历的一个项目,来聊聊我们是怎么解决分布式事务问题的,希望能给同样面临类似挑战的你一些启发。

项目背景

项目背景

事情要回到2021年我参与的一个电商订单中心重构项目。当时我们的系统已经从原来的单体应用拆分成了多个服务:用户服务、库存服务、支付服务、订单服务和物流服务。每个服务都有自己的数据库,彼此之间通过 RPC 和消息队列通信。

新业务需求来了一个组合下单流程,要求在同一个接口里完成以下几个动作:

  1. 扣减库存;
  2. 创建订单;
  3. 锁定账户余额;
  4. 推送消息到物流平台准备发货。

这四个操作分别落在不同的服务上,而且任何一个步骤失败都要回滚前面的操作,否则就会出现数据不一致甚至资损的问题。

一开始我们采用了一个看似“简单粗暴”的做法:本地事务 + 最终一致性补偿机制。也就是每步操作完成后都往各自的数据库写个“事务日志”,然后定时任务异步检查是否有未完成事务,并做补偿。刚开始还能应付小规模流量,但随着业务增长,这套方案逐渐暴露出了很多问题。

面临的挑战

面临的挑战

1. 补偿逻辑复杂,维护成本高

比如订单创建失败但库存已经扣减了,这时候需要反向调用库存服务进行加库存。但如果补偿请求因为网络问题没收到怎么办?有没有幂等处理?如何保证补偿顺序?这些都要一一考虑,代码变得越来越庞大复杂。

2. 状态一致性难以保障

由于各服务是异步更新的,短时间内很容易出现状态不一致的情况。比如订单状态已经是“已支付”,但库存还没被扣减,导致超卖风险。

3. 调试困难,问题定位慢

补偿流程一旦出错,排查起来非常麻烦。我们需要查订单表、库存表、用户账务表、日志记录、MQ消费情况……整个链路可能涉及十几个服务、几十张表,运维工作量巨大。

4. 性能瓶颈明显

补偿任务频繁地扫描状态、发起远程调用,在高峰时直接把数据库压垮,甚至影响主流程。

这些问题让我们意识到:这种临时性解决方案已经无法满足当前系统的稳定性和扩展性需求。我们必须找一套更合适的分布式事务方案。

解决方案选型与设计思路

我们开始调研各种分布式事务解决方案。最终决定采用 Seata + Saga 模式 作为主方案,部分关键路径采用 TCC 模式兜底。

为什么要选择 Seata 的 Saga 模式?

我们最初也考虑过 XA 模式(两阶段提交),但它对数据库性能损耗较大,尤其在高并发场景下容易卡死。TCC 模式虽然灵活,但需要为每个服务编写正向和补偿方法,实现成本较高。

而 Saga 模式是一个长期运行的事务模型,适用于我们这样多个微服务、长周期操作的业务流程。它的核心思想是:

“如果某一步骤失败,则依次执行之前所有成功步骤的补偿操作。”

也就是说,我们可以在每一个服务中定义好各自的操作和对应的补偿逻辑,由 Saga 引擎负责协调整个流程。

我们选择了 Seata 开源框架,它支持多种事务模式,社区活跃度高,而且已经在不少大厂落地使用。

架构设计概览

整体架构如下图所示:

+------------------+        +------------------+
|   Order Service  | -----> |   Inventory SVC  |
+--------+---------+        +--------+---------+
         |                            |
         v                            v
+--------+---------+        +--------+---------+
|   Account SVC    | <----- |   Logistics SVC  |
+------------------+        +------------------+

             ↖                           ↗
               ↘                     ↙
                 ↘              ↙
                   ↘       ↙
                +--------------+
                |  Saga Engine |
                +--------------+

Saga 引擎作为控制中枢,管理整个事务流程的状态转换。我们为每个子服务提供了两个接口:

  • 正向操作(如扣库存)
  • 补偿操作(如加库存)

当其中任意一个服务调用失败时,Saga 引擎会按顺序调用之前成功服务的补偿接口,从而保持最终一致性。

数据库设计

为了支持 Saga 模式的事务追踪,我们在每个服务中新增了一张事务状态表,结构大致如下:

CREATE TABLE saga_transaction_log (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    transaction_id VARCHAR(64) NOT NULL,
    service_name VARCHAR(64) NOT NULL,
    action_name VARCHAR(64) NOT NULL, -- 如 deductInventory / revertInventory
    status ENUM('pending', 'success', 'failed') NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

这张表用于记录每次 Saga 执行过程中各个服务的状态,方便故障排查和重试。

关键代码实践

下面是一段简化版的 Saga 事务逻辑,以 Java + Spring Boot 为例:

定义 Saga 流程

public class OrderSaga extends AbstractSaga {
    
    private final InventoryService inventoryService;
    private final AccountService accountService;
    private final OrderService orderService;


![负载均衡配置-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062517/6366a5f3-0c05-4db5-89a3-313ad5ce78d3.jpg)


    public OrderSaga(...) {
        this.inventoryService = inventoryService;
        this.accountService = accountService;
        this.orderService = orderService;
    }

    @Override
    public void execute() {
        try {
            // 1. 扣减库存
            if (!inventoryService.deductStock(productId)) {
                throw new RuntimeException("库存不足");
            }
            recordAction("deductStock", "inventory");

            // 2. 冻结账户余额
            if (!accountService.freezeBalance(userId, amount)) {
                throw new RuntimeException("冻结余额失败");
            }
            recordAction("freezeBalance", "account");

            // 3. 创建订单
            String orderId = orderService.createOrder(...);
            recordAction("createOrder", "order");

        } catch (Exception e) {
            compensate();
            log.error("事务失败", e);
        }
    }

    @Override
    public void compensate() {
        List<SagaStep> executedSteps = getExecutedSteps();

        // 倒序执行补偿逻辑
        for (int i = executedSteps.size() - 1; i >= 0; i--) {
            SagaStep step = executedSteps.get(i);
            switch (step.getAction()) {
                case "deductStock":
                    inventoryService.revertStock(step.getProductId());
                    break;
                case "freezeBalance":
                    accountService.unfreezeBalance(step.getUserId(), step.getAmount());
                    break;
                default:
                    log.warn("未知的操作: {}", step.getAction());
            }
        }
    }

    private void recordAction(String actionName, String serviceName) {
        sagaTransactionLogRepository.save(new SagaTransactionLog(UUID.randomUUID().toString(), serviceName, actionName, "pending"));
    }
}

这段代码展示了基本的 Saga 流程控制逻辑,包括正常执行和异常情况下的回滚操作。

Saga 引擎配置(伪代码)

# application.yml

saga:
  mode: SAGA
  retry: true
  retry-limit: 3
  enable-compensation: true
  transaction-manager-host: seata-server-host

我们还集成了 Seata 的 Saga 引擎作为事务编排控制器,通过状态机引擎描述整个事务流程,进一步提升了事务调度的灵活性。

实战踩坑经验

虽然 Saga 是一种相对成熟、稳定的方案,但在实际开发过程中我们也踩了不少坑,总结了几点比较有价值的经验。

1. 补偿操作必须幂等!

我们在上线初期曾遇到一个问题:某个服务因网络原因多次收到补偿请求,结果重复加库存两次,导致数据异常。后来我们增加了唯一事务ID校验机制:

public boolean revertStock(String transactionId, Long productId) {
    if (redis.exists("reverted_stock:" + transactionId)) {
        return true; // 已补偿
    }

    // 执行真正的补偿逻辑...

    redis.setex("reverted_stock:" + transactionId, 86400, "1"); // 缓存1天
    return success;
}

这个措施大大减少了重复补偿带来的副作用。

2. Saga 引擎本身不可靠,需要容灾设计

有一次 Seata Server 挂掉了,导致正在运行中的 Saga 事务全部中断,无法自动恢复。后来我们增加了一个异步监控任务,定期扫描未完成的事务日志,主动触发补偿或重试流程。

@Scheduled(fixedRate = 60_000)
public void checkOrphanTransactions() {
    List<String> pendingTxIds = sagaTransactionLogRepository.findPendingTransactions();
    
    for (String txId : pendingTxIds) {
        SagaTransactionLog lastStep = sagaTransactionLogRepository.findLastByTxId(txId);
        SagaEngine.resumeTransaction(txId, fromStep(lastStep));
    }
}

这样即使 Saga 引擎宕机也能尽可能降低数据不一致的风险。

3. 不要把 Saga 当作银弹,适当使用 TCC 作为补充

在一些关键交易路径(如退款)上,我们最终采用了 TCC 模式,因为它可以提供更强的一致性保证,适合金额类场景。

例如:

@Transactional
public boolean prepareRefund(String refundId) {
    // 先冻结金额
    return accountService.freezeRefundAmount(refundId, amount);
}

public boolean commitRefund(String refundId) {
    // 实际扣除冻结资金
    return accountService.withdrawFrozenAmount(refundId, amount);
}

public boolean cancelRefund(String refundId) {
    // 释放冻结资金
    return accountService.releaseFrozenAmount(refundId, amount);
}

这里 TCC 的三段式模型更适合对数据一致性要求高的场景。

4. 日志追踪要完整,不然排查效率低

我们一开始只记录了事务ID和状态,没有埋点完整的操作上下文。后来改造成记录详细参数,并接入 ELK:

{
  "tx_id": "abc123",
  "service": "inventory",
  "action": "deductStock",
  "payload": {
    "product_id": 1001,
    "quantity": 2
  },
  "status": "success",
  "timestamp": "2023-09-15T10:23:00Z"
}

有了这些详细的日志信息,排查起问题来事半功倍。

上线后的效果与收益

我们从 2022 年 Q2 正式将这套 Saga + TCC 的混合方案上线生产环境,经过一年的打磨和优化,收获了不少成果:

  • 数据一致性显著提升:跨服务数据不一致的问题减少90%以上;
  • 系统稳定性增强:补偿流程自动化程度高,运维压力大大减轻;
  • 开发效率提升:Saga 引擎屏蔽了复杂的编排逻辑,业务开发人员只需关注本地事务;
  • 容灾能力加强:即使 Saga Server 故障,也有完善的降级机制;
  • 性能优化空间变大:异步化之后,核心链路响应速度更快,吞吐量提升约30%。

当然,这也带来了一些额外的开销,比如 Saga 日志存储、补偿重试的资源消耗等,但总体来看收益远大于成本。

给读者的建议与注意事项

如果你也在考虑引入分布式事务,我有几点真心建议想分享:

✅ 尽早统一事务模型

不要等到服务拆多了才去想怎么处理事务一致性问题。越晚介入,改造成本越高。

✅ 根据业务场景灵活选用不同模式

Saga、TCC、XA、SAGA + TCC 混合、甚至本地事务 + 补偿机制都可以共存。关键是根据具体业务特点选择合适的方式。

✅ 注意服务幂等性设计

不管是补偿还是重试,都必须保证调用是幂等的。这是避免雪崩效应的关键防线。

✅ 监控不能少

要有健全的监控体系,包括事务成功率、补偿次数、延迟指标等,及时发现问题并干预。

✅ 技术选型要慎重

像 Seata 这种开源中间件虽好,但也得注意版本兼容性、部署方式、运维复杂度。如果是公司内部自研组件,那更要做好文档、测试和灰度发布。

最后一点感悟:分布式事务是个“妥协的艺术”。永远不可能做到绝对一致性,只能追求最终一致性 + 快速恢复能力。我们要做的,是在稳定性和开发效率之间找到一个合适的平衡点。

结语

这篇文章写到这里差不多快结束了,回头看看这一路走来的酸甜苦辣,真挺感慨的。分布式事务这件事,说简单也简单,说复杂也确实够复杂。但我相信只要我们坚持从真实业务出发,不断迭代优化,就一定能找到最适合自己的解决方案。

希望这篇从一线实战中提炼出来的经验,能够帮助你在面对分布式事务难题时多一份从容,少一些焦虑。如果有什么问题或者你也碰到了类似的坑,欢迎留言交流。

—— 一位热爱折腾的后端架构师 🧑‍💻

评论 0

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