SpringCloud——分布式事务Seata:解决微服务数据不一致的利器
目录
Seata 第一讲:为什么普通 @Transactional 管不了微服务事务?
好,网关已经学过,接下来正式进入 分布式事务 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 原理的起点。
更多推荐


所有评论(0)