分布式事务实战:从踩坑到掌控的五年心路

宋建国
2025-06-27 11:05
阅读 268

引言:为什么我决定写这篇文章?

引言:为什么我决定写这篇文章?

五年前刚入行的时候,我在一个电商项目里负责订单服务。有一天上线后突然收到报警:用户下单后积分没扣、商品库存也少了,但订单却显示成功。更糟的是,支付系统那边已经收到了交易成功的通知。

当时的我一头雾水,查日志、看代码、翻文档……最后才发现,这其实是一个典型的分布式事务问题——服务间状态不一致。我们当时使用的是多个微服务+不同的数据库,整个流程横跨了支付、订单、商品、积分四个系统,每个系统的数据一致性都只能通过程序去保障。

那一刻起,我就意识到,在微服务架构下,“事务”这件事比以前复杂太多了。

今天,我想结合自己这几年在多个项目中遇到的真实场景,分享一下我对分布式事务的实践经验和思考。


问题描述:真实业务中的典型挑战

问题描述:真实业务中的典型挑战

负载均衡配置-2

场景背景

最近一个项目是一个 SaaS 平台,面向中小企业提供供应链管理功能。这个平台中有:

  • 商品中心(MySQL)
  • 库存中心(MongoDB)
  • 订单中心(PostgreSQL)
  • 支付中心(外部接口)
  • 用户中心(Redis 缓存用户余额)

当用户完成一次采购,会经历如下关键步骤:

  1. 下单
  2. 扣用户余额
  3. 减库存
  4. 写订单记录
  5. 调用第三方支付回调确认

如果其中任何一个环节失败,都要保证所有操作回滚,否则就会出现类似“钱扣了货没发”、“货没了钱没收到”的严重问题。

起初我们想当然地认为:“用本地事务包裹一下这些调用不就好了?”然而事实是,每个服务都有自己的数据库,事务无法跨服务提交或回滚。

更复杂的还有:

  • 有些服务是外部系统,比如支付系统是第三方的
  • 存在异步处理的需求,比如生成报表、同步日志
  • 需要支持高并发和低延迟

解决方案选型:不是只有 Seata!

解决方案选型:不是只有 Seata!

为了解决这个问题,我和团队调研了很多方案,最终选择了 TCC + Saga 混合模式。下面是我总结的一些主流方案及其适用场景:

方案 适用场景 优点 缺点
两阶段提交 小规模、强一致性要求 简单易实现 性能差、存在单点故障
TCC 核心业务链路,可拆分为 Confirm/Cancel 性能好、适合高并发 逻辑复杂、需要补偿机制设计
Saga 异常较少、补偿代价较低 易扩展、适合长流程 最终一致性,可能带来数据波动
消息队列事务 数据异步同步、事件驱动 松耦合、性能好 实现成本高、需考虑幂等
Seata 单一技术栈、Spring Cloud生态 集成简单、社区活跃 对 DB 类型和框架兼容有限

我们最终采用 TCC(Try-Confirm-Cancel) + Saga 组合的方式实现了核心下单流程的一致性保障。


关键代码与实现思路:以订单创建为例

关键代码与实现思路:以订单创建为例

我们把一个订单创建流程划分为以下几个 TCC 步骤:

  1. Try 阶段:

    • 冻结用户余额(在 Redis 中预扣)
    • 预减库存(MongoDB 设置冻结字段)
  2. Confirm 阶段:

    • 扣除用户余额
    • 真正减少库存
    • 创建订单记录
  3. Cancel 阶段:

    • 回退用户余额
    • 回滚库存

以下是简化的核心流程伪代码(实际项目使用 Spring Boot + Dubbo + RocketMQ 消息队列):

// 下单入口
public Order createOrder(OrderRequest request) {
    // Step 1: Try 阶段
    tryService.freezeBalance(request.getUserId(), request.getTotalPrice());
    tryService.reduceInventory(request.getProductId(), request.getCount());

    // Step 2: 创建订单
    Order order = orderService.create(request);

    // Step 3: 调用 Confirm 或触发 Cancel
    if (paymentService.charge(order)) {
        confirmService.confirmBalance(request.getUserId(), request.getTotalPrice());
        confirmService.confirmInventory(request.getProductId(), request.getCount());
    } else {
        cancelService.rollbackBalance(request.getUserId(), request.getTotalPrice());
        cancelService.rollbackInventory(request.getProductId(), request.getCount());
    }

    return order;
}

看起来很理想?但真正的难点在于如何设计这些接口,以及在失败时自动重试/补偿。

为此我们引入了一个 Saga 工作流引擎,用于编排整个流程,并记录每一步的状态。

🧠 小插曲:最开始我们没有记录每一步状态,结果线上有个任务卡住了,根本不知道是在哪个环节出了问题。后来才加上了完整的 traceID 和 step 日志追踪机制。


踩过的坑:那些年我掉过的陷阱

1. 幂等性缺失引发重复扣款

某次促销活动中,用户余额被重复扣除。排查发现是因为消息队列中的 Confirm 操作被多次消费。

教训:所有的 Confirm/Cancel 操作必须携带唯一标识符,并在服务端做幂等校验。

// 示例:基于 Redis 的幂等校验
public void confirmBalance(String userId, String requestId) {
    String key = "confirm_balance:" + userId + ":" + requestId;
    Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "1", 30, TimeUnit.MINUTES);
    if (result == null || !result) {
        log.warn("该余额确认已执行过");
        return;
    }
    
    // 执行真正逻辑
    balanceService.deduct(userId, amount);
}

系统架构设计图-1


2. Cancel 失败导致脏数据残留

有一次 Cancel 失败了,但上层流程已经结束,导致库存未释放。客户投诉商品明明“买到了”,但库存却还是原来的数。

解决方案是:

  • 增加 Cancel 失败的重试策略(比如三次重试)
  • 引入定时扫描 job,兜底处理超时未取消的数据

3. TCC 接口设计不当

最开始 Try 接口没有返回值,导致 Confirm/Cannel 不知道到底有没有成功执行。后来改成带返回码和预留 ID 的结构,才方便下游判断。


实施后的效果与收益

项目上线之后,系统稳定性显著提升:

  • 事务一致性达到 99.999%
  • 平均响应时间控制在 300ms 内
  • 高峰期每秒处理能力超过 1w TPS

我们还在后台搭建了一个可视化监控看板,展示不同事务阶段的成功率、耗时分布、错误类型统计等,极大提升了排查效率。

更重要的是,我们的开发流程也更加规范了:

  • 所有涉及资金变动的操作必须走事务流程
  • 提交前必须跑事务测试套件
  • 所有事务操作要有完整日志追踪(traceId + spanId)

我的经验总结与建议

经过几年在多个项目上的尝试与打磨,我总结出几点实用经验:

✅ 1. 不要过度追求“强一致性”

很多时候我们陷入误区,总觉得所有操作都必须严格原子化。其实,很多业务可以接受“最终一致性”。例如退款、物流状态更新就可以用 Saga 或者消息队列来解耦。

✅ 2. 做好事务的隔离与限界上下文划分

不要把一个事务做得太大,要根据业务边界切分职责。比如订单、库存、账户这三个系统之间最好互不影响。

✅ 3. 引入可观测性是必须的

  • 日志一定要带上 traceId/spanId
  • 每个事务的生命周期要全程监控
  • 异常情况要能快速定位并修复

✅ 4. 补偿机制不能少

即使再完善的系统,也可能因为网络抖动、服务不可用等问题导致失败。所以你得准备好补偿方案,比如定时 Job、人工干预接口。

✅ 5. 技术选型要结合团队成熟度

如果你的团队对 RocketMQ 不熟,就别强行搞消息事务模型;如果你的技术栈主要在 Spring Cloud,那不妨试试 Seata。


写在结尾:工程师的成长就是不断面对不确定

说实话,分布式事务不是一个“有标准答案”的话题。它更像是一个艺术,需要你在架构设计、容错机制、性能优化等多个维度进行权衡。

这几年我经历过凌晨三点紧急回滚版本,也经历过因为一个 Cancel 方法没写好导致整个库存系统瘫痪。但也正是这些“踩坑”的过程,让我慢慢成长为一个能独立承担责任的后端工程师。

我希望这篇分享,不只是告诉你“怎么解决分布式事务”,而是让你看到我在面对复杂问题时的思考方式和取舍思路。希望它对你也有启发。

如果你也在做微服务相关的项目,欢迎留言交流,我们一起成长 😊


作者:一位在一线敲代码的后端开发者,热爱技术也热爱生活。欢迎关注我的 GitHub 和公众号【TechGrower】。

评论 0

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