目录

Seata 第一讲:为什么普通 @Transactional 管不了微服务事务?

一、先看一个典型下单场景

二、没有分布式事务时会发生什么?

三、为什么加了 @Transactional 也没用?

原因是什么?

四、把三个本地事务画开来看

五、什么是分布式事务?

本地事务

分布式事务

六、Seata 到底在做什么?

七、Seata 的三个核心角色

1. TC:Transaction Coordinator

2. TM:Transaction Manager

3. RM:Resource Manager

八、XID 是什么?

九、一笔 Seata 全局事务的完整流程

第一步:TM 开启全局事务

第二步:订单分支事务执行

第三步:XID 随远程调用传播

第四步:库存分支执行

第五步:账户分支执行

第六步:TM 请求全局回滚

第七步:TC 通知各 RM 回滚

十、为什么还需要 Seata Server?

十一、Seata 和 Nacos 的关系

Nacos 的职责

Seata 的职责

十二、今天这一讲需要真正掌握的内容

1. 为什么本地事务失效?

2. 什么是分布式事务?

3. Seata 是干什么的?

4. 三个角色是什么?

5. XID 是什么?

十三、面试式检查

问题 1

问题 2

问题 3

后续学习顺序


好,网关已经学过,接下来正式进入 分布式事务 Seata

这部分不建议一上来就下载、部署 Seata。否则容易变成“照着配置启动成功了,但不知道它到底解决了什么”。我们先把问题弄清楚,再搭环境。

你上传的资料可以分成两部分:

  • 《分布式事务 - Alibaba Seata》:原理、问题演示、XA、AT 等事务模式。

  • 《Seata 下载和部署》:Seata Server、Nacos、数据库存储模式等部署内容。

  • 《SkyWalking》是下一阶段的链路追踪,暂时先不展开。


Seata 第一讲:为什么普通 @Transactional 管不了微服务事务?

一、先看一个典型下单场景

假设一次下单需要完成三件事:

order-service
    1. 创建订单

storage-service
    2. 扣减库存

account-service
    3. 扣减余额

调用关系:

客户端
  ↓
order-service
  ├─ 写入订单数据库
  ├─ 调用 storage-service 扣库存
  └─ 调用 account-service 扣余额

三个服务分别拥有自己的数据库:

order-service   → order_db
storage-service → storage_db
account-service → account_db

课件采用的也是这个简化模型:订单服务创建订单,同时远程调用库存服务和账户服务;当余额不足时,余额扣减失败,但之前创建的订单和扣减的库存不会自动回滚。


二、没有分布式事务时会发生什么?

假设用户下单:

{
  "userId": 1002,
  "commodityCode": 2002,
  "count": 10,
  "money": 20000
}

执行过程:

① order-service 创建订单成功
② storage-service 扣减库存成功
③ account-service 扣减余额失败:余额不足

最终数据库状态可能是:

数据库执行结果
订单库已经创建订单
库存库已经扣减库存
账户库扣款失败

这就产生了数据不一致:

用户没有付款成功
但订单已经创建
库存也已经减少

从业务角度看,这三个操作应该是一个整体:

全部成功
或者
全部失败

但实际上却只执行了一半。

这就是分布式事务要解决的问题。课件将其概括为:当一个事务涉及多个数据库、服务或应用实例时,需要保证多个节点的数据操作同时成功或同时失败。


三、为什么加了 @Transactional 也没用?

很多人第一次遇到这个问题时,会直接在订单方法上加:

@Transactional
public void createOrder(OrderRequest request) {

    // 1. 写订单数据库
    orderMapper.insert(...);

    // 2. 远程调用库存服务
    storageClient.deduct(...);

    // 3. 远程调用账户服务
    accountClient.deduct(...);
}

看起来这个方法被事务包住了,但它只能控制:

order-service 当前使用的数据库连接

也就是:

order_db

它控制不了:

storage-service 的数据库连接
account-service 的数据库连接

原因是什么?

Spring 本地事务的核心边界通常是:

一个应用
+
一个数据源
+
一个数据库连接

order-service 中的事务管理器大致只能完成:

开启 order_db 事务
        ↓
执行订单 SQL
        ↓
提交或回滚 order_db

而调用库存服务时,本质上发出的是一次 HTTP 或 RPC 请求:

order-service
    ↓ HTTP/Feign
storage-service

库存服务收到请求后,会在自己的 JVM 中:

从 storage_db 连接池获取连接
开启自己的本地事务
执行扣库存 SQL
提交自己的本地事务

两个事务彼此独立:

order-service 本地事务
storage-service 本地事务

订单服务的 PlatformTransactionManager 根本拿不到库存服务的数据库连接,因此没有能力命令它回滚。


四、把三个本地事务画开来看

一次下单实际上包含三个本地事务:

全局下单业务
│
├─ 本地事务 A:订单库
│    BEGIN
│    INSERT order
│    COMMIT
│
├─ 本地事务 B:库存库
│    BEGIN
│    UPDATE stock
│    COMMIT
│
└─ 本地事务 C:账户库
     BEGIN
     UPDATE account
     ROLLBACK

问题在于:

事务 A、B、C 互相不知道对方的结果

账户事务失败时:

事务 C 可以回滚自己
但无法回滚已经提交的事务 A 和事务 B

因此,分布式事务不是“把 @Transactional 写得更大一点”,而是:

需要一个额外的协调机制,把多个独立的本地事务组织成一个全局事务。


五、什么是分布式事务?

可以这样理解:

本地事务

一个数据库中的多条 SQL
要么全部成功,要么全部失败

例如:

UPDATE account
SET balance = balance - 100
WHERE user_id = 1;

INSERT INTO account_log(...);

这两条 SQL 在同一个数据库、同一个事务中。


分布式事务

多个服务、多个数据库中的操作
在业务上要么全部成功,要么全部失败

例如:

订单库创建订单
+
库存库扣减库存
+
账户库扣减余额

这三个本地事务共同组成一个全局事务:

全局事务
├─ 订单分支事务
├─ 库存分支事务
└─ 账户分支事务

所以要分清两个概念:

概念含义
本地事务单个服务、单个数据库内部的事务
全局事务包含多个服务本地事务的整体事务
分支事务全局事务中的某一个本地事务

六、Seata 到底在做什么?

Seata 是一个分布式事务协调框架。

它不会取代 MySQL 的本地事务,而是在多个本地事务上面增加一层协调:

                 Seata
                   ↓
          统一协调多个本地事务
                   
订单本地事务   库存本地事务   账户本地事务

目标是:

全部分支成功
→ 全局提交

任意分支失败
→ 全局回滚

Seata 提供:

AT
XA
TCC
SAGA

四类事务模式。课件也是按照这几个模式展开,其中 AT 模式会涉及 undo_log,XA 模式则更依赖数据库原生 XA 能力。

我们后面会重点学习:

AT 模式

因为它是 Seata 中最典型、对普通数据库业务代码侵入较低的模式。


七、Seata 的三个核心角色

理解 Seata,必须先掌握:

TC
TM
RM

这三个缩写非常重要,也是高频面试题。


1. TC:Transaction Coordinator

中文:

事务协调者

通常就是单独部署的:

seata-server

它是整个全局事务的总协调者。

负责:

创建全局事务
生成 XID
记录全局事务状态
记录各分支事务
决定最终提交还是回滚
通知各个 RM 执行提交或回滚

可以把 TC 类比成总指挥:

订单分支成功了吗?
库存分支成功了吗?
账户分支成功了吗?

全部成功 → 大家提交
有人失败 → 大家回滚

部署资料中提到的 seata-server,就是负责全局事务协调和管理的服务端。


2. TM:Transaction Manager

中文:

事务管理器

TM 通常位于全局事务的发起方。

在下单业务中:

order-service

一般就是 TM 所在的位置。

代码通常会在业务入口增加:

@GlobalTransactional
public void createOrder(OrderRequest request) {
    ...
}

TM 负责:

向 TC 申请开启全局事务
拿到全局事务 XID
执行业务调用
根据执行结果通知 TC:
提交全局事务或回滚全局事务

注意:

TM 不是某个单独部署的服务

它通常是 Seata 客户端在业务服务中的一个角色。


3. RM:Resource Manager

中文:

资源管理器

RM 负责管理具体的事务资源,通常就是数据库。

在这个案例中:

order-service   中有一个 RM
storage-service 中有一个 RM
account-service 中有一个 RM

每个 RM 负责:

管理本地数据库操作
向 TC 注册分支事务
报告分支执行结果
接收 TC 的提交或回滚指令
执行本地提交或回滚

所以:

TC = 总协调者
TM = 全局事务发起者
RM = 各数据库分支参与者

八、XID 是什么?

XID 是:

全局事务唯一编号

类似:

Sentinel 资源名标识一项受保护资源
SkyWalking TraceID 标识一次完整调用链
Seata XID 标识一次完整全局事务

例如:

XID = 192.168.1.10:8091:123456789

一次下单的所有分支必须携带同一个 XID:

order-service   → XID=123
storage-service → XID=123
account-service → XID=123

TC 根据 XID 知道:

这些分支属于同一个全局事务

如果没有 XID,TC 无法判断某次扣库存和某次扣余额是否属于同一次下单。


九、一笔 Seata 全局事务的完整流程

假设订单服务代码:

@GlobalTransactional
public void createOrder(OrderRequest request) {

    orderMapper.insertOrder(request);

    storageClient.deduct(
            request.getCommodityCode(),
            request.getCount()
    );

    accountClient.deduct(
            request.getUserId(),
            request.getMoney()
    );
}

执行流程如下。

第一步:TM 开启全局事务

order-service 中的 TM
        ↓
向 TC 请求开启全局事务
        ↓
TC 创建事务并返回 XID

例如:

XID = 10001

第二步:订单分支事务执行

order-service 的 RM
        ↓
向 TC 注册订单分支
        ↓
执行订单 SQL
        ↓
向 TC 报告结果

TC 此时记录:

全局事务 10001
└─ 订单分支:成功

第三步:XID 随远程调用传播

订单服务调用库存服务:

order-service
    ↓ 携带 XID=10001
storage-service

Seata 会通过 RPC/Feign 调用上下文传播 XID。

库存服务收到请求后,知道自己不是一个独立事务,而是属于:

全局事务 10001

第四步:库存分支执行

库存服务中的 RM:

向 TC 注册库存分支
执行扣库存 SQL
报告执行结果

TC 记录:

全局事务 10001
├─ 订单分支:成功
└─ 库存分支:成功

第五步:账户分支执行

账户服务收到同一个 XID:

XID=10001

它向 TC 注册账户分支,然后执行扣余额。

假设余额不足,账户分支失败。

TC 记录:

全局事务 10001
├─ 订单分支:成功
├─ 库存分支:成功
└─ 账户分支:失败

第六步:TM 请求全局回滚

异常回到订单服务后,TM 发现整个业务失败:

TM
 ↓
通知 TC:
全局事务 10001 需要回滚

第七步:TC 通知各 RM 回滚

TC
├─ 通知订单 RM 回滚
├─ 通知库存 RM 回滚
└─ 通知账户 RM 回滚

最终数据恢复:

订单删除或恢复
库存恢复
账户保持原值

课件对 TC、TM、RM 的整体机制概括为:TM 开启全局事务并取得 XID;RM 注册和执行分支事务并报告状态;TM 通知事务结束后,TC 根据所有分支结果决定提交或回滚,再通知各 RM 执行。


十、为什么还需要 Seata Server?

可能会有一个疑问:

各个服务之间互相通知不行吗?为什么一定还要有 TC?

如果让服务之间自行协调:

order-service 通知 storage-service
storage-service 再通知 account-service

会遇到很多问题:

谁记录全局事务状态?
谁知道所有分支是否完成?
某个服务宕机后怎么办?
回滚消息丢失怎么办?
一个分支重复提交怎么办?
全局锁由谁管理?

所以需要一个独立中心统一记录:

全局事务
分支事务
全局锁
事务状态

Seata Server 在 DB 存储模式下,会使用类似以下表:

global_table
branch_table
lock_table

课件的部署资料也说明,全局事务会话由全局事务、分支事务和全局锁组成,分别对应这些服务端表。


十一、Seata 和 Nacos 的关系

你之前已经学过 Nacos,因此这里要避免混淆。

Nacos 的职责

服务注册发现
配置管理

Seata Server 可以注册到 Nacos:

seata-server
    ↓ 注册
Nacos

业务服务通过 Nacos找到 Seata Server:

order-service
    ↓ 查询
Nacos
    ↓
找到 seata-server

Seata 的职责

协调全局事务
管理分支事务
决定提交或回滚

所以:

Nacos ≠ 分布式事务协调器

Nacos 只是帮助各服务找到 Seata Server,并提供相关配置。

可以类比:

Nacos:
通讯录和配置仓库

Seata Server:
真正负责事务决策的总指挥

十二、今天这一讲需要真正掌握的内容

先不要急着背配置。你需要能独立说出:

1. 为什么本地事务失效?

因为:

@Transactional 只能控制当前服务的本地数据源和数据库连接
无法控制远程服务已经提交的本地事务

2. 什么是分布式事务?

一个业务操作涉及多个服务或数据库,
要求这些操作整体提交或整体回滚。

3. Seata 是干什么的?

把多个独立的本地事务组织成一个全局事务,
由 TC 统一协调提交或回滚。

4. 三个角色是什么?

角色含义
TC事务协调者,通常是 Seata Server
TM全局事务发起者
RM管理数据库分支事务的资源管理器

5. XID 是什么?

一次全局事务的唯一编号,
会在跨服务调用中传播。

十三、面试式检查

问题 1

订单服务添加了 @Transactional,调用库存服务成功、账户服务失败,为什么库存不会回滚?

标准回答:

@Transactional 管理的是订单服务自身数据源的本地事务。库存服务通过远程调用在另一个 JVM、另一个数据源上开启并提交自己的本地事务。订单服务的事务管理器无法控制库存服务的数据库连接,因此账户服务失败后,只能回滚订单服务本地事务,不能自动回滚已提交的库存事务。


问题 2

Seata 为什么需要传播 XID?

标准回答:

XID 是全局事务唯一标识。远程服务收到 XID 后,才能将自身的本地事务注册为该全局事务的分支事务。TC 也依靠 XID 将订单、库存、账户等分支关联到同一个全局事务中,并统一决定提交或回滚。


问题 3

TC、TM、RM 是三个独立服务吗?

标准回答:

不是。TC 通常是独立部署的 Seata Server;TM 和 RM 通常作为 Seata 客户端能力运行在业务服务内部。全局事务入口所在服务承担 TM 角色,各操作数据库的服务承担 RM 角色,一个服务也可能同时具有 TM 和 RM 两种角色。


后续学习顺序

下一步按这个顺序推进:

第 2 讲:搭建或分析订单、库存、账户三个服务
         亲自复现数据不一致

第 3 讲:Seata Server 下载和部署
         Nacos 注册中心
         DB 存储模式

第 4 讲:业务服务接入 Seata
         @GlobalTransactional
         XID 传播

第 5 讲:XA 模式原理与实测

第 6 讲:AT 模式核心原理
         前镜像、后镜像、undo_log、全局锁

第 7 讲:AT 两阶段提交与回滚源码链路

第 8 讲:异常、并发、脏写和回滚失败排查

第 9 讲:AT、XA、TCC、SAGA 的选型

下一讲先不启动 Seata,而是亲自构造一次失败下单,观察订单、库存、余额三张表为什么出现不一致。这一步是后续所有 Seata 原理的起点。

更多推荐