分布式事务解决方案

电商业务分布式事务解决方案

本文系统介绍电商业务中分布式事务的多种解决方案,从简单到复杂逐一分析。


目录


一、电商业务中的分布式事务场景

典型场景:下单扣库存

+-------------+     +-------------+     +-------------+
|   订单服务   |     |   库存服务   |     |   账户服务   |
|  (创建订单)  |     |  (扣减库存)  |     |  (扣减余额)  |
+------+------+     +------+------+     +------+------+
       |                   |                   |
       +-------------------+-------------------+
              三个服务,三个独立数据库

问题:创建订单、扣库存、扣余额,这三个操作必须同时成功或同时失败,但每个服务有自己的数据库,本地事务无法保证跨服务的一致性。


二、分布式事务的本质

核心逻辑

分布式事务的本质,是所有方案都在解决同一个问题:

多个服务要一起成功,或者一起失败,不能出现部分成功的中间状态。

不允许的情况:
  订单创建了 ✓
  库存扣了   ✓
  余额没扣   ✗  ← 钱没付,货扣了,订单也有了,系统就乱了

必须保证:
  情况1:全部成功
    订单创建了 ✓
    库存扣了   ✓
    余额扣了   ✓

  情况2:全部回滚
    订单没创建 ✗
    库存没扣   ✗
    余额没扣   ✗

所有方案的本质 = 协调 + 补偿

┌─────────────────────────────────────────┐
│           分布式事务 = 协调 + 补偿         │
├─────────────────────────────────────────┤
│                                         │
│  协调:怎么让多个服务按顺序执行            │
│        → 2PC的协调者统一指挥              │
│        → Saga的状态机调度                 │
│        → 本地消息表的定时任务扫描          │
│                                         │
│  补偿:某个服务失败了,怎么把其他的回滚      │
│        → Seata AT的undo_log反向SQL        │
│        → TCC的Cancel方法                  │
│        → Saga的补偿事务                   │
│        → 本地消息表的重试机制              │
│                                         │
└─────────────────────────────────────────┘

用一个例子理解所有方案

场景:公司组织团建,要同时完成三件事:

  • A:订餐厅(订单服务)
  • B:订大巴(库存服务)
  • C:收团费(账户服务)
方案 生活类比 核心逻辑
本地消息表 发微信群通知 我先订好餐厅,群里@小红订大巴,小红@小刚收团费
2PC 开会举手表决 领导问"能不能去?",大家都说能,领导说"执行!"
TCC 先交定金再付尾款 每个人都先交定金预留资源,能交齐就付尾款,交不齐就退定金
Saga 一步一步来,错了往回退 先订餐厅→再订大巴→再收团费,哪步错了就取消前面的
Seata AT 自动拍照记账 每做一步自动拍照记录,最后取消时按照片恢复原状
最大努力通知 发通知不管结果 发了通知就行,对方收没收到不管,配合对账兜底

三、分布式事务解决方案(从简单到复杂)


方案 1:本地消息表(最简单,最终一致性)

核心思想

将分布式事务拆分为本地事务 + 异步消息补偿。

架构图

+---------------------------------------------------------+
|                      订单服务                              |
|  +-------------+    +-------------+    +-------------+  |
|  |  业务表:订单  |    |  消息表      |    |  定时任务扫描  |  |
|  | (同一DB事务) |<-->| (同一DB事务) |<-->| (未发送消息) |  |
|  +-------------+    +-------------+    +------+------+  |
+---------------------------------------------------------+                                                |
                                                 |
                    +----------------------------+
                    |
                    v
            +-------------+
            |   MQ消息队列  |
            +------+------+
                   |
       +-----------+-----------+
       |                       |
       v                       v
+-------------+         +-------------+
|   库存服务    |         |   账户服务    |
| (消费消息扣库存)|         | (消费消息扣余额)|
+-------------+         +-------------+

流程说明

  1. 订单服务开启本地事务

    • 插入订单记录(业务操作)
    • 插入消息记录(status=PENDING)
    • 提交本地事务(两者原子性保证)
  2. 定时任务扫描消息表,将PENDING消息发送到MQ

  3. 库存服务消费消息,扣减库存

    • 成功 → 订单服务更新消息status=SENT
    • 失败 → 消息重试(幂等性保证)
  4. 账户服务同理

优缺点

优点 缺点
实现简单,不依赖外部框架 有延迟
最终一致性 需要保证消费者幂等性

适用:电商下单、支付通知等对实时性要求不高的场景

失败后的业务处理

本地消息表是异步方案,一阶段本地事务已提交,用户已收到成功响应。二阶段消息消费可能失败。

用户点击"下单"
     ↓
本地事务成功(订单已创建,消息已记录)
     ↓
立即返回"下单成功"
     ↓
定时任务扫描消息表发送到MQ ←── 可能失败
     ↓
库存服务消费消息扣减库存 ←── 可能失败
     ↓
   ┌────┴────┐
   ↓         ↓
  成功      失败
   ↓         ↓
更新消息    消息重试
状态        ↓
           定时重试(固定间隔)
           ├─ 重试成功 → 更新消息状态
           └─ 重试耗尽 → 进入死信队列
                          ↓
                     人工介入 / 自动补偿
                          ↓
                     对账系统每日兜底核对

电商实际案例:下单成功但库存扣减失败

场景:用户下单成功,订单已创建,但库存服务消费消息失败

用户看到:下单成功 ✓
库存实际:未扣减(超卖风险)

系统处理:
  1. 消息进入重试队列,每隔 5 分钟重试,最多重试 10 次
  2. 重试 10 次仍失败 → 进入死信队列
  3. 对账系统每日扫描:
     - 发现订单状态为"已创建",但库存未扣减
     - 自动补扣:发送补偿消息扣减库存
     - 或自动关单:如果库存确实不足,关闭订单并退款
  4. 超过 30 分钟仍未处理 → 触发告警,人工介入

用户侧处理:
  - 订单状态可能长时间停留在"待处理"
  - 可联系客服查询订单状态
  - 客服后台可手动触发库存扣减或关闭订单

面试要点

  • 本地消息表是异步方案,用户已收到成功响应,后续失败用户无感知
  • 必须有消息重试机制,固定间隔或指数退避
  • 必须有死信队列,重试耗尽后人工介入
  • 必须有对账系统兜底,每日核对订单和库存一致性
  • 客服系统要能看到异常订单,方便用户咨询时处理

方案 2:2PC(两阶段提交,强一致性)

核心思想

引入协调者(Coordinator),分两个阶段投票和提交。

架构图

                    协调者(Coordinator)
                           |
           +---------------+---------------+
           |               |               |
           v               v               v
      +---------+    +---------+    +---------+
      | 订单服务  |    | 库存服务  |    | 账户服务  |
      | (参与者) |    | (参与者) |    | (参与者) |
      +----+----+    +----+----+    +----+----+
           |               |               |
           v               v               v
      +---------+    +---------+    +---------+
      | 订单DB   |    | 库存DB   |    | 账户DB   |
      +---------+    +---------+    +---------+

第一阶段(投票阶段)

协调者 --prepare--> 订单服务 --> 订单DB(锁定资源,记录undo/redo日志)
       --prepare--> 库存服务 --> 库存DB(锁定资源)
       --prepare--> 账户服务 --> 账户DB(锁定资源)

各参与者返回 Yes/No(能否执行)

第二阶段(提交/回滚阶段)

如果所有参与者返回 Yes:
  协调者 --commit--> 订单服务 --> 订单DB(真正提交)
         --commit--> 库存服务 --> 库存DB(真正提交)
         --commit--> 账户服务 --> 账户DB(真正提交)

如果有参与者返回 No:
  协调者 --rollback--> 订单服务 --> 订单DB(回滚)
         --rollback--> 库存服务 --> 库存DB(回滚)
         --rollback--> 账户服务 --> 账户DB(回滚)

状态转换图

        开始
         |
         v
    +---------+
    |  投票阶段  |
    | (prepare) |
    +----+----+
         |
    +----+----+
    |         |
    v         v
  全部Yes    有No
    |         |
    v         v
+-------+  +--------+
| 提交阶段 |  | 回滚阶段 |
|(commit)|  |(rollback)|
+---+---+  +----+---+
    |           |
    v           v
  成功         失败

优缺点

优点 缺点
强一致性,理论成熟 同步阻塞:参与者需要锁定资源等待协调者指令
单点故障:协调者挂了,参与者一直阻塞
性能差:两次网络往返,延迟高

适用:对一致性要求极高、并发量低的场景(金融核心系统)

失败后的业务处理

2PC 是同步方案,失败会立即返回给调用方。协调者统一指挥,参与者锁定资源等待指令。

用户点击"转账"
     ↓
2PC 协调者发起 prepare
     ↓
   ┌────┴────┐
   ↓         ↓
  全部Yes    有No
   ↓         ↓
  commit   rollback
   ↓         ↓
  成功      失败
   ↓         ↓
返回成功   返回错误

电商实际案例:银行转账失败

场景:用户从A银行转账到B银行

失败场景:B银行 prepare 返回 No(账户冻结)

系统处理:
  1. 协调者收到 No,向所有参与者发送 rollback
  2. A银行回滚:释放锁定的转出金额
  3. B银行回滚:无需操作(本来就没执行)
  4. 返回用户:"转账失败,目标账户异常"

业务处理:
  - 返回具体错误:"对方账户状态异常,无法接收转账"
  - 建议用户:"请确认对方账户是否正常,或联系对方银行"
  - 记录日志:用于排查账户冻结原因
  - 监控告警:如果大量转账失败,可能B银行系统故障

极端情况:协调者挂了
  - 参与者一直持有锁,等待协调者恢复
  - 需要超时机制,超时后自动释放锁
  - 可能导致数据不一致(超时释放锁后,协调者恢复发送commit)

面试要点

  • 2PC 是同步方案,失败立即返回,用户感知明显
  • 协调者单点故障是最大风险,需要超时机制兜底
  • 错误信息要具体,让用户知道失败原因
  • 金融场景下,协调者故障需要人工介入恢复

方案 3:TCC(Try-Confirm-Cancel,业务层面补偿)

核心思想

将每个操作拆分为三个方法:预留资源、确认执行、取消回滚。

架构图

+---------------------------------------------------------+
|                      订单服务                              |
|  Try: 创建订单(状态=待确认)                                |
|  Confirm: 更新订单状态=已确认                                |
|  Cancel: 删除订单 或 更新状态=已取消                         |
+---------------------------------------------------------+
                           |
           +---------------+---------------+
           |               |               |
           v               v               v
      +---------+    +---------+    +---------+
      | 库存服务  |    | 账户服务  |    | 优惠券服务 |
      |         |    |         |    |         |
      |Try: 预扣|    |Try: 冻结|    |Try: 预留 |
      |  库存   |    |  金额   |    |  优惠券  |
      |         |    |         |    |         |
      |Confirm: |    |Confirm: |    |Confirm: |
      | 真正扣减|    | 真正扣款|    | 真正使用 |
      |         |    |         |    |         |
      |Cancel:  |    |Cancel:  |    |Cancel:  |
      | 释放库存|    | 解冻金额|    | 释放优惠券 |
      +---------+    +---------+    +---------+

TCC 执行流程(成功路径)

协调器/事务管理器
     |
     v
+---------+
| Try阶段  |--------------------------------------+
|(预留资源)|                                      |
+----+----+                                      |
     |                                           |
     v                                           v
+---------+    +---------+    +---------+
| 订单Try  |    | 库存Try  |    | 账户Try  |
|创建待确认 |    | 预扣库存  |    | 冻结金额  |
|  订单    |    |         |    |         |
+----+----+    +----+----+    +----+----+
     |              |              |
     +--------------+--------------+
                    |
              全部Try成功
                    |
                    v
            +---------+
            |Confirm  |--------------------------+
            |(确认执行)|                            |
            +----+----+                            |
                 |                                 v
                 v                    +---------+    +---------+
            +---------+               | 库存Confirm|    | 账户Confirm|
            | 订单Confirm|              | 真正扣减  |    | 真正扣款  |
            | 更新已确认 |              +---------+    +---------+
            +---------+

TCC 执行流程(失败路径)

协调器/事务管理器
     |
     v
+---------+
| Try阶段  |
|(预留资源)|
+----+----+
     |
     v
+---------+    +---------+    +---------+
| 订单Try  |    | 库存Try  |    | 账户Try  |
|  成功    |    |  成功    |    |  失败!  |  <-- 某个服务Try失败
+----+----+    +----+----+    +----+----+
     |              |              |
     +--------------+--------------+
                    |
              有Try失败
                    |
                    v
            +---------+
            | Cancel  |--------------------------+
            |(回滚释放)|                            |
            +----+----+                            |
                 |                                 v
                 v                    +---------+    +---------+
            +---------+               | 库存Cancel|    | 账户Cancel|
            | 订单Cancel|              | 释放库存  |    | 解冻金额  |
            | 删除订单  |              +---------+    +---------+
            +---------+

TCC 设计要点

  1. 幂等性:Confirm/Cancel 必须幂等(网络重试会多次调用)
  2. 空回滚:Try没执行,Cancel被调用了(如Try超时),需要处理
  3. 悬挂:Cancel先执行,Try后到(网络延迟),Try要拒绝
  4. 业务侵入:每个业务操作要拆成3个方法,改动大

优缺点

优点 缺点
无全局锁,性能高 业务侵入性强,开发成本高
数据最终一致性,适合高并发 幂等性、空回滚、悬挂等问题需要处理
业务层面控制,灵活 回滚逻辑复杂

适用:电商高并发场景(如秒杀)、金融支付
代表框架:ByteTCC、TCC-Transaction、Seata TCC模式

失败后的业务处理

TCC 是同步方案,Try 阶段预留资源,Confirm 阶段确认执行,Cancel 阶段回滚释放。失败会立即返回给调用方。

用户点击"秒杀"
     ↓
TCC Try 阶段(预留资源)
     ↓
   ┌────┴────┐
   ↓         ↓
  全部成功   有失败
   ↓         ↓
 Confirm   Cancel
   ↓         ↓
  成功      失败
   ↓         ↓
返回成功   返回错误

电商实际案例:秒杀扣库存失败

场景:用户参与秒杀,抢购 1 件商品

Try 阶段:
  - 订单服务:创建待确认订单 ✓
  - 库存服务:预扣库存(冻结 1 件)✓
  - 账户服务:冻结金额(冻结 99 元)✗(余额不足)

Cancel 阶段:
  - 订单服务:删除待确认订单
  - 库存服务:释放冻结的 1 件库存
  - 账户服务:无需操作(Try 未成功)

业务处理:
  - 返回用户:"秒杀失败,账户余额不足"
  - 建议用户:"当前余额 XX 元,商品需 99 元,请充值后重试"
  - 前端处理:显示充值入口,保留秒杀商品信息
  - 记录日志:分析秒杀失败原因分布

极端情况:Cancel 失败
  - 库存释放失败 → 库存被冻结,其他用户无法购买
  - 需要定时任务扫描超时冻结库存,自动释放
  - 或人工介入释放库存

面试要点

  • TCC 是同步方案,失败立即返回,用户感知明显
  • Cancel 必须幂等,网络重试会多次调用
  • 空回滚和悬挂问题需要处理(Try 超时、Cancel 先执行)
  • 超时冻结资源需要定时释放,避免资源泄漏

四、其他分布式事务解决方案


方案 4:Saga 模式(长事务拆分)

核心思想

将长事务拆分为多个本地事务,每个本地事务有对应的补偿操作。

+---------+    +---------+    +---------+    +---------+
| T1:创建订单 |-->| T2:扣库存  |-->| T3:扣余额  |-->| T4:发优惠券 |
|  本地事务   |    |  本地事务   |    |  本地事务   |    |  本地事务   |
+----+----+    +----+----+    +----+----+    +----+----+
     |              |              |              |
     v              v              v              -
   成功            成功            失败!          -
     |              |              |              |
     +--------------+------+-------+              |
                           |                     |
                    +-------------+               |
                    |  补偿回滚     |               |
                    | C3:退还余额  |               |
                    | C2:恢复库存  |               |
                    | C1:取消订单  |               |
                    +-------------+               |

两种实现方式

1. 编排式 Saga(Choreography)

每个服务完成本地事务后,发送事件到下一个服务。失败时发送补偿事件,反向执行补偿。

订单服务 --订单创建事件--> 库存服务 --库存扣减事件--> 账户服务
   ^                                              |
   +------------ 补偿事件 <------------------------+

2. 协调式 Saga(Orchestration)

由 Saga 协调器统一调度各服务的执行和补偿。

适用:长事务业务(如旅游预订:订机票→订酒店→租车→买保险)
代表框架:Seata Saga、Axon Framework

失败后的业务处理

Saga 是长事务,补偿执行后业务状态复杂,不能简单返回"失败"。用户可能已经看到中间状态。

场景:旅游预订(机票 → 酒店 → 租车)

执行:订机票 ✓ → 订酒店 ✓ → 租车 ✗(无车可租)
           ↓
      Saga 补偿:取消酒店 ✓ → 退机票 ✓
           ↓
      业务结果:用户已经看到"机票酒店都订好了",突然全没了

错误处理(不能简单返回"失败"):
  ✗ 不好:"预订失败"(用户懵逼:我刚看到都订好了?)
  ✓ 正确:"租车服务暂不可用,已为您取消机票和酒店预订"
         "推荐以下替代方案:"
         "1. 选择其他日期租车"
         "2. 先完成机票酒店预订,租车稍后单独处理"
         "3. 更换目的地"

电商实际案例:订单履约 Saga 失败

场景:用户下单后,订单履约流程

正常流程:
  下单 ✓ → 仓库拣货 ✓ → 打包 ✓ → 发货 ✓ → 物流运输 ✓ → 签收 ✓

失败场景:打包环节发现商品破损

Saga 补偿:
  发货(未执行)
  打包(回滚:取消打包,商品退回货架)
  拣货(回滚:取消拣货,库存恢复)
  订单(补偿:标记为"缺货待处理")

业务处理:
  1. 系统自动:
     - 订单状态改为"缺货待处理"
     - 给用户发送短信/推送:"您购买的XX商品库存异常,正在为您调货"
  
  2. 系统自动化处理:
     - 查询其他仓库是否有库存
     - 有 → 自动转单到其他仓库重新履约
     - 无 → 进入采购流程,预计到货时间告知用户
  
  3. 用户侧选择:
     - 等待调货(系统到货后自动发货)
     - 更换同款其他颜色/规格
     - 取消订单全额退款
     - 联系客服协商补偿(如赠送优惠券)

  4. 兜底:
     - 超过承诺发货时间 → 自动触发赔付(延迟发货补偿)
     - 超过最大等待时间 → 自动退款 + 补偿

面试要点

  • Saga 补偿后,用户可能已经看到中间状态,不能简单返回"失败"
  • 必须给出替代方案,让用户有选择权
  • 系统要有自动化处理能力(转仓、调货)
  • 必须有超时兜底机制(自动退款/补偿)

方案 5:Seata AT 模式(自动补偿,最常用)

核心思想

自动代理数据源,拦截SQL生成反向SQL,失败时自动回滚。

+---------------------------------------------------------+
|                    业务应用(无侵入)                        |
|              订单服务 / 库存服务 / 账户服务                    |
+---------------------------------------------------------+
                           |
                           v
+---------------------------------------------------------+
|              Seata Client(数据源代理)                     |
|         自动拦截SQL,解析生成undo_log                      |
+---------------------------------------------------------+
                           |
                           v
+---------------------------------------------------------+
|              TC (Transaction Coordinator)                |
|                  全局事务协调器                            |
|              维护全局事务状态,驱动二阶段提交                  |
+---------------------------------------------------------+

AT 模式执行流程

一阶段(业务执行)

  1. 业务SQL正常执行
  2. Seata解析SQL,查询前镜像(修改前的数据)
  3. 执行SQL
  4. 查询后镜像(修改后的数据)
  5. 插入undo_log(前镜像 + 后镜像,用于回滚)
  6. 注册分支事务到TC
  7. 提交本地事务

二阶段(全局提交/回滚)

  • 全局提交:异步删除undo_log
  • 全局回滚:根据undo_log生成反向SQL,执行回滚

电商实际业务案例:用户下单全流程

业务场景:用户在电商平台下单购买1件商品,需要同时完成:创建订单、扣减库存、扣减用户余额。

系统架构

+-------------------------------------------------------------+
|                        用户下单请求                            |
|                   商品ID: 1001, 数量: 1, 价格: 199元          |
+-------------------------------------------------------------+
                              |
                              v
+-------------------------------------------------------------+
|                         订单服务                              |
|  @GlobalTransactional  <-- Seata 全局事务注解                  |
|  createOrder() {                                            |
|    1. 创建订单(订单DB)                                       |
|    2. 调用库存服务扣减库存                                      |
|    3. 调用账户服务扣减余额                                      |
|  }                                                          |
+-------------------------------------------------------------+
       |              |                  |
       v              v                  v
+-------------+  +-------------+  +-------------+
|   订单DB     |  |   库存DB     |  |   账户DB     |
|  (独立数据库) |  |  (独立数据库) |  |  (独立数据库) |
+-------------+  +-------------+  +-------------+

一阶段执行过程

订单服务 (TM - 事务管理器)
    |
    v
开始全局事务 (向TC申请 XID)
    |
    +---> 执行本地SQL: INSERT INTO orders (id, user_id, sku_id, amount, status)
    |                    VALUES ('O20240512001', 'U10086', 'SKU1001', 199, 'UNPAID')
    |
    |     Seata代理拦截:
    |     1. 查询前镜像: SELECT * FROM orders WHERE id = 'O20240512001'
    |        → 结果为空(新插入)
    |     2. 执行业务SQL(插入订单)
    |     3. 查询后镜像: SELECT * FROM orders WHERE id = 'O20240512001'
    |        → {id: 'O20240512001', user_id: 'U10086', sku_id: 'SKU1001', amount: 199, status: 'UNPAID'}
    |     4. 生成undo_log(记录INSERT的反向操作DELETE)
    |     5. 注册分支事务到TC
    |     6. 提交本地事务
    |
    +---> RPC调用库存服务 (RM - 资源管理器)
    |     |
    |     v
    |     库存服务执行: UPDATE stock SET count = count - 1, freeze_count = freeze_count + 1
    |                   WHERE sku_id = 'SKU1001' AND count >= 1
    |     |
    |     Seata代理拦截:
    |     1. 查询前镜像: SELECT * FROM stock WHERE sku_id = 'SKU1001'
    |        → {sku_id: 'SKU1001', count: 500, freeze_count: 0}
    |     2. 执行业务SQL(扣减库存)
    |     3. 查询后镜像: SELECT * FROM stock WHERE sku_id = 'SKU1001'
    |        → {sku_id: 'SKU1001', count: 499, freeze_count: 1}
    |     4. 生成undo_log:
    |        {
    |          "beforeImage": {"sku_id": "SKU1001", "count": 500, "freeze_count": 0},
    |          "afterImage":  {"sku_id": "SKU1001", "count": 499, "freeze_count": 1},
    |          "sqlType": "UPDATE"
    |        }
    |     5. 注册分支事务到TC
    |     6. 提交本地事务
    |
    +---> RPC调用账户服务 (RM - 资源管理器)
          |
          v
          账户服务执行: UPDATE account SET balance = balance - 199
                        WHERE user_id = 'U10086' AND balance >= 199
          |
          Seata代理拦截:
          1. 查询前镜像: SELECT * FROM account WHERE user_id = 'U10086'
             → {user_id: 'U10086', balance: 1000}
          2. 执行业务SQL(扣减余额)
          3. 查询后镜像: SELECT * FROM account WHERE user_id = 'U10086'
             → {user_id: 'U10086', balance: 801}
          4. 生成undo_log:
             {
               "beforeImage": {"user_id": "U10086", "balance": 1000},
               "afterImage":  {"user_id": "U10086", "balance": 801},
               "sqlType": "UPDATE"
             }
          5. 注册分支事务到TC
          6. 提交本地事务

一阶段结束,所有本地事务已提交,undo_log已记录

二阶段 - 全局提交(成功场景)

订单服务业务代码执行完毕,无异常抛出
    |
    v
TM 向 TC 发起全局提交请求
    |
    v
TC 异步通知各RM删除undo_log
    |
    +---> 订单DB: DELETE FROM undo_log WHERE xid = 'xxx'
    +---> 库存DB: DELETE FROM undo_log WHERE xid = 'xxx'
    +---> 账户DB: DELETE FROM undo_log WHERE xid = 'xxx'
    |
    v
全局事务结束,数据最终状态:
  订单: {id: 'O20240512001', status: 'UNPAID', ...}
  库存: {sku_id: 'SKU1001', count: 499, freeze_count: 1}
  账户: {user_id: 'U10086', balance: 801}

二阶段 - 全局回滚(失败场景)

假设:账户余额不足,账户服务抛出异常
    |
    v
TM 捕获异常,向 TC 发起全局回滚请求
    |
    v
TC 通知各RM执行回滚
    |
    +---> 订单DB: 根据undo_log执行反向操作
    |     undo_log记录的是INSERT操作
    |     反向SQL: DELETE FROM orders WHERE id = 'O20240512001'
    |     执行后删除undo_log
    |
    +---> 库存DB: 根据undo_log执行反向操作
    |     undo_log记录的是UPDATE操作
    |     反向SQL: UPDATE stock SET count = 500, freeze_count = 0
    |               WHERE sku_id = 'SKU1001'
    |     执行后删除undo_log
    |
    +---> 账户DB: 根据undo_log执行反向操作
          undo_log记录的是UPDATE操作(实际未成功,但已生成日志)
          反向SQL: UPDATE account SET balance = 1000
                    WHERE user_id = 'U10086'
          执行后删除undo_log
    |
    v
全局事务结束,数据恢复到事务前状态:
  订单: 订单记录被删除(仿佛从未创建)
  库存: {sku_id: 'SKU1001', count: 500, freeze_count: 0}(恢复)
  账户: {user_id: 'U10086', balance: 1000}(恢复)

核心代码示例

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private StockFeignClient stockFeignClient;

    @Autowired
    private AccountFeignClient accountFeignClient;

    // Seata 全局事务注解 - 业务无侵入的核心
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public Order createOrder(Long userId, Long skuId, Integer count) {
        // 1. 计算订单金额
        BigDecimal price = getSkuPrice(skuId);
        BigDecimal totalAmount = price.multiply(new BigDecimal(count));

        // 2. 创建订单(本地事务,Seata自动代理)
        Order order = new Order();
        order.setUserId(userId);
        order.setSkuId(skuId);
        order.setCount(count);
        order.setAmount(totalAmount);
        order.setStatus(OrderStatus.UNPAID);
        orderMapper.insert(order);

        // 3. 扣减库存(RPC调用,Seata自动传递XID)
        stockFeignClient.deduct(skuId, count);

        // 4. 扣减余额(RPC调用,Seata自动传递XID)
        accountFeignClient.debit(userId, totalAmount);

        // 5. 更新订单状态为已支付
        order.setStatus(OrderStatus.PAID);
        orderMapper.updateById(order);

        return order;
    }
}

关键点:业务代码中完全看不到事务管理逻辑,只需要一个 @GlobalTransactional 注解,Seata 自动完成全局事务的协调、undo_log 的生成与回滚。

优缺点

优点 缺点
对业务零侵入(自动代理) 依赖Seata中间件
性能较好(一阶段就提交本地事务) 有脏读风险(一阶段已提交,二阶段可能回滚)
使用简单,学习成本低 复杂SQL支持有限

适用:大多数微服务场景,电商、金融等
代表框架:Seata(阿里巴巴开源)

失败后的业务处理

Seata AT 是同步方案,失败会立即返回给调用方。回滚是自动的,但业务异常处理需要手动实现。

用户点击"下单"
     ↓
分布式事务执行
     ↓
   ┌────┴────┐
   ↓         ↓
  成功      失败
   ↓         ↓
返回订单   自动回滚数据(Seata自动)
            ↓
         判断失败原因
         ├─ 库存不足 → 返回:"库存不足,剩余X件,请减少数量"
         ├─ 余额不足 → 返回:"余额不足,请充值或更换支付方式"
         ├─ 服务超时 → 返回:"系统繁忙,请稍后重试"
         └─ 未知异常 → 返回:"下单失败,请稍后重试"
                      ↓
                   记录失败日志(用于分析)
                      ↓
                   监控告警(如果大量同类失败)

电商实际案例:Seata AT 下单失败

@Service
public class OrderService {

    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public Order createOrder(CreateOrderRequest request) {
        try {
            // 1. 创建订单
            Order order = orderMapper.insert(buildOrder(request));

            // 2. 扣减库存(RPC调用库存服务)
            stockFeignClient.deduct(request.getSkuId(), request.getCount());

            // 3. 扣减余额(RPC调用账户服务)
            accountFeignClient.debit(request.getUserId(), request.getAmount());

            // 全部成功 → 返回订单
            return order;

        } catch (StockNotEnoughException e) {
            // Seata 自动回滚:订单删除、库存恢复、余额恢复
            // 业务处理:返回具体错误,引导用户重新操作
            log.warn("下单失败-库存不足, skuId={}, requestCount={}, available={}",
                    request.getSkuId(), request.getCount(), e.getAvailableStock());
            throw new BizException(ResultCode.STOCK_NOT_ENOUGH,
                    "商品库存不足,当前仅剩 " + e.getAvailableStock() + " 件,请减少购买数量");

        } catch (BalanceNotEnoughException e) {
            // Seata 自动回滚
            // 业务处理:提示用户充值或换支付方式
            log.warn("下单失败-余额不足, userId={}, need={}, available={}",
                    request.getUserId(), request.getAmount(), e.getAvailableBalance());
            throw new BizException(ResultCode.BALANCE_NOT_ENOUGH,
                    "账户余额不足,当前余额 " + e.getAvailableBalance() + " 元,请充值或更换支付方式");

        } catch (FeignException e) {
            // 下游服务超时或不可用
            // Seata 自动回滚
            // 业务处理:通用提示,建议重试
            log.error("下单失败-服务调用异常, userId={}, skuId={}",
                    request.getUserId(), request.getSkuId(), e);
            throw new BizException(ResultCode.SERVICE_ERROR,
                    "系统繁忙,请稍后重试");

        } catch (Exception e) {
            // 未知异常
            // Seata 自动回滚
            log.error("下单失败-未知异常, request={}", request, e);
            throw new BizException(ResultCode.SYSTEM_ERROR,
                    "下单失败,请稍后重试或联系客服");
        }
    }
}

面试要点

  • Seata AT 回滚是自动的,不需要手动写回滚代码
  • 但业务异常处理必须自己写,要区分不同错误类型
  • 错误信息要具体,让用户知道下一步怎么做
  • 必须记录日志,方便排查问题

方案 6:最大努力通知(最简单,适用特定场景)

核心思想

服务执行本地事务后,通过消息通知下游,失败则重试,最终成功。

+---------+      +---------+      +---------+
| 支付服务  |----->|  消息队列  |----->|  订单服务  |
|(支付成功) |      |(可靠投递) |      |(更新状态) |
+---------+      +---------+      +---------+
     |                                |
     |         +---------+            |
     +-------->| 对账系统  |<-----------+
               |(兜底核对) |
               +---------+

特点

  • 单向通知,不需要回滚
  • 配合对账系统兜底
  • 最终一致性,延迟较大

适用:支付回调、对账场景

失败后的业务处理

最大努力通知是异步方案,一阶段本地事务已提交,用户已收到成功响应。二阶段通知可能失败。

用户点击"支付"
     ↓
本地事务成功(记录支付流水)
     ↓
立即返回"支付成功"
     ↓
异步通知订单服务 ←── 可能失败
     ↓
   ┌────┴────┐
   ↓         ↓
  成功      失败
   ↓         ↓
更新订单    进入重试队列
   状态      ↓
            定时重试(指数退避)
            ├─ 重试成功 → 更新订单状态
            └─ 重试耗尽 → 进入死信队列
                           ↓
                      人工介入 / 自动退款
                           ↓
                      对账系统每日兜底核对

电商实际案例:支付成功但订单状态未更新

场景:用户支付 199 元,支付流水已记录,但通知订单服务失败

用户看到:支付成功 ✓
订单页面:还是"待支付"

系统处理:
  1. 消息进入重试队列,每隔 1s → 2s → 4s → 8s → ... 重试
  2. 重试 10 次仍失败 → 进入死信队列
  3. 对账系统每日扫描:
     - 发现支付流水有记录,但订单状态还是"待支付"
     - 自动补单:更新订单状态为"已支付"
     - 或自动退款:如果订单已超时关闭
  4. 超过 24 小时仍未处理 → 客服工单系统自动创建

用户侧处理:
  - 用户在"我的订单"看到状态不一致
  - 可点击"刷新状态"触发主动查询
  - 或联系客服,客服通过后台对账工具手动处理

重试机制设计(指数退避)

@Component
public class PaymentNotifyRetryService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private PaymentNotifyRecordMapper notifyRecordMapper;

    // 最大重试次数
    private static final int MAX_RETRY = 10;

    // 重试间隔(毫秒):1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s
    private long getRetryInterval(int retryCount) {
        return (long) Math.pow(2, retryCount) * 1000;
    }

    public void sendNotify(PaymentNotifyMessage message) {
        try {
            // 调用订单服务更新状态
            orderFeignClient.updatePayStatus(message.getOrderId(), message.getPayStatus());

            // 成功 → 更新通知记录为成功
            notifyRecordMapper.updateStatus(message.getRecordId(), NotifyStatus.SUCCESS);

        } catch (Exception e) {
            // 失败 → 判断是否需要重试
            if (message.getRetryCount() < MAX_RETRY) {
                // 增加重试次数
                message.setRetryCount(message.getRetryCount() + 1);

                // 计算下次重试时间
                long delay = getRetryInterval(message.getRetryCount());

                // 发送延迟消息到队列
                rabbitTemplate.convertAndSend(
                    "payment.notify.delay.exchange",
                    "payment.notify.delay.routingKey",
                    message,
                    msg -> {
                        msg.getMessageProperties().setDelay((int) delay);
                        return msg;
                    }
                );

                log.warn("支付通知失败,进入第{}次重试, orderId={}, delay={}ms",
                        message.getRetryCount(), message.getOrderId(), delay);

            } else {
                // 超过最大重试次数 → 进入死信队列
                rabbitTemplate.convertAndSend(
                    "payment.notify.dlx.exchange",
                    "payment.notify.dlx.routingKey",
                    message
                );

                notifyRecordMapper.updateStatus(message.getRecordId(), NotifyStatus.DEAD_LETTER);

                log.error("支付通知重试耗尽,进入死信队列, orderId={}", message.getOrderId());

                // 发送告警通知运维/客服
                alertService.sendAlert("支付通知死信", "orderId=" + message.getOrderId());
            }
        }
    }
}

面试要点

  • 异步方案用户已经收到成功响应,后续失败用户无感知
  • 必须有重试机制,指数退避避免压垮下游
  • 必须有死信队列,重试耗尽后人工介入
  • 必须有对账系统兜底,每日核对数据一致性
  • 客服系统要能看到异常订单,方便用户咨询时处理

五、方案对比总结

方案 一致性 性能 复杂度 侵入性 适用
本地消息表 最终一致 通用
2PC 强一致 低频金融
TCC 最终一致 高并发
Saga 最终一致 长事务
Seata AT 最终一致 通用
最大努力通知 最终一致 通知类

六、电商场景选型建议

                    是否需要强一致性?
                         |
            +------------+------------+
            |                         |
            v                         v
           是                         否
            |                         |
            v                         v
      使用 2PC(极少)            是否高并发?
      或 Seata XA模式                |
                                  +---+---+
                                  |       |
                                  v       v
                                 是       否
                                  |       |
                                  v       v
                            使用 TCC    是否长事务?
                            或 Seata AT    |
                                        +---+---+
                                        |       |
                                        v       v
                                       是       否
                                        |       |
                                        v       v
                                      Saga    Seata AT
                                             或本地消息表

实际电商推荐

场景 推荐方案
常规业务 Seata AT(无侵入,性能好)
秒杀/大促 TCC(性能最高,需自己实现)
复杂流程 Saga(如订单履约全流程)
支付回调 最大努力通知 + 对账

七、六种方案主流使用情况(业界现状)

主流程度排名

排名 方案 主流程度 现状
🥇 Seata AT 模式 ⭐⭐⭐⭐⭐ 最主流,阿里开源,国内大厂广泛采用
🥈 本地消息表 ⭐⭐⭐⭐⭐ 非常主流,自研方案首选,几乎所有大厂都在用
🥉 TCC ⭐⭐⭐⭐ 主流,高并发场景标配,但实现成本高
4 Saga ⭐⭐⭐ 较主流,长事务场景使用,Seata Saga 逐渐普及
5 最大努力通知 ⭐⭐⭐ 特定场景主流,支付回调、对账必用
6 2PC/XA ⭐⭐ 极少使用,仅金融核心系统偶尔采用

各方案详细现状

🥇 Seata AT 模式 — 当前最主流

国内大厂采用情况:

  • 阿里巴巴(发源地,内部大规模使用)
  • 蚂蚁集团、美团、滴滴、字节跳动、京东
  • 众多中小型互联网公司

框架生态:

  • Seata(Java,最成熟)
  • dtm(Go,新兴,支持多模式)
  • EasyTransaction(Java)

为什么最主流?

  • 对业务零侵入,接入成本低
  • 性能足够好(一阶段本地提交)
  • 阿里背书,社区活跃,文档完善
  • 支持多种模式(AT、TCC、Saga、XA)

典型使用场景: 电商订单、支付、库存等常规微服务事务


🥈 本地消息表 — 自研方案首选

采用情况:

  • 几乎所有大厂都有自研实现
    • 阿里:内部消息中间件(RocketMQ 事务消息)
    • 美团:自研消息平台
    • 京东:JMQ 事务消息
    • 拼多多:自研消息系统
  • 中小厂基于 RocketMQ/Kafka 自研
  • 配合定时任务 + 消息队列实现

技术栈:

  • RocketMQ 事务消息(最常用)
  • Kafka + 本地消息表
  • RabbitMQ + 补偿机制

为什么主流?

  • 实现简单,不依赖外部框架
  • 与现有 MQ 基础设施结合
  • 最终一致性满足绝大多数业务需求
  • 阿里 RocketMQ 原生支持事务消息,开箱即用

典型使用场景: 订单创建后发消息、支付成功后通知、数据同步


🥉 TCC — 高并发场景标配

采用情况:

  • 阿里(内部 TCC 框架,秒杀大促场景)
  • 美团(高并发支付场景)
  • 京东(库存扣减)
  • 字节(部分核心业务)
  • 金融类公司(银行、证券)

开源框架:

  • ByteTCC(较老,维护少)
  • TCC-Transaction(个人开源)
  • Seata TCC 模式(推荐,生态好)
  • dtm(Go,支持 TCC)

为什么主流但门槛高?

  • 性能最好,无全局锁
  • 业务侵入性强,每个接口要拆3个方法
  • 需要处理幂等、空回滚、悬挂等问题
  • 开发成本高,只用在真正需要高性能的场景

典型使用场景: 秒杀扣库存、红包发放、高并发支付


Saga — 长事务场景逐渐普及

采用情况:

  • 阿里(Seata Saga 模式)
  • 携程(旅游预订长流程)
  • 飞猪(机票+酒店+租车组合)
  • 保险行业(复杂投保流程)
  • 跨境电商(清关+物流+支付长链路)

框架:

  • Seata Saga(基于状态机引擎)
  • Axon Framework(Java)
  • dtm(Go,支持 Saga)

为什么较主流?

  • 微服务拆分越来越细,长事务场景增多
  • Seata Saga 降低了使用门槛
  • 适合业务流程复杂、步骤多的场景

典型使用场景: 旅游预订、保险投保、跨境电商履约、供应链长流程


最大努力通知 — 支付场景必用

采用情况:

  • 几乎所有支付系统都在用
  • 微信支付回调、支付宝回调、银联支付通知
  • 各银行支付系统

实现方式:

  • MQ 可靠投递 + 重试机制
  • 对账系统兜底
  • 定时补偿任务

为什么特定场景主流?

  • 支付是单向通知,不需要回滚
  • 配合对账系统,最终一定能一致
  • 简单可靠,成本最低

典型使用场景: 支付结果通知、退款通知、第三方回调


2PC/XA — 极少使用

采用情况:

  • 传统金融核心系统(银行核心账务)
  • 部分 Oracle 数据库场景
  • 遗留系统改造困难时保留
  • 学术研究、教学演示

框架:

  • Atomikos(Java,较老)
  • Bitronix(Java,较老)
  • Seata XA 模式(新选择,但用的人少)
  • 各数据库原生 XA 实现

为什么极少使用?

  • 同步阻塞,性能极差
  • 协调者单点故障问题
  • 互联网高并发场景完全不适合
  • 仅在对一致性要求极高、并发极低的金融核心保留

主流技术选型建议(2024-2025)

+-------------------------------------------------------------+
|                    技术选型决策树                            |
+-------------------------------------------------------------+
|                                                             |
|  1. 是否强一致性且并发极低?                                  |
|     → 是:Seata XA 或 2PC(极少场景)                        |
|     → 否:继续判断                                           |
|                                                             |
|  2. 是否超高并发(秒杀级别)?                                |
|     → 是:TCC(Seata TCC 或自研)                           |
|     → 否:继续判断                                           |
|                                                             |
|  3. 是否长事务(多步骤业务流程)?                            |
|     → 是:Saga(Seata Saga)                                |
|     → 否:继续判断                                           |
|                                                             |
|  4. 是否单向通知(支付回调类)?                              |
|     → 是:最大努力通知 + 对账                                |
|     → 否:继续判断                                           |
|                                                             |
|  5. 是否有现成 MQ 基础设施?                                  |
|     → 是:本地消息表 / RocketMQ 事务消息                      |
|     → 否:Seata AT(最推荐,零侵入)                         |
|                                                             |
+-------------------------------------------------------------+

一句话总结

方案 一句话定位
Seata AT 2024年首选方案,没有特殊要求就用它
本地消息表 有 MQ 基础设施时的自研首选
TCC 秒杀大促时的性能终极方案
Saga 复杂业务流程的长事务解决方案
最大努力通知 支付回调的标准做法
2PC/XA 金融核心的古董方案,新项目别用

目前国内互联网公司的实际情况是:Seata AT + 本地消息表 覆盖了 80% 的场景,TCC 覆盖 15% 的高并发场景,其余方案占 5%。


八、关键设计原则与面试高频问题

关键设计原则

原则 说明 电商实践
快速失败 同步场景立即返回错误,不要让用户空等 下单超时 3 秒未响应,前端提示"网络繁忙"
具体错误 告诉用户具体原因和解决办法 "库存不足"比"系统错误"好 100 倍
自动重试 异步场景要有自动重试机制 支付通知指数退避重试 10 次
指数退避 重试间隔逐渐增大,避免压垮下游 1s → 2s → 4s → 8s → ...
死信队列 重试耗尽后进入死信队列,人工或自动处理 超过 10 次自动创建客服工单
对账兜底 每日对账发现不一致,自动修复或告警 凌晨 3 点对账,异常自动补单/退款
监控告警 大量失败时及时告警,快速发现问题 下单失败率 > 5% 立即告警
优雅降级 非核心服务失败时,可选择跳过继续主流程 优惠券服务挂了,不用优惠券继续下单
用户自助 提供用户自助查询/刷新/重试能力 "刷新支付状态"、"重新提交订单"
客服工具 客服后台要有完整的异常处理工具 手动补单、手动退款、发放补偿券

面试高频问题

Q1:Seata AT 回滚后,业务上怎么处理?

Seata AT 的回滚是自动的,根据 undo_log 生成反向 SQL 执行。业务上需要在 @GlobalTransactional 的 catch 块中捕获具体异常,区分失败原因(库存不足、余额不足、服务超时等),返回具体的错误信息给用户,引导用户下一步操作。同时要记录日志,方便排查问题。

Q2:支付成功但订单状态没更新,怎么办?

这是异步通知的典型问题。处理方式是:

  1. 支付回调进入延迟重试队列,指数退避重试
  2. 重试耗尽进入死信队列,触发告警
  3. 对账系统每日兜底,发现不一致自动补单或退款
  4. 用户可主动点击"刷新支付状态"触发查询
  5. 客服后台可手动补单
  6. 超过一定时间自动退款,保护用户权益

Q3:Saga 补偿后,用户已经看到中间状态了,怎么处理?

Saga 补偿不能简单返回"失败",因为用户可能已经收到"机票酒店预订成功"的通知。正确的做法是:

  1. 明确告知用户发生了什么("租车服务暂不可用")
  2. 告知用户已做的处理("已为您取消机票和酒店预订")
  3. 提供替代方案("推荐其他日期"、"先订机票酒店")
  4. 系统自动化处理(转仓、调货)
  5. 超时自动兜底(自动退款 + 补偿)

Q4:分布式事务失败了,数据已经回滚了,但用户钱已经扣了怎么办?

这种情况理论上不应该发生(因为回滚会恢复余额),但如果真的发生了:

  1. 对账系统会发现支付流水和订单状态不一致
  2. 自动触发退款流程
  3. 如果自动退款失败,进入人工处理流程
  4. 客服主动联系用户,手动退款
  5. 同时排查为什么回滚没成功,修复系统 Bug

九、总结

分布式事务没有银弹,需要根据业务场景选择合适的方案:

  1. 简单场景:本地消息表、最大努力通知
  2. 通用场景:Seata AT(推荐)
  3. 高并发场景:TCC
  4. 长事务场景:Saga
  5. 强一致场景:2PC/XA(极少使用)

技术回滚保证数据正确,业务处理保证用户体验。 理解各方案的原理和适用场景,才能在电商业务中做出正确的技术选型。

posted @ 2026-05-19 11:24  SeiunSky  阅读(58)  评论(0)    收藏  举报