Seata(一):分布式事务与 Seata 架构总览
Seata(一):分布式事务与 Seata 架构总览
导语:Seata 面试的第一道门槛是「说清它到底解决了什么问题、由谁协作完成」。本篇先厘清分布式事务的成因与刚性/柔性两大流派,再展开 Seata 的 TC / TM / RM 三角色与完整执行流程,随后横向对比 AT / TCC / Saga / XA 四种模式,最后讲清 seata-server 的存储模式、高可用与 2.x 的 Raft 集群,共 14 题。
一、分布式事务的成因与流派
1. 为什么单机事务解决不了微服务的问题?分布式事务是怎么产生的?
答: 单机事务(ACID)的能力边界是「一个数据库连接 / 一个资源管理器」,一旦跨出这个边界,ACID 就不成立了。
分布式事务产生的三个典型场景:
| 场景 | 例子 |
|---|---|
| 跨多个数据库 | 订单库扣减库存、账户库扣款(分库分表后同一业务甚至落在不同库) |
| 跨多个服务 | 订单服务 → 库存服务 → 支付服务,每个服务有自己的库 |
| 跨异构资源 | 数据库 + Redis + MQ + 文件系统,需要「一起成功或一起回滚」 |
本质:「多个本地事务」需要一个全局的协调者来保证「要么都提交、要么都回滚」。这正是 Seata 要解决的问题。
2. 刚性事务与柔性事务有什么区别?
答: 这是理解所有分布式事务方案的总纲:
| 维度 | 刚性事务(CP 取向) | 柔性事务(AP 取向) |
|---|---|---|
| 一致性 | 强一致(与本地事务等同) | 最终一致(允许中间态) |
| 理论基础 | ACID | BASE(基本可用、软状态、最终一致) |
| 代表实现 | XA / 2PC / 3PC(JTA/JTS);Seata 的 XA、AT 模式 | TCC、Saga、本地消息表、事务消息、最大努力通知 |
| 同步/异步 | 同步阻塞(prepare 后持锁等 commit) | 异步/补偿(先提交再补偿,或预留后确认) |
| 隔离性 | 有(数据库级或全局锁) | 通常无隔离,靠业务保证 |
| 性能 | 低(锁持有时间长、并发差) | 高(无长事务持锁) |
| 适用 | 短事务、低并发、强一致核心链路 | 长流程、高并发、跨多服务的业务链 |
| 代价 | 吞吐、可用性 | 业务改造(幂等/补偿/防悬挂)、可能出现中间态 |
选型口诀:
重要澄清:Seata 的 AT 模式虽然基于「两阶段」,但它属于柔性事务——一阶段就提交了本地事务(并释放本地锁),二阶段是异步补偿(回滚靠 undo_log 反向补偿),所以它不保证全局隔离性(默认全局读未提交)。
3. 2PC 的原理与缺陷是什么?为什么互联网很少直接用 2PC?
答: 2PC(两阶段提交)引入协调者(Coordinator):
四个致命缺陷(这也是 XA 在大规模互联网系统少用的原因):
| 缺陷 | 说明 |
|---|---|
| 同步阻塞 | 参与者 prepare 后就一直持有资源锁直到收到 commit/rollback,期间并发极低(是性能杀手) |
| 单点故障 | 协调者宕机,参与者一直阻塞、资源不释放(只能靠人工/超时介入) |
| 数据不一致 | 阶段二中协调者先给部分参与者发了 commit 后宕机 → 部分提交、部分未提交 |
| 故障恢复复杂 | 依赖协调者的日志与人工干预,无自动决策 |
3PC 的改进与局限:把 Prepare 拆成 CanCommit + PreCommit,并给参与者加超时自动决策,降低阻塞时间,但仍不能彻底解决网络分区下的不一致,工程上几乎不用。
结论:2PC/XA 的正确适用场景是「同一机房、短事务、低并发、强一致」——这也是为什么「分库分表后跨库转账」这类场景有市场,但高并发交易链路几乎都用柔性事务。
4. 主流分布式事务方案有哪些?怎么对比选型?
答:
| 方案 | 一致性 | 业务侵入 | 性能 | 适用场景 |
|---|---|---|---|---|
| XA(2PC/3PC) | 强一致 | 低(框架托管) | 低(同步阻塞) | 短事务、低并发、强一致 |
| Seata AT | 最终一致(默认全局读未提交) | 最低(无侵入,自动补偿) | 中高 | 通用(互联网最常见) |
| TCC | 准强一致(预留式) | 高(写 Try/Confirm/Cancel) | 高 | 核心交易、需要资源预留 |
| Saga | 最终一致 | 中(写正向 + 补偿) | 高 | 长流程(跨多服务的履约、审批) |
| 本地消息表 | 最终一致 | 中 | 高 | 异步解耦、对实时性要求不高 |
| MQ 事务消息 | 最终一致 | 低 | 高 | 上游下游可用 MQ 解耦(主流) |
| 最大努力通知 | 弱一致(尽力而为) | 低 | 高 | 通知类(支付结果回调)、可对账兜底 |
选型决策树(面试可直接画):
一句话:「能用本地事务就别用分布式事务;能用消息最终一致就别用 TCC;TCC 只用在核心资金链路」——这是带团队的人最想听到的答案。
二、Seata 架构与角色
5. Seata 的三大角色(TC / TM / RM)分别是什么?
答: Seata 把分布式事务的执行拆成三个角色,这与「模式」无关,是所有模式的公共骨架:
| 角色 | 部署位置 | 职责 | 关键 API / 注解 |
|---|---|---|---|
| TC | 独立服务(seata-server,可集群) | 全局事务的大脑:XID 生成、状态机、全局锁、提交/回滚决策、故障恢复 | 由 seata-server 提供 |
| TM | 业务服务的客户端(发起方) | 定义全局事务边界:开启全局事务、最终通知 TC commit/rollback | @GlobalTransactional |
| RM | 业务服务的客户端(每个参与方) | 管理分支事务资源:向 TC 注册分支、执行本地事务、上报状态、接收二阶段指令 | DataSourceProxy(AT)、@TwoPhaseBusinessAction(TCC) |
为什么一定要把 TC 拆出去独立部署?
- 事务状态需要跨服务共享(TM 挂了、RM 挂了,事务状态不能丢)→ 必须外部化;
- 全局锁需要集中裁决(AT 模式防止不同全局事务脏写同一行)→ 需要中心化的锁服务器;
- 决策要一致(谁是发起方不应决定结果)→ 由中立的 TC 统一决定。
6. 一次完整的 Seata 全局事务(AT 模式)流程是怎样的?
答: 这是必背流程图(AT 模式,最常考):
要点说明:
| 步骤 | 关键点 |
|---|---|
| ① 开启全局事务 | @GlobalTransactional 方法进入时,TM 向 TC 发起 begin,拿到 XID |
| ② XID 传播 | XID 存在 RootContext(ThreadLocal)里,通过 RPC 框架(Dubbo/Feign)的隐式参数传到下游(跨线程要手动传递,见第 12 题) |
| ③ 注册分支 | 每个数据源操作前,RM 向 TC 注册分支事务(BranchId) |
| ④ 申请全局锁 | AT 模式下本地提交前必须先拿到该行记录的全局锁(防脏写,见《Seata(二)》) |
| ⑤ 一阶段提交 | 业务数据 + undo_log 在同一个本地事务里提交(关键区别:不是等二阶段才提交) |
| ⑥ 跨服务调用 | 下游服务通过 XID 加入同一个全局事务(成为新的 RM) |
| ⑦-⑧ 二阶段提交 | TC 决策后,提交是异步的、极快:只是异步批量删除 undo_log 并释放全局锁 |
| ⑨ 二阶段回滚 | 按 XID + BranchId 找到 undo_log,用前镜像反向补偿恢复数据 |
7. Seata 支持哪几种事务模式?怎么对比?
答:
| 模式 | 一致性 | 侵入性 | 隔离性 | 一阶段是否提交本地事务 | 适用场景 |
|---|---|---|---|---|---|
| AT(默认) | 最终一致 | 无侵入(自动生成 undo_log) | 全局读未提交(可通过 SELECT FOR UPDATE 提升) | 是(提交并释放本地锁) | 通用业务(80% 场景) |
| TCC | 准强一致 | 高(手写 Try/Confirm/Cancel) | 由业务保证(Try 阶段预留资源) | 否(Try 只预留,不提交真正业务) | 核心交易(资金、库存预留) |
| Saga | 最终一致 | 中(正向 + 补偿动作) | 无隔离 | 是(每步都是本地事务,直接提交) | 长流程(履约、审批、跨多系统) |
| XA | 强一致 | 低(数据库 XA 协议,框架托管) | 有(数据库级隔离) | 否(prepare 后持锁不提交) | 短事务、低并发、强一致 |
四模式的"本质一句话":
- AT:「自动补偿」——记录前后镜像,回滚时反向执行 SQL,业务零改造;
- TCC:「资源预留」——Try 冻结资源、Confirm 落实、Cancel 解冻,需要业务自己写三个方法;
- Saga:「长流程补偿」——每步都提交,失败时按逆序调用补偿接口;
- XA:「数据库原生 2PC」——靠数据库的 XA 协议,强一致但持锁阻塞。
四模式的选择(面试必答):
8. Seata 的两种使用方式(编程/注解)与接入步骤?
答: AT 模式下的接入步骤(AT 是唯一几乎零改造的模式):
# application.yml(客户端)
seata:
enabled: true
application-id: order-service
tx-service-group: my_tx_group # ★ 事务分组,与 TC 端的映射对应
service:
vgroup-mapping:
my_tx_group: default # 分组 → TC 集群名
registry:
type: nacos
nacos:
server-addr: nacos:8848
application: seata-server
group: SEATA_GROUP
config:
type: nacos
nacos:
server-addr: nacos:8848@GlobalTransactional(timeoutMills = 60000, name = "create-order") // ★ 全局事务边界
public void createOrder(OrderDTO dto) {
orderMapper.insert(dto.toOrder()); // 分支事务 1(订单库)
storageClient.deductStock(dto.getSku(), dto.getNum()); // 分支事务 2(库存服务,跨服务)
accountClient.debit(dto.getUserId(), dto.getAmount()); // 分支事务 3(账户服务)
}四个接入细节(坑):
tx-service-group必须与 TC 端的service.vgroup-mapping配置匹配,配置错会报no available service 'default' found;@GlobalTransactional的作用是"开启全局事务",它内部的方法调用才成为分支事务;发起方自己也是一个 RM;@GlobalTransactional与@Transactional不要乱混:同一个方法上同时用会让本地事务与全局事务的边界混乱(一般全局事务方法上不要加@Transactional,或者理解清楚嵌套语义);- Seata 代理了数据源,所以要保证「业务使用的 DataSource 是被 Seata 包装过的」——如果代码里
new了原生 DataSource、或用了其他 ORM 的多数据源方案,可能没走代理 → 事务失效(表现为「全局回滚了但本地没回滚」)。
三、部署形态与高可用
9. Seata 的存储模式(store.mode)有哪几种?
答: TC 需要持久化全局事务会话(GlobalSession)与分支事务(BranchSession),store.mode 决定存哪:
| 模式 | 存储位置 | 特点 | 适用 |
|---|---|---|---|
file | 本地文件(sessionStore 目录) | 单机可用,重启后状态丢失/不可靠;无法水平扩展 | 单机测试 |
db | 关系型数据库(global_table / branch_table / lock_table) | 生产常用;需要建表脚本;有 DB 依赖 | 生产(有现成 DB) |
redis | Redis | 性能好;需要 Redis 高可用;全局锁与事务会话放 Redis | 生产(Redis 集群成熟) |
raft(2.0+) | 内置 Raft 集群(TC 自己组成 Raft Group) | 免第三方注册中心与后端存储,TC 集群自带一致性 | 2.x 推荐的新形态 |
db 模式的三张核心表(面试常问):
| 表 | 作用 |
|---|---|
global_table | 全局事务会话:xid、status、application_id、transaction_service_group、begin_time、timeout |
branch_table | 分支事务:xid、branch_id、resource_id、lock_key、status |
lock_table | 全局锁:xid、table_name、pk、row_key(AT 模式防脏写的核心表) |
注意区分两张"锁表":
lock_table是 TC 端存的全局锁;undo_log是 业务库存的回滚日志。它们完全不同(见《Seata(二)》)。
10. Seata 如何做到高可用?
答:
高可用的四个层面:
| 层面 | 做法 |
|---|---|
| TC 无状态化 + 集群 | TC 本身不保存内存状态(状态在 DB/Redis/Raft),所以可以水平扩展、挂一个不影响服务 |
| 注册中心 | 客户端通过 Nacos/ZK 动态发现 TC 列表,TC 挂掉自动剔除;客户端多 TC 地址也有 failover(service.disableGlobalTransaction、service.enableDegrade 等) |
| 共享存储高可用 | db 模式要 DB 主从/集群;redis 模式要 Redis Sentinel/Cluster;raft 模式自己保证多数派 |
| 客户端降级 | service.enableDegrade:TC 不可用时自动降级(事务退化为本地事务),避免"TC 挂了业务全挂"——这是生产必须考虑的一环 |
Raft 模式的特殊之处(2.0+):
- 不再依赖第三方注册中心与后端存储:TC 节点之间通过 Raft 选举与复制会话状态;
- 接入方式:客户端配置
seata.registry.type = raft,多个 TC 地址按偏移量规则给出; - 优势:部署简单(少依赖 Nacos/DB)、强一致、宕机自动切换;
- 代价:Raft 写需要多数派确认,性能略低于纯 Redis 模式;脑裂与恢复要按 Raft 规则理解。
11. Seata 2.x 相比 1.x 有哪些变化?
答:
| 变化 | 说明 |
|---|---|
| 版本与许可 | Seata 已进入 Apache 基金会(Apache Seata),包名/坐标逐步迁移(io.seata → org.apache.seata,2.x 系列演进) |
| 支持 Raft 集群模式 | 2.0.0 引入,TC 可以不依赖第三方注册中心与存储 |
| 控制台(Console)分离 | seata-server 的控制台功能独立(seata-console),便于独立部署与权限管理 |
| Spring Boot 3 / JDK 17 支持 | 适配 jakarta.* 命名空间与新 JDK(1.x 主要在 JDK 8/Spring Boot 2) |
| 配置与存储增强 | 支持更多配置中心/存储;会话管理的性能与稳定性优化 |
| 与旧版兼容性 | 客户端与服务端版本兼容窗口要按官方矩阵核对(大版本混用容易踩协议不兼容) |
面试建议:2.x 只讲确定的关键词——「Apache 毕业、Raft 集群、控制台分离、Boot 3/JDK 17」,不要编造细节。
12. Seata 生产落地有哪些常见坑?
答:
| 坑 | 现象 | 原因 / 解法 |
|---|---|---|
no available service 'xxx' found | 客户端连不上 TC | ①tx-service-group 与 TC 的 vgroup-mapping 不匹配;②注册中心里没有 seata-server;③网络/防火墙 |
| 全局回滚了,本地数据没回滚 | 数据不一致 | ①数据源没被 DataSourceProxy 代理;②SQL 经过了其他连接/框架(如手写 JDBC、MyBatis 多数据源未代理);③RM 注册失败 |
| undo_log 表不存在 | 一阶段报错 | 每个参与分支事务的业务库都要建 undo_log 表 |
undo_log 越来越大 | 磁盘/性能问题 | 二阶段提交是异步删 undo_log,若 TC 挂了会堆积;需监控并清理(脏数据/异常事务也会留下记录) |
| 跨线程/异步调用丢 XID | 下游服务没加入全局事务(变成独立事务) | XID 在 RootContext(ThreadLocal)里,线程池/异步/@Async/MQ 消费场景都要手动传递 XID(或用 Seata 提供的包装) |
@GlobalTransactional 不生效 | 事务没回滚 | ①方法被同类内部调用(绕过代理,this.xxx())→ 注解不生效(与 Spring 事务同样的坑);②方法不是 public;③异常被 catch 吞掉 |
| 全局锁冲突/超时 | Global lock wait timeout | 热点行竞争;调 client.rm.lock.retryInterval/retryTimes;降低事务持有时长、拆分热点 |
@GlobalTransactional 与 MQ/缓存操作混用 | 出现不一致 | MQ/Redis 不参与 Seata 事务(除非用对应的 RM 适配),要考虑补偿或改用消息最终一致 |
| 事务超时 | transaction timeout | @GlobalTransactional(timeoutMills=...) 默认 60s;长事务要评估(超时后 TC 会触发回滚) |
| 性能下降明显 | TPS 下滑 | AT 的额外开销是 undo_log 写入 + 全局锁申请(一次 RPC);热点行会串行化;可考虑只用 Seata 管核心链路,其余走消息最终一致 |
排查全局事务的标准动作:
13. 为什么说「Seata AT 的一阶段提交」是关键设计?
答: 这是 AT 模式最容易被误解、也最能体现理解深度的一点。
传统 XA 的两阶段:阶段一 prepare 后不提交,一直持锁到阶段二 → 锁持有时间长 = 性能差。
Seata AT 的创新:一阶段就把本地事务提交了,靠两样东西保证「还能回滚」:
由此产生的连锁结论(面试要点):
| 结论 | 解释 |
|---|---|
| AT 性能远好于 XA | 本地锁只持有到"本地提交",而 XA 要持有到全局提交 |
| AT 不是"强一致" | 一阶段已提交,其他事务可能读到中间态(全局读未提交) |
| 全局锁是 AT 的瓶颈 | 热点行的全局锁会让不同全局事务串行化(在 lock_table 里排队/RPC 重试) |
| undo_log 是"回滚的凭据" | 所以每个业务库必须有 undo_log 表,且不能被随意清理 |
| 二阶段提交是"异步清理" | 所以会出现「全局已提交,但 undo_log 还没删干净」的短暂现象(不影响正确性) |
一句话:XA 是「先准备、后提交(长锁)」;AT 是「先提交、后补偿(短锁 + undo_log)」。这个差异决定了二者性能与一致性的根本差别。
14. 综合实战:一个「下单扣库存扣余额」的场景,你会怎么选方案?
答: 完整回答框架(先讲取舍,再讲方案):
第一步:明确诉求
第二步:方案对比与选择
| 候选 | 判断 |
|---|---|
| 本地事务 + 拆分 | 下单/扣库存/扣余额在不同库不同服务,本地事务做不到 |
| XA | 并发高 + 同步阻塞 → 不合适 |
| Saga | 事务短、步骤少 → Saga 的补偿编排过重,不合适 |
| Seata AT | 无侵入、开发成本最低,能满足"要么都成功";缺点是全局锁在热点 SKU 上会串行化 |
| TCC | 能解决热点问题(Try 阶段预留,减少锁竞争),但开发成本高 |
| MQ 事务消息 + 最终一致 | 适合"下单成功后异步通知"(如发券、发短信),不适合"扣款不能失败"的强约束 |
第三步:落地建议(分层方案,最加分)
第四步:必须说出的风险
| 风险 | 应对 |
|---|---|
| 全局锁热点竞争 | 热点用 TCC;或库存分桶(把一行库存拆成 N 行,分散锁) |
| TC 单点/不可用 | TC 集群 + enableDegrade 降级策略 |
| 事务超时导致回滚 | 控制事务粒度,避免在全局事务里做耗时操作(如调用第三方) |
| 中间态被读到 | 关键查询加 SELECT ... FOR UPDATE(AT 支持,会申请全局锁);或业务上规避 |
| 跨线程 XID 丢失 | 统一封装线程池/Trace 传递,做专项测试验证 |
下一篇:《Seata(二)》把 AT 模式彻底拆开——SQL 解析与前后镜像、undo_log 表结构、二阶段回滚的完整流程与"幂等/校验"、全局锁的申请与释放、写隔离与读隔离、脏写与脏读的边界,以及
SELECT FOR UPDATE的代理原理。
