Seata
Seata 分布式事务
基于 Seata 2.x,结合本项目(catfun:社区 / 地图 / 用户 / 积分)设计
一、Seata 是什么 / 解决什么问题
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里开源的分布式事务框架,解决微服务调用链中的数据一致性问题。
微服务化后,一次业务横跨多个服务、多个数据库,本地事务管不到别人家的库,就需要分布式事务框架。
核心卖点:对业务侵入极小——通常一个注解 + 少量配置就能接入,业务 SQL 不用大改。
二、整体架构:TC / TM / RM(必考)
| 角色 | 全称 | 位置 | 职责 |
|---|---|---|---|
| TC | Transaction Coordinator | Seata Server(独立进程) | 全局事务注册、生成 XID、二阶段调度、状态管理、超时重试下发 |
| TM | Transaction Manager | 发起方(业务服务内) | 通过 @GlobalTransactional 开启/提交/回滚全局事务 |
| RM | Resource Manager | 各参与方(业务服务内) | 执行分支事务(AT 的 SQL 代理 / TCC 的三方法),上报分支状态 |
1
2
3
4
5
① TM 开启全局事务 ──► TC 注册,生成 XID
② XID 透传到所有下游服务(Feign Header / Dubbo attachment)
③ 各 RM 执行分支事务并上报 TC
④ TM 收到全部成功 → 通知 TC 提交
⑤ TC 向各 RM 下发二阶段指令(Confirm/Commit 或 Cancel/Rollback,可重试)
XID 透传:XID 是全局事务的唯一标识,必须随 RPC 一路传递。Feign 用 RequestInterceptor 塞 header,Dubbo 用 RpcContext attachment,Seata 已内置拦截器,但要确认没被网关/过滤器吞掉。
三、四大模式总览
| 模式 | 本质 | 一致性 | 侵入 | 锁 | 性能 | 一句话 |
|---|---|---|---|---|---|---|
| AT | 2PC 改良(自动补偿) | 最终一致 | 低(注解) | 全局锁 | 中 | 自动生成 undo_log,回滚靠日志反向补偿 |
| TCC | 业务三方法(手动补偿) | 最终一致 | 高(三方法) | 无(冻结) | 好 | 业务自己写 Try/Confirm/Cancel |
| XA | 原生 2PC(数据库级) | 强一致 | 低 | 长锁 | 差 | 数据库 XA 协议,一阶段 prepare |
| Saga | 长事务(正反业务) | 最终一致 | 中 | 无 | 好 | 正向业务 + 反向补偿,适合长流程 |
四、AT 模式(最常考,必背原理)
4.1 执行流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
一阶段:
① 拦截业务 SQL(DataSourceProxy 代理数据源)
② 执行前查数据 → 生成 before image(前镜像快照)
③ 执行业务 SQL(正常提交本地事务)
④ 执行后查数据 → 生成 after image(后镜像快照)
⑤ 写 undo_log(before + after 序列化存 blob)→ 与业务 SQL 同事务提交
⑥ 向 TC 注册分支事务(申请全局锁)
二阶段提交(全局成功):
TC 通知 → RM 异步删除 undo_log(业务数据已提交,日志没用了)
二阶段回滚(任一分支失败):
TC 通知 → RM 读 undo_log
① 校验 after image 与当前数据是否一致(防止期间被其他事务改过 → 脏写)
② 一致 → 用 before image 生成反向 SQL(DELETE/UPDATE/INSERT),恢复现场
③ 不一致 → 回滚失败,抛异常告警,需要人工介入
4.2 undo_log 是什么
undo_log 是 AT 模式的回滚日志表(每个参与库都要建):
1
2
3
4
5
6
7
8
9
10
CREATE TABLE `undo_log` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`branch_id` BIGINT NOT NULL, -- 分支事务 ID
`xid` VARCHAR(100) NOT NULL, -- 全局事务 ID
`rollback_info` LONGBLOB NOT NULL, -- before/after 镜像序列化数据
`log_status` INT NOT NULL, -- 0=正常 1=已清理
`log_created` DATETIME,
`log_modified` DATETIME,
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE=InnoDB;
为什么能回滚:因为记录了改动前后的完整镜像,回滚时用 before image 生成反向 SQL,等价于”把数据改回原样”。这也是”自动补偿”的含义。
4.3 全局锁与隔离性(高频)
- 全局锁:Seata 在 TC 侧维护一张全局锁表(
global_table/ 内存),不同全局事务对同一行数据的修改要先抢全局锁再提交(同一全局事务内可重入),防止并发写同一行导致 undo_log 回滚时覆盖已提交数据。 - 本地锁 + 全局锁:本地事务锁 + 全局锁双重保障,写-写冲突被挡住。
- 读的隔离:AT 默认 读已提交(RC),普通
SELECT不加全局锁 → 可能脏读(读到别人未提交全局事务的数据)。 - 防脏读:
SELECT ... FOR UPDATE(会申请全局锁)或加@GlobalLock/@GlobalTransactional注解读。
4.4 脏写问题
场景:全局事务 A 改了行 X(未提交),另一个事务 B 直接改行 X 并提交 → A 回滚时 after image 校验失败(数据已被 B 改过)→ 回滚失败。 解法:B 也纳入 Seata(B 的写会抢全局锁),或对 A 的读加锁。全局锁解决的就是这个。
4.5 适用场景
- 跨服务改数据、需要强一致(同生共死)
- 希望侵入小、快速接入
- 低并发、不介意全局锁开销
- 数据库须支持事务(MySQL InnoDB),MyISAM 不支持
五、TCC 模式
5.1 三方法职责
1
2
3
Try 预留资源(冻结金额/预占库存/冻结积分)——不落正式账
Confirm 真正执行业务(使用预留资源)——只做"使用",不做新校验
Cancel 释放预留资源(解冻)——释放自己 Try 冻结的
- Try 全成功 → Confirm;任一 Try 失败 → Cancel 补偿
- 进入 Confirm 后没有回滚,Confirm 失败只能幂等重试(与 2PC 的本质区别)
5.2 fence 表:解决”网络不确定性”的四大问题
TCC 一旦落到工程里,网络会带来两类麻烦:指令可能被重发(TC 超时重试)、指令可能乱序(Cancel 比 Try 先到)。这四大问题全靠一张 fence 表解决。
第一步:一张分支事务状态表
1
2
3
4
5
6
7
tcc_fence_log(fence 表)
┌────────────┬────────────┬────────────┬───────────┬──────────┐
│ xid │ branch_id │ action │ status │ 时间戳 │
│ 全局事务ID │ 分支事务ID │ 方法名 │ 0/1/2 │ │
└────────────┴────────────┴────────────┴───────────┴──────────┘
status: 0=Try 已执行 1=已提交(Confirm) 2=已回滚(Cancel)
主键: (xid, branch_id) ← 唯一约束 = 幂等的基础
四个问题全围绕这张表展开,本质就是三句话:谁先到、谁后到、重复来了怎么办。
问题一:TC 超时重试 Confirm/Cancel
什么意思:TC 给分支下发二阶段指令(Confirm/Cancel)时网络超时,TC 不知道分支到底执行没有,只能重试下发。所以同一个指令可能被送来 2 次、3 次。
为什么必须有:不重试,指令丢了,业务就永远停在”Try 完了但没确认/没回滚”,一直不一致。
怎么做:TC 内置重试机制(Seata Server 侧,无需配置),分支执行完必须上报结果,TC 收不到上报就重发。
问题二:幂等去重(重复的二阶段不执行业务)
什么意思:既然 Confirm/Cancel 会被重发,分支必须保证重复收到时不重复执行业务,否则会重复扣款/重复解冻。
怎么做:先改状态、后执行业务(CAS 更新):
1
2
3
4
Confirm 收到后:
先 UPDATE tcc_fence_log SET status=1 WHERE xid=? AND branch_id=? AND status=0
→ 影响行数 == 1:第一次来,继续执行业务 ✅
→ 影响行数 == 0:执行过了,直接返回成功,不执行业务 ✅
问题三:空回滚判断(Try 没执行,Cancel 却来了)
什么意思:Try 超时(实际没执行),TC 判全局失败,给所有分支发 Cancel——包括那个”其实 Try 根本没执行”的分支。此时 Cancel 如果直接回滚,会把从未冻结的资源解冻(余额变负数那种事故)。
怎么做:Cancel 先查表:
1
2
3
4
Cancel 收到后:
查 tcc_fence_log 有无 (xid, branch_id) 记录
→ 没有记录:Try 从未执行 → 不执行业务,只插入一条 status=2 记录(空回滚)
→ 有记录且 status=0:正常执行回滚
问题四:悬挂防护(Cancel 先到了,迟到的 Try 才来)
什么意思:Try 超时 → Cancel 先执行完(空回滚)→ 那个迟到的 Try 又执行了 → 资源被冻结了,但全局事务已结束,永远不会有人来 Confirm/Cancel → 资源永久悬挂(泄漏)。
怎么做:Try 执行前先查表:
1
2
3
4
Try 执行前:
查 tcc_fence_log 有无 (xid, branch_id) 记录
→ 已有二阶段记录(status=1/2):被 Cancel 过了 → 拒绝执行业务,直接返回成功(防悬挂)
→ 无记录:正常冻结资源,插入 status=0 记录
一张表总结四个问题
| 问题 | 谁来了 | 靠什么防 |
|---|---|---|
| TC 超时重试 | TC 反复下发二阶段 | TC 内置重试 + 分支上报结果 |
| 幂等去重 | Confirm/Cancel 重复执行 | (xid, branch_id) 唯一键 + CAS 更新 |
| 空回滚 | Cancel 先于 Try | Cancel 前查表,无记录只登记不执行业务 |
| 悬挂防护 | Try 晚于 Cancel | Try 前查表,已回滚就拒绝执行 |
一句话记忆:Try 前查表防悬挂,Cancel 前查表防空回滚,Confirm/Cancel 执行时 CAS 更新防重复。
Seata 落地:不用自己写,注解 + fence 表全自动
1
2
3
4
5
6
7
8
9
10
11
12
13
@TwoPhaseBusinessAction(name = "redeem", commitMethod = "confirmRedeem", rollbackMethod = "cancelRedeem")
public boolean tryRedeem(BusinessActionContext ctx,
@BusinessActionContextParameter(paramName = "flowId") String flowId) {
// 冻结积分(Seata 拦截器自动做防悬挂 + 插 fence 记录)
}
public boolean confirmRedeem(BusinessActionContext ctx) {
// 真正扣积分(Seata 自动做幂等去重)
}
public boolean cancelRedeem(BusinessActionContext ctx) {
// 解冻积分(Seata 自动做空回滚判断)
}
1
2
3
4
5
seata:
tcc:
fence:
log-table-name: tcc_fence_log # fence 表名
clean-period: 1h # 过期记录清理周期
5.3 常见坑
- Confirm/Cancel 忘写幂等 → 重复扣款/重复解冻
- 二阶段拿不到 Try 的入参 → 必须
@BusinessActionContextParameter标记,参数走 XID 上下文 - Try 本身防重放 → 事务表唯一约束
- fence 表忘建 → 二阶段直接报错
六、XA 模式
- Seata XA = 标准的数据库级 2PC:一阶段 prepare,二阶段 commit/rollback,由数据库 XA 协议保证。
- 优点:强一致、业务无感知(只要数据库支持 XA)
- 缺点:Prepare 后资源长锁、协调者单点、性能差
- 面试点:Seata 的 XA 模式就是 2PC 的”原教旨”实现;互联网高并发场景基本不用。
七、Saga 模式
- 长事务:每个参与方提供正向业务 + 反向补偿两个方法,Saga 按序执行正向;某步失败,逆序执行已成功步骤的补偿。
- Seata Saga 支持注解和状态机(JSON)两种编排方式。
- 特点:无锁、无隔离(中间态对外可见)、适合流程长/跨多服务/允许最终一致(如:下单 → 支付 → 履约 → 售后)。
- 面试点:Saga 没有 Try 的”预留”概念,隔离性最弱,需要业务容忍中间态。
八、选型对比 + 决策树(必考)
| 维度 | AT | TCC | XA | Saga |
|---|---|---|---|---|
| 一致性 | 最终一致 | 最终一致 | 强一致 | 最终一致 |
| 业务侵入 | 低 | 高 | 低 | 中 |
| 性能 | 中(全局锁+日志) | 好(无锁) | 差(长锁) | 好 |
| 隔离性 | 全局锁保证写隔离 | 冻结机制 | 数据库保证 | 最弱 |
| 长流程 | 不适合 | 不适合 | 不适合 | 适合 |
| 典型场景 | 短事务、跨库改数据 | 高并发、钱/库存/积分 | 银行强一致 | 订单+物流+售后 |
1
2
3
4
5
选型决策:
① 流程很长 / 中间态可接受? ──► Saga
② 强一致(不能容忍任何不一致)? ──► XA(性能换强一致)
③ 有资源概念 + 高并发 + 无锁? ──► TCC
④ 其他跨服务短事务? ──► AT(默认首选)
九、Seata vs 消息队列方案(本地消息表 / 事务消息)
| 维度 | Seata(AT/TCC) | MQ 事务消息 / 本地消息表 |
|---|---|---|
| 时机 | 同步,调用链内完成 | 异步,消息解耦 |
| 一致性 | 最终一致,窗口短 | 最终一致,窗口长 |
| 侵入 | 注解/三方法 | 建表 + 消费者幂等 |
| 回滚 | 框架自动/补偿 | 无回滚,靠补偿消息 |
| 适用 | 必须同步完成、强一致 | 允许异步、削峰解耦 |
本项目积分变更就是 RocketMQ 事务消息(PointTransactionProducer):本地写 cf_point_flow + 投递消息原子化,消费侧 flowId 幂等——因为积分变更允许异步,不需要同步强一致。选 Seata 还是 MQ,核心就是”能不能等”。
十、部署与配置(项目落地)
10.1 Seata Server(TC)部署
- 下载
seata-server,独立进程,可集群 - 配置注册中心:本项目用 Nacos →
registry.type=nacos,TC 注册到 Nacos 供客户端发现 - 配置存储:
store.mode=db(生产用 DB 存事务状态,单机可用 file)
10.2 Client(业务服务)配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
seata:
enabled: true
application-id: catfun-community # 服务名
tx-service-group: catfun_tx_group # 事务分组(与 Server 端一致)
registry:
type: nacos
nacos:
server-addr: ${NACOS_ADDR:127.0.0.1:8848}
service:
vgroup-mapping:
catfun_tx_group: default # 分组映射到 TC 集群
client:
tm:
commit-retry-count: 5 # TM 提交全局事务重试次数
rollback-retry-count: 5 # TM 回滚重试次数
rm:
report-retry-count: 5 # 分支上报重试次数
依赖:seata-spring-boot-starter + 启动类 @EnableAutoDataSourceProxy(AT 模式代理数据源)。
10.3 常见部署坑
tx-service-group与服务端不一致 → 找不到 TC- Nacos 连不上 → 全局事务直接失败
- 网关层吞掉 XID header → 透传中断
- 忘记建 undo_log / fence 表 → 二阶段报错
十一、高频面试题 Q&A
Q1:Seata AT 模式执行流程? 一阶段:代理 SQL → before 快照 → 执行 → after 快照 → 写 undo_log → 注册分支;二阶段提交删日志,回滚靠镜像反向补偿。
Q2:undo_log 为什么能回滚? 记录 before/after 完整镜像,回滚用 before 生成反向 SQL 恢复现场,等价于”改回去”。
Q3:AT 怎么保证隔离性?脏读怎么解决?
全局锁防写-写冲突;普通 SELECT 不加锁可能脏读,用 FOR UPDATE / @GlobalLock 解决。
Q4:Seata 四模式怎么选? 见第八节决策树:短事务默认 AT、资源型高并发 TCC、强一致 XA、长流程 Saga。
Q5:XID 怎么传递? TM 生成,随 RPC 透传(Feign header / Dubbo attachment),RM 解析后关联分支。
Q6:AT 模式回滚失败怎么办? after image 校验不一致(被脏写)→ 抛异常 + 告警 + 人工介入,这也是 AT 的固有缺陷。
Q7:Seata 和 2PC 什么关系? XA 模式 = 原生 2PC;AT = 2PC 改良(一阶段直接提交 + 日志补偿,解决阻塞问题)。
Q8:Seata Server(TC)挂了会怎样? 全局事务无法提交/回滚(分支悬挂)→ 生产必须 TC 集群 + DB 持久化。
Q9:AT 支持哪些数据库? MySQL(InnoDB)/PostgreSQL/Oracle 等,MyISAM 不支持。
Q10:Seata 的性能损耗来自哪? SQL 解析、前后快照查询、undo_log 写入、全局锁申请(约 10%~30% 开销)。
十二、本项目场景设计
场景一:AT 模式 —— 取消订单释放冻结库存(低并发 + 强一致)
涉及服务:catfun-trade(订单,TM 发起)+ catfun-merchant(库存,RM 参与)
背景:下单 TCC 冻结库存后订单待支付(30 分钟),用户取消 / 超时未支付时需同时完成三件事——订单置 CANCELLED、释放冻结库存(unfreeze)、还原优惠券,跨 trade/merchant 两库强一致:不能出现”订单取消了、库存还冻着”(frozen 泄漏、可售库存虚低)。
流程(OrderCancelService.cancel,@GlobalTransactional + 本地 @Transactional):
1
2
3
4
5
6
7
取消订单(用户主动 / 超时定时任务)
├─ ① 本地:Order → CANCELLED(cancelTime / cancelReason 落库)
├─ ② Feign 调 /api/merchant/inventory/unfreeze:frozen - N / stock + N(注册 AT 分支,写 undo_log)
├─ ③ 本地:还原优惠券(coupon_user → UNUSED)
└─ 任一步失败 → 全局回滚
merchant 分支 → undo_log 反向补偿(UPDATE → UPDATE)
本地事务 → 本地 SQL 回滚
为什么选 AT:取消是低频、短事务(每条明细一次 UPDATE)→ AT 全局锁无压力;释放库存无外部副作用,undo_log 物理回滚足够,无需 TCC 三段式;侵入小,只加注解。与场景二互补——高频写(下单冻结)用 TCC 防超卖,低频强一致(取消释放)用 AT。
注意:releaseStock 旧实现失败仅 log.warn 吞异常(造成冻结泄漏),现改为失败抛 BusinessException 触发全局回滚;超时任务每单独立一个全局事务,单笔失败不影响其他订单(避免一个脏单拖垮整批)。
场景二:TCC 模式 —— 下单库存冻结(高并发 + 资源模型)
涉及服务:catfun-trade(订单,TM 发起)+ catfun-merchant(库存,RM 参与)
背景:下单需预占库存防超卖,库存高频扣减不能长时间持行锁;下单方法全部执行完、全局事务即提交(Confirm)——天然”冻结/解冻”资源模型。
资源模型:cf_inventory 表双字段(stock 可售 / frozen 冻结):
| 阶段 | 方法(InventoryTccAction) |
SQL 效果 |
|---|---|---|
| Try | freeze |
stock - N / frozen + N(WHERE stock >= N 原子防超卖) |
| Confirm | confirm |
下单成功后提交(空操作,冻结保留) |
| Cancel | cancel |
frozen - N / stock + N(TC 全局回滚时自动下发) |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Try:预占库存——原子 SQL 防超卖(InventoryRepositoryImpl.freeze)
// 影响 0 行(库存不足)→ 抛 BusinessException 触发全局回滚
public boolean freeze(BusinessActionContext ctx,
@BusinessActionContextParameter(paramName = "productId") String productId,
@BusinessActionContextParameter(paramName = "quantity") Integer quantity) {
if (!inventoryRepository.freeze(productId, quantity)) {
throw new BusinessException("库存不足: productId=" + productId + ", quantity=" + quantity);
}
return true;
}
// Confirm:空操作——下单成功后(全局事务提交时)由 TC 提交,冻结保留
public boolean confirm(BusinessActionContext ctx) { return true; }
// Cancel:解冻库存(TC 下发;productId/quantity 由注解自动持久化到 ctx)
public boolean cancel(BusinessActionContext ctx) {
Object productId = ctx.getActionContext("productId");
Object quantity = ctx.getActionContext("quantity");
return inventoryRepository.unfreeze(String.valueOf(productId), Integer.parseInt(String.valueOf(quantity)));
}
时序(OrderAppService.create,@GlobalTransactional + 本地 @Transactional):
1
2
3
4
5
6
7
8
创建订单
├─ ① 循环 Feign 调 catfun-merchant /api/merchant/inventory/tcc/freeze(TCC Try,注册分支)
│ 原子 SQL 预占库存;返回 data != true → 抛 BusinessException
├─ ② 订单 + 订单明细落库
├─ ③ 核销优惠券 + 清理购物车
└─ 任一步失败 → 全局回滚
本地事务 → 本地 SQL 回滚(undo_log / 本地回滚)
TCC 分支 → TC 下发 Cancel → cancel() 解冻(fence 幂等,见 5.2)
为什么选 TCC:库存高频扣减 → AT 的全局锁(SELECT FOR UPDATE)长事务会成瓶颈;”下单冻结、成功提交、失败解冻”天然契合 Try/Confirm/Cancel 三段语义,且下单成功即提交(Confirm 空操作、冻结保留)正是纯业务需求;fence 表兜底幂等/空回滚/防悬挂,Try 失败走 fence 空回滚,业务侧不需要冻结流水表。
十三、面试速记
- Seata = 微服务分布式事务框架,注解级低侵入
- 三角色:TC(Server 调度)/ TM(发起)/ RM(执行),XID 随 RPC 透传
- AT = 2PC 改良:一阶段提交 + undo_log 镜像,二阶段删日志或反向补偿
- AT 全局锁防脏写;普通读可能脏读,
FOR UPDATE/@GlobalLock解决 - TCC = 手动三方法:Try 冻结 / Confirm 执行 / Cancel 释放;二阶段”超时重试、幂等去重、空回滚、悬挂防护”四大问题靠 fence 表解决
- XA = 原生 2PC(强一致慢);Saga = 长流程正反补偿(隔离最弱)
- 选型:短事务 AT、资源高并发 TCC、强一致 XA、长流程 Saga;能异步就用 MQ
- 本项目:取消订单释放库存 → AT;下单库存冻结 → TCC;积分变更 → RocketMQ 事务消息
十四、AT vs TCC:性能 / 安全区别
结合 catfun-trade(库存冻结 + 订单创建)场景,只看 AT 和 TCC 在性能、安全两个维度的差别,方便选型时对比。
14.1 性能
| 维度 | AT | TCC |
|---|---|---|
| 网络交互 | 每分支注册+上报 ≈ 2N 次 RPC | Try 即业务调用,无镜像上报 |
| 写放大 | 每次 DML 多写一条 undo_log(数据量×2) | 无 |
| 并发吞吐 | 全局锁串行化热点行 + 脏写回滚率上升 | 无全局锁,吞吐高 |
| 长流程 | 强制短事务 | 预占是逻辑占位,与时长无关 |
14.2 安全
| 维度 | AT | TCC |
|---|---|---|
| 回滚方式 | 物理镜像:把字段改回 before 值(精确但机械) | 业务语义:调 unfreeze 等方法(灵活,可加审计) |
| 外部副作用 | ❌ 只能回滚数据库,回滚不了短信/MQ/第三方 | ✅ Cancel 是业务代码,能补偿外部副作用 |
| 隔离性 | 写加全局锁,读不加锁可能脏读(需 @GlobalLock) | Try 预占标记即逻辑隔离 |
| 幂等 | 天然(undo_log 唯一键) | 强制自己写(否则重复扣款/解冻) |
| 故障恢复 | undo_log + TC 会话重建,较自动 | 幂等重试 + 对账/定时任务兜底 |
| 异构写者 | 脏写检查会误伤(没接 Seata 的服务改同一行 → 莫名回滚) | 无感 |
| SQL 边界 | UPDATE 带 JOIN/LIMIT 等不支持镜像 | 无限制 |
14.3 一句话结论
“涉及钱用 TCC”的真正原因:资金/支付环节有大量数据库之外的外部副作用(短信、第三方回调、MQ),AT 的物理回滚补不了它们;TCC 的 Cancel 是业务代码,可以补偿。
注意:库存冻结这个场景几乎没有外部副作用,所以 AT 足够用(秒级短事务、无外部副作用 → 选 AT)。