分布式事务解决方案:一次真实项目中的战斗

代码里的风
2025-06-15 06:34
阅读 529

开头:为什么我决定写这篇文章

最近在公司的一个新项目中,我又一次撞上了那个“老朋友”——分布式事务问题。这次我们做的是一个跨多个服务的订单履约系统,用户下单后需要调用库存服务、积分服务、物流服务等多个下游系统,还要保证状态的一致性。

刚开始开发的时候大家都没太当回事,觉得加个事务不就完了?但当你面对多个微服务、多库、甚至多个业务单元时,事情就没那么简单了。这篇文章我想用自己的亲身经历,讲讲我们在实际项目中遇到的问题、踩过的坑,以及最终是如何找到一套“能打”的解决方案的。


项目背景与挑战描述

我们的项目是给某大型电商平台新增一套订单履约系统,目标是支持复杂的履约逻辑,包括:

  • 订单创建
  • 库存扣减
  • 积分扣除
  • 物流单推送
  • 状态更新和消息通知

整个系统采用的是典型的微服务架构,拆分为订单服务、库存服务、积分服务、物流服务等几个核心模块,各自拥有独立数据库和接口。

初期设计(简单粗暴)

最开始的设计是这样的:

  1. 下单完成后,通过同步 RPC 调用各个服务,执行各自的写操作;
  2. 所有服务都返回成功才算整个流程完成;
  3. 如果中间任何一个失败,立即回滚当前流程。

看起来没毛病对吧?但我们很快遇到了几个关键问题:

  • 接口超时导致整体流程卡死;
  • 部分服务失败无法回滚,比如已经扣了积分,但库存扣减失败;
  • 数据一致性难以保障
  • 重试机制复杂且容易重复扣款/发货

更严重的是,有一次灰度发布时,因为一个服务异常未返回正确响应码,导致一批用户的积分被重复扣除,直接上生产故障单。

这下大家都意识到:我们必须为这个系统引入一套可靠的分布式事务机制了。


解决方案探索与选型

针对这个问题,我们先后考虑过几种主流方案:

方案一:两阶段提交(2PC)

2PC 是一种强一致性协议,适合强一致场景,但在实践中存在明显缺点:

  • 同步阻塞时间长;
  • 协调者节点存在单点风险;
  • 网络不稳定时易造成数据不一致;
  • 微服务之间接口通常没有 XA 支持。

我们调研了一下目前的服务现状,几乎不具备支持 2PC 的条件,而且对性能要求高,不能接受长时间阻塞。

结论:PASS

方案二:TCC(Try-Confirm-Cancel)

这是我们最终选择的主要方案之一。

TCC 的思想是:将一个操作分为三个阶段:

  • Try 阶段:资源预留(例如冻结库存、冻结积分);
  • Confirm 阶段:业务执行(如正式扣减库存);
  • Cancel 阶段:回滚处理(释放预留资源);

这种方式的好处在于不需要数据库的支持,只需要服务本身实现这三个接口即可,非常适合我们的业务场景。

我们是怎么做的?

以“下单+扣库存+扣积分”为例:

// 在订单服务中
public Order placeOrder(...) {
    // Step 1: Try phase
    inventoryService.prepareInventory(orderId, items);
    pointsService.deductPointsPreparation(userId, points);

    // Step 2: 创建订单
    Order order = createOrderInDB(...);

    try {
        // Step 3: Confirm phase
        inventoryService.confirmInventoryDeduction(orderId);
        pointsService.confirmPointsDeduction(userId);
    } catch (Exception e) {
        // Step 4: Cancel phase
        inventoryService.rollbackInventory(orderId);
        pointsService.rollbackPoints(userId);
        throw new OrderProcessingException("Failed to confirm deduction", e);
    }


![服务器部署方案-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025061506/5dce9497-547e-4d9a-8faf-e670c75e6343.jpg)


    return order;
}

当然,这只是伪代码。真实实现远比这个要复杂得多,尤其是关于幂等、补偿逻辑、日志追踪这些方面。

补充说明:幂等控制很重要!

我们在每个接口中都加入了全局唯一的业务流水号(bizId)和请求ID(requestId),确保即使网络抖动或重发也不会造成重复操作。

实施难点
  1. 每个服务都要自己维护 TCC 接口;
  2. 异常场景下的状态追踪非常麻烦,需要引入事务日志;
  3. 补偿过程必须自动、可靠,否则就会出现“卡单”问题;
  4. 对业务逻辑的理解成本较高,需要团队统一认知;

为了简化管理,我们还专门搭建了一个事务协调服务(Transaction Coordinator),负责记录每一步的状态、发起补偿、触发异步清理任务等。

方案三:SAGA 模式

SAGA 是另一种常用的分布式事务模式,特点是每个子事务独立提交,如果失败则依次执行逆向补偿动作。

我们也做过尝试,但在我们这种对一致性要求极高的电商场景下,SAGA 太容易出错,补偿链太长,很难跟踪。所以最后还是选择了相对更可控的 TCC。

方案四:本地事务表 + MQ 异步补偿

这个思路我们主要用来处理订单状态同步、短信/邮件通知等非核心路径的环节。

具体做法是:

  1. 在主业务库中记录本地事务表(local_transaction);
  2. 使用消息队列(Kafka / RocketMQ)发送异步事件;
  3. 消费端监听事件,并消费成功后标记事务完成;
  4. 定时任务扫描未完成事务并重试。

优点是性能好、解耦程度高、适用于非实时性强一致场景。


实践成果与效果评估

这套基于 TCC + 异步补偿组合的方案实施之后,我们系统的稳定性和一致性得到了显著提升。

关键指标变化如下:

指标 上线前 上线后
平均下单耗时 800ms 500ms
数据不一致率 ~3% <0.1%
日均故障单数量 10+ 0~2
接口成功率 ~97% >99.8%

最重要的是,在后来的几次大促中,系统都能扛住流量压力,没有再发生积分误扣、库存负值等事故。

虽然开发成本高了一些,但从稳定性角度看是非常值得的。


经验总结与建议

如果你也在做类似的微服务项目,以下是我亲身踩坑后的一些经验分享:

1. 不要低估一致性的重要性

尤其是在资金、库存类操作中,任何小的失误都会造成巨大影响。哪怕你只漏掉了一种异常情况,可能就需要人工介入修复数据。

2. TCC 是把双刃剑,用不好反伤己身

TCC 带来的最大好处就是灵活性,但它也对代码质量、接口幂等、事务追踪提出了更高要求。如果没有完整的事务日志体系,很容易陷入“卡单”的黑洞。

3. 日志才是救命稻草

我们专门为每个事务流程设计了一个“事务上下文日志”,记录每一步操作的输入输出、耗时、状态、是否重试、重试次数等信息。这些数据不仅对排查问题非常重要,后期还可以用来做数据分析和监控报警。

4. 异步补偿也要设计成可追踪、可恢复

我们曾经犯过一个错误:把订单状态同步完全交给 Kafka 消费,结果 Kafka 出现短暂不可用,导致很多订单一直处于“已创建未发货”状态,客户投诉激增。

后来我们加上了事务状态检查 + 死信队列 + 定时扫表补发,才解决了这类问题。

5. 做好技术降级预案

比如:在某个服务不可用时,应该允许跳过非核心事务步骤,但要明确标注“待后续补偿”。


当下趋势与未来展望

目前业界对于分布式事务的关注点正在往两个方向走:

  1. 柔性一致性 + 异步补偿:这也是我们方案的核心思想之一;
  2. 基于 Event Sourcing 和 CQRS 架构的新玩法:虽然暂时还没用到,但已经在我们架构升级计划中。

另外,像 Seata、Apache ServiceComb Saga 这些分布式事务框架也在不断完善中,未来我们可以尝试集成使用,减少一些自研成本。


写在最后:开发不是搭积木,而是在拼图

说实话,写这篇文章的时候,我还是有点感慨。

刚接触分布式事务的时候,我也觉得只要加个注解就能搞定一切;但真正落地的时候才发现,它背后牵涉的不只是技术,还有产品逻辑、运维策略、运营配合,甚至是公司流程制度。

每一次系统重构,都是一次重新认识自己的机会。而每解决一个问题,也都意味着我们离“高可用、高性能、高扩展”的目标又近了一步。

希望这篇来自实战的文章,能帮助你少走弯路。如果你也在处理类似的问题,欢迎留言交流,我们一起成长!


作者简介:

一名工作6年的后端开发者,经历过从单体架构到微服务再到云原生的全过程。目前专注系统稳定性、高并发场景优化和分布式系统设计。热爱开源,喜欢捣鼓各种中间件源码。

评论 0

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