分布式事务解决方案
电商业务分布式事务解决方案
本文系统介绍电商业务中分布式事务的多种解决方案,从简单到复杂逐一分析。
目录
- 一、电商业务中的分布式事务场景
- 二、分布式事务的本质
- 三、分布式事务解决方案(从简单到复杂)
- 四、其他分布式事务解决方案
- 五、方案对比总结
- 六、电商场景选型建议
- 七、六种方案主流使用情况(业界现状)
- 八、关键设计原则与面试高频问题
- 九、总结
一、电商业务中的分布式事务场景
典型场景:下单扣库存
+-------------+ +-------------+ +-------------+
| 订单服务 | | 库存服务 | | 账户服务 |
| (创建订单) | | (扣减库存) | | (扣减余额) |
+------+------+ +------+------+ +------+------+
| | |
+-------------------+-------------------+
三个服务,三个独立数据库
问题:创建订单、扣库存、扣余额,这三个操作必须同时成功或同时失败,但每个服务有自己的数据库,本地事务无法保证跨服务的一致性。
二、分布式事务的本质
核心逻辑
分布式事务的本质,是所有方案都在解决同一个问题:
多个服务要一起成功,或者一起失败,不能出现部分成功的中间状态。
不允许的情况:
订单创建了 ✓
库存扣了 ✓
余额没扣 ✗ ← 钱没付,货扣了,订单也有了,系统就乱了
必须保证:
情况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
+-------------+ +-------------+
| 库存服务 | | 账户服务 |
| (消费消息扣库存)| | (消费消息扣余额)|
+-------------+ +-------------+
流程说明
-
订单服务开启本地事务
- 插入订单记录(业务操作)
- 插入消息记录(status=PENDING)
- 提交本地事务(两者原子性保证)
-
定时任务扫描消息表,将PENDING消息发送到MQ
-
库存服务消费消息,扣减库存
- 成功 → 订单服务更新消息status=SENT
- 失败 → 消息重试(幂等性保证)
-
账户服务同理
优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单,不依赖外部框架 | 有延迟 |
| 最终一致性 | 需要保证消费者幂等性 |
适用:电商下单、支付通知等对实时性要求不高的场景
失败后的业务处理
本地消息表是异步方案,一阶段本地事务已提交,用户已收到成功响应。二阶段消息消费可能失败。
用户点击"下单"
↓
本地事务成功(订单已创建,消息已记录)
↓
立即返回"下单成功"
↓
定时任务扫描消息表发送到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 设计要点
- 幂等性:Confirm/Cancel 必须幂等(网络重试会多次调用)
- 空回滚:Try没执行,Cancel被调用了(如Try超时),需要处理
- 悬挂:Cancel先执行,Try后到(网络延迟),Try要拒绝
- 业务侵入:每个业务操作要拆成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 模式执行流程
一阶段(业务执行):
- 业务SQL正常执行
- Seata解析SQL,查询前镜像(修改前的数据)
- 执行SQL
- 查询后镜像(修改后的数据)
- 插入undo_log(前镜像 + 后镜像,用于回滚)
- 注册分支事务到TC
- 提交本地事务
二阶段(全局提交/回滚):
- 全局提交:异步删除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:支付成功但订单状态没更新,怎么办?
这是异步通知的典型问题。处理方式是:
- 支付回调进入延迟重试队列,指数退避重试
- 重试耗尽进入死信队列,触发告警
- 对账系统每日兜底,发现不一致自动补单或退款
- 用户可主动点击"刷新支付状态"触发查询
- 客服后台可手动补单
- 超过一定时间自动退款,保护用户权益
Q3:Saga 补偿后,用户已经看到中间状态了,怎么处理?
Saga 补偿不能简单返回"失败",因为用户可能已经收到"机票酒店预订成功"的通知。正确的做法是:
- 明确告知用户发生了什么("租车服务暂不可用")
- 告知用户已做的处理("已为您取消机票和酒店预订")
- 提供替代方案("推荐其他日期"、"先订机票酒店")
- 系统自动化处理(转仓、调货)
- 超时自动兜底(自动退款 + 补偿)
Q4:分布式事务失败了,数据已经回滚了,但用户钱已经扣了怎么办?
这种情况理论上不应该发生(因为回滚会恢复余额),但如果真的发生了:
- 对账系统会发现支付流水和订单状态不一致
- 自动触发退款流程
- 如果自动退款失败,进入人工处理流程
- 客服主动联系用户,手动退款
- 同时排查为什么回滚没成功,修复系统 Bug
九、总结
分布式事务没有银弹,需要根据业务场景选择合适的方案:
- 简单场景:本地消息表、最大努力通知
- 通用场景:Seata AT(推荐)
- 高并发场景:TCC
- 长事务场景:Saga
- 强一致场景:2PC/XA(极少使用)
技术回滚保证数据正确,业务处理保证用户体验。 理解各方案的原理和适用场景,才能在电商业务中做出正确的技术选型。

浙公网安备 33010602011771号