分布式事务与 Seata AT
一、分布式事务要解决什么
单体架构里,一个 @Transactional 就能让多张表的操作原子提交或回滚,靠的是单个数据库的本地事务。
但微服务下,一次下单要跨三个服务:
- 订单服务:创建订单
- 库存服务:扣减库存
- 支付服务:扣款
每个服务有自己独立的数据库,订单库的事务管不了库存库,更管不了支付库。“订单创建成功但库存没扣”或“钱扣了订单没成”,就是分布式事务要解决的问题。
二、Seata 的三大角色:谁发起、谁决策、谁执行
Seata 把分布式事务拆成三个角色协作,用一个下单场景贯穿:
| 角色 | 全称 | 部署位置 | 通俗理解 | 在下单场景里是谁 |
|---|---|---|---|---|
| TM | Transaction Manager | 嵌入业务应用 | 发起人,定义全局事务边界 | 订单服务(入口) |
| TC | Transaction Coordinator | 独立部署的 Server | 总指挥,维护全局事务状态,驱动提交/回滚 | Seata Server |
| RM | Resource Manager | 嵌入业务应用 | 执行者,管理分支事务,执行二阶段动作 | 库存服务、支付服务 |
关键认知:TC 是独立服务,不碰业务数据;TM 和 RM 是 jar 包,跟业务应用一起跑。 三者通过长连接通信,用 XID(全局事务 ID) 串联。
三、完整流程:一条 XID 串起 TM、TC、RM
还是下单场景:订单服务(TM)→ 库存服务(RM)→ 支付服务(RM)。
第 1 步:TM 向 TC 申请开启全局事务
订单服务作为入口,TM 向 TC 发起 begin。TC 生成一个全局唯一的 XID,作为这次分布式事务的“身份证”。
TM 动作:发起 begin。TC 动作:生成 XID,记录全局事务状态为“开始”。
第 2 步:XID 沿调用链向下传递
订单服务调用库存服务、支付服务时,XID 被自动传递下去(通过 RPC 上下文),确保所有参与者都知道“我们属于同一个全局事务”。
第 3 步:每个 RM 向 TC 注册分支事务
库存服务、支付服务在执行本地操作前,带着 XID 向 TC 注册分支事务,把自己的本地事务挂到全局事务名下。
RM 动作:注册分支,拿到 Branch ID。TC 动作:记录这个全局事务下有哪些分支。
第 4 步:RM 执行本地事务,并向 TC 申请全局锁
这是 AT 模式的核心。每个 RM 执行本地 SQL 时,Seata 通过代理数据源在背后做四件事:
- 解析 SQL:拿到操作类型、表名、条件。
- 查前镜像:查出修改前的数据。
- 执行业务 SQL:正常更新。
- 查后镜像:查出修改后的数据。
然后,把前镜像、后镜像、SQL 信息打包成 undo log,插入 UNDO_LOG 表。
关键一步:在本地事务提交之前,RM 必须先向 TC 申请这条记录的全局锁。拿到全局锁,才能提交本地事务;拿不到,就回滚本地事务、释放本地锁,然后重试。
RM 动作:申请全局锁 → 拿到后,把业务更新和 undo log 在同一个本地事务中一起提交,释放本地数据库锁。TC 动作:记录全局锁归属。
一阶段结束时:数据已经真实写入了数据库,但全局锁还攥在 TC 手里。其他全局事务想改同一行数据,过不去。
第 5 步:TM 向 TC 发起全局决议
所有分支执行完毕后,订单服务的 TM 根据整体结果告诉 TC:这个 XID 对应的全局事务是提交还是回滚。
TM 动作:发起 commit 或 rollback。TC 动作:根据决议,驱动所有 RM 执行二阶段。
第 6 步:TC 驱动所有 RM 执行二阶段动作
如果全局提交:TC 通知各 RM 提交。RM 把这个请求丢进异步队列,马上返回成功,之后异步批量删除 undo log。因为一阶段数据已经落库了,二阶段提交几乎没有成本。
如果全局回滚:TC 通知各 RM 回滚。RM 开启一个本地事务:
- 找到 undo log(通过 XID 和 Branch ID)。
- 数据校验:拿 undo log 里的后镜像和当前数据库数据对比,一致才安全回滚。
- 生成反向 SQL 并执行:根据前镜像把数据改回去,提交本地事务,上报 TC。
RM 动作:提交则异步删 undo log;回滚则按 undo log 反向补偿。TC 动作:确保所有分支都完成二阶段。
四、把 TM、TC、RM 在每个阶段的动作汇总
| 阶段 | TM(订单服务) | TC(Seata Server) | RM(库存/支付服务) |
|---|---|---|---|
| 开启 | 发起 begin | 生成 XID,记录全局事务 | — |
| 注册 | — | 记录分支 | 带 XID 注册分支,拿 Branch ID |
| 一阶段 | — | 管理全局锁 | 执行 SQL、生成 undo log、申请全局锁、提交本地事务 |
| 决议 | 发起 commit/rollback | 接收决议,准备驱动二阶段 | — |
| 二阶段 | — | 通知各 RM 执行 | 提交则异步删 undo log;回滚则按 undo log 反向补偿 |
一句话串起来:TM 发起,TC 决策,RM 执行,XID 串联。 一阶段 RM 先提交本地事务但攥着全局锁,二阶段 TC 根据 TM 的决议驱动 RM 异步提交或反向补偿。
五、AT 模式的一个关键权衡:全局锁与隔离性
AT 的默认全局隔离级别是“读未提交”——因为一阶段本地事务已经提交,其他事务在数据库层面能读到中间状态数据。
Seata 的补救方式:只有带 SELECT FOR UPDATE 的查询才会去申请全局锁。如果全局锁被别的全局事务持有,这个查询会阻塞等待,直到锁释放,从而读到“已提交”的数据。普通 SELECT 不会被代理,所以全局读已提交需要业务显式用 FOR UPDATE 来换取。
这也意味着:高并发写同一批数据时,全局锁会成为竞争点,极端情况下可能和本地锁形成死锁环。实际落地要控制事务边界、避免长事务。
六、一句话总结
分布式事务解决的是“一次业务跨多个服务/数据库时,如何保证最终一致”。Seata AT 的原理是:TM 发起全局事务并拿到 XID,RM 在各服务里执行本地 SQL 时自动生成 undo log 并向 TC 申请全局锁,一阶段就提交本地事务;二阶段由 TC 根据 TM 的决议驱动 RM 异步提交或按 undo log 反向补偿。核心取舍是用“全局锁 + 读未提交”换低侵入和较高性能,代价是高并发写场景下的锁竞争。

浙公网安备 33010602011771号