黑马商城学习项目08-分布式事务
分布式事务
在微服务架构中,业务往往需要跨多个服务和数据库协作完成,此时传统的单体事务(ACID)无法保证全局数据一致性,分布式事务问题由此产生。本文将结合电商下单业务场景,详细讲解分布式事务的本质、解决方案(以 Seata 为例)及核心实现模式。
1. 分布式事务问题引入
1.1 下单业务场景分析
以电商平台的下单业务为例,完整流程涉及 3 个独立的微服务,每个服务对应独立数据库:
- 交易服务(trade-service:负责创建订单,操作
hm-trade数据库; - 购物车服务(cart-service):负责清空用户购物车中已下单的商品,操作
hm-cart数据库; - 库存服务(item-service):负责扣减商品库存,操作
hm-item数据库。
每个服务内部的操作都是本地事务(分支事务),例如:
- 交易服务的 “创建订单” 是一个分支事务;
- 购物车服务的 “清空购物车” 是一个分支事务;
- 库存服务的 “扣减库存” 是一个分支事务。
这些分支事务共同构成一个全局事务,业务要求:所有分支事务必须同时成功或同时失败(例如:若库存不足导致下单失败,购物车已清空的数据必须回滚)。
1.2 分布式事务问题复现
我们通过实际测试验证分布式事务的问题:
- 初始状态:用户购物车中有 4 件商品,商品 A 库存为 10;
- 操作步骤:
- 用户选择购物车中所有商品结算,进入订单确认页;
- 手动将商品 A 的库存修改为 0(模拟库存不足场景);
- 用户提交订单,库存服务因 “库存不足” 返回失败,交易服务下单失败;
- 异常结果:查看购物车时发现,4 件商品已被清空(购物车服务的分支事务已执行成功),但订单未创建、库存未扣减。
1.3 问题本质
每个分支事务仅能保证自身的 ACID 特性(例如购物车服务的 “清空” 操作在本地数据库中是原子的),但分支事务之间无感知:购物车服务不知道库存服务执行失败,因此无法回滚已清空的数据。
当满足以下任一条件时,就会产生分布式事务问题:
- 业务跨多个微服务实现(如下单涉及交易、购物车、库存 3 个服务);
- 业务跨多个数据源实现(如一个服务操作 2 个独立数据库)。
2. 认识 Seata
解决分布式事务的核心思路是:引入统一的事务协调者,协调所有分支事务的执行状态,确保全局一致性。Seata(Alibaba 开源的分布式事务框架)是目前工业界应用最广的解决方案之一,官网:https://seata.io/zh-cn/docs/overview/what-is-seata.html
2.1 Seata 的核心角色
Seata 通过 3 个核心角色实现分布式事务协调,分工明确:
| 角色 | 英文全称 | 核心职责 | 部署方式 |
|---|---|---|---|
| TC | Transaction Coordinator(事务协调者) | 1. 维护全局事务和分支事务的状态; 2. 协调所有分支事务提交或回滚; 3. 接收 TM 的全局事务请求,接收 RM 的分支事务状态报告 |
独立微服务,需单独部署 |
| TM | Transaction Manager(事务管理器) | 1. 定义全局事务的范围(即哪些操作属于全局事务); 2. 发起全局事务的开始、提交或回滚请求; 3. 与 TC 通信,驱动全局事务流程 |
嵌入在业务微服务中(如交易服务),通过注解标记全局事务入口 |
| RM | Resource Manager(资源管理器) | 1. 管理本地分支事务(执行 SQL、提交 / 回滚本地事务); 2. 向 TC 注册分支事务,报告分支事务执行状态; 3. 接收 TC 的指令,执行分支事务的提交或回滚 |
嵌入在所有参与分布式事务的微服务中(如交易、购物车、库存服务) |
2.2 Seata 的工作流程(简化)
- TM 发起全局事务,向 TC 注册全局事务,获取全局事务 ID;
- TM 调用各个分支事务(如调用购物车服务清空接口、库存服务扣减接口);
- 每个 RM 在执行本地分支事务前,向 TC 注册分支事务(绑定全局事务 ID);
- RM 执行本地分支事务,向 TC 报告执行结果(成功 / 失败);
- TC 汇总所有分支事务状态:
- 若全部成功:通知所有 RM 提交分支事务;
- 若任意失败:通知所有 RM 回滚分支事务;
- TM 接收 TC 的全局事务结果,完成流程。
3. 部署 TC 服务
TC 是 Seata 的核心协调中心,需独立部署。以下以 Docker+MySQL 存储为例(MySQL 用于持久化全局事务状态,避免 TC 重启后数据丢失)。
3.1 准备数据库表
Seata 的 TC 服务需要存储全局事务、分支事务的状态数据,需先在 MySQL 中创建专用表。执行课前资料中的 seata-tc.sql 脚本,核心表说明:
global_table:存储全局事务信息(如全局事务 ID、状态、创建时间);branch_table:存储分支事务信息(如分支事务 ID、全局事务 ID、执行状态);lock_table:存储分布式锁信息(避免并发修改数据导致不一致)。
3.2 准备配置文件
课前资料提供 seata 目录,包含 TC 服务的核心配置(如数据库连接、注册中心配置),关键配置说明:
registry.conf:配置 TC 服务的注册中心(如 Nacos),让微服务能找到 TC;file.conf:配置 TC 的存储模式(此处为 MySQL),指定 MySQL 连接信息(地址、用户名、密码)。
将 seata 目录拷贝到虚拟机的 /root 目录(例如通过 XShell 的文件传输功能)。
3.3 Docker 部署 TC 服务
前提条件
确保 Nacos、MySQL 已加入自定义网络(如 hm-net),若未加入,执行以下命令添加:
1 | # 将nacos容器加入hm-net网络(容器名替换为实际名称) |
执行部署命令
在虚拟机 /root 目录下执行 Docker 命令,启动 Seata TC 服务:
1 | docker run --name seata \ |
镜像加载备选方案
若 Docker Hub 下载镜像缓慢,可通过课前资料提供的镜像文件本地加载:
bash
1 | # 加载本地镜像文件(xxx.tar为镜像文件名) |
4. 微服务集成 Seata
所有参与分布式事务的微服务(交易、购物车、库存服务)都需集成 Seata 的 TM 和 RM 客户端。以下以 trade-service(交易服务) 为例,其他服务步骤类似。
4.1 引入依赖
在 trade-service 的 pom.xml 中添加 Seata 和 Nacos 配置依赖(Nacos 用于共享 Seata 配置,避免每个服务重复配置):
1 | <!-- 1. Nacos统一配置管理:用于加载共享的Seata配置 --> |
4.2 改造配置
步骤 1:在 Nacos 添加共享 Seata 配置
为避免每个服务重复配置 Seata,在 Nacos 中创建共享配置文件 shared-seata.yaml,所有服务可直接引用:
1 | seata: |
步骤 2:改造 trade-service 的配置文件
添加
bootstrap.yaml:用于加载 Nacos 中的共享配置(包括 Seata 配置),内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15spring:
application:
name: trade-service # 服务名称(需与Nacos中注册的一致)
profiles:
active: dev # 激活的环境(dev/test/prod)
cloud:
nacos:
server-addr: 192.168.150.101 # Nacos地址
config:
file-extension: yaml # 配置文件后缀(yaml/xml)
shared-configs: # 加载共享配置
- dataId: shared-jdbc.yaml # 共享的数据库配置(如数据源、MyBatis)
- dataId: shared-log.yaml # 共享的日志配置(如Logback)
- dataId: shared-swagger.yaml # 共享的Swagger文档配置
- dataId: shared-seata.yaml # 共享的Seata配置(关键)改造
application.yaml:保留服务自身的端口、Feign、业务配置,内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13server:
port: 8085 # 交易服务端口(避免与其他服务冲突)
feign:
okhttp:
enabled: true # 开启OKHttp连接池,优化Feign调用性能
sentinel:
enabled: true # 开启Feign与Sentinel的整合(熔断降级)
hm:
swagger:
title: 交易服务接口文档 # Swagger文档标题
package: com.hmall.trade.controller # Swagger扫描的Controller包
db:
database: hm-trade # 交易服务对应的数据库名
步骤 3:同步改造其他服务
按照上述步骤,分别改造 hm-cart(购物车服务)和 hm-item(库存服务):
- 引入相同的 Seata 和 Nacos 依赖;
- 添加
bootstrap.yaml,加载shared-seata.yaml等共享配置; - 确保
tx-service-group(事务组名称)与 trade-service 一致(均为hmall)。
4.3 添加数据库表(Seata 客户端)
Seata 的 RM 客户端在执行分支事务时,需要记录数据快照(undo-log) 用于回滚(AT 模式),因此需在每个服务对应的数据库中创建 undo_log 表。
执行课前资料中的 seata-at.sql 脚本,分别在 hm-trade、hm-cart、hm-item 三个数据库中创建表:
1 | CREATE TABLE `undo_log` ( |
4.4 标记全局事务入口
分布式事务的入口是发起全局事务的方法(如交易服务的 “创建订单” 方法),需将 Spring 的 @Transactional 注解替换为 Seata 的 @GlobalTransactional,标记该方法为全局事务起点。
以 trade-service 的 OrderServiceImpl 类为例:
1 | import io.seata.spring.annotation.GlobalTransactional; |
@GlobalTransactional 的作用:
- 告诉 TM:此方法是全局事务的入口,需要发起全局事务;
- 自动向 TC 注册全局事务,生成全局事务 ID;
- 后续所有远程调用(如 Feign 调用购物车、库存服务)会自动携带全局事务 ID,让其他服务的 RM 能关联到该全局事务。
4.5 测试验证
- 重启
trade-service、cart-service、item-service三个服务; - 重复之前的测试步骤(修改商品库存为 0,提交订单);
- 观察结果:
- 库存服务因 “库存不足” 返回失败;
- 交易服务的 “创建订单” 回滚(订单表无新数据);
- 购物车服务的 “清空购物车” 回滚(购物车数据未被删除);
- 所有分支事务均回滚,全局数据一致。
5. Seata 的核心实现模式:XA 模式
Seata 支持 4 种分布式事务模式(XA、AT、TCC、SAGA),其中 XA 模式是基于数据库原生 XA 规范实现的强一致性方案,几乎所有主流数据库(MySQL、Oracle、PostgreSQL)都支持。
5.1 XA 规范与两阶段提交
XA 规范是 X/Open 组织定义的分布式事务标准,核心思想是两阶段提交(2PC),将分布式事务分为 “准备阶段” 和 “提交阶段”,确保所有分支事务要么同时成功,要么同时失败。
正常流程(所有分支事务成功)
- 一阶段(准备阶段:
- TC 向所有 RM 发送 “准备指令”;
- 每个 RM 执行本地分支事务(如 “扣减库存”“清空购物车”),但不提交事务,仅将事务状态设置为 “待提交”,并持有数据库锁;
- 每个 RM 向 TC 报告 “准备成功” 或 “准备失败”。
- 二阶段(提交阶段):
- TC 收到所有 RM 的 “准备成功” 报告,向所有 RM 发送 “提交指令”;
- 每个 RM 执行事务提交,释放数据库锁;
- 每个 RM 向 TC 报告 “提交成功”,全局事务完成。
异常流程(任意分支事务失败)
- 一阶段(准备阶段:
- TC 向所有 RM 发送 “准备指令”;
- 其中一个 RM 执行本地事务失败(如库存不足),向 TC 报告 “准备失败”;
- 二阶段(回滚阶段):
- TC 收到 “准备失败” 报告,向所有 RM 发送 “回滚指令”;
- 每个 RM 执行事务回滚,释放数据库锁;
- 每个 RM 向 TC 报告 “回滚成功”,全局事务回滚完成。
5.2 Seata 的 XA 模式实现
Seata 对原生 XA 模式进行了封装,适配自身的 TC/TM/RM 架构,流程如下:
| 角色 | 一阶段(准备阶段) | 二阶段(提交 / 回滚阶段) |
|---|---|---|
| TC | 1. 接收 TM 的全局事务请求,生成全局事务 ID; 2. 向所有 RM 发送 “分支事务准备指令”; 3. 收集所有 RM 的准备状态 |
1. 若所有 RM 准备成功:发送 “提交指令”; 2. 若任意 RM 准备失败:发送 “回滚指令”; 3. 接收 RM 的最终执行结果,通知 TM |
| RM | 1. 注册分支事务(绑定全局事务 ID); 2. 执行本地 SQL,不提交事务; 3. 向 TC 报告 “准备成功” 或 “准备失败” |
1. 接收 TC 指令; 2. 若为 “提交”:执行事务提交,释放锁; 3. 若为 “回滚”:执行事务回滚,释放锁; 4. 向 TC 报告执行结果 |
| TM | 1. 标记全局事务入口,向 TC 发起全局事务; 2. 调用各分支事务 |
1. 接收 TC 的全局事务结果; 2. 处理业务逻辑(如返回给前端成功 / 失败信息) |
5.3 XA 模式的优缺点
优点
- 强一致性:严格遵循 ACID 原则,所有分支事务要么同时提交,要么同时回滚,数据无不一致风险;
- 无代码侵入:基于数据库原生 XA 规范,无需修改业务代码,仅需配置 Seata 模式和添加注解;
- 兼容性好:几乎所有主流关系型数据库都支持 XA 规范,无需额外改造数据库。
缺点
- 性能差:一阶段执行完成后,数据库锁会一直持有,直到二阶段结束(提交或回滚)。若分支事务较多或执行时间长,会导致数据库锁竞争激烈,影响并发性能;
- 依赖数据库:仅支持关系型数据库,无法用于非关系型数据库(如 Redis、MongoDB)或第三方 API(如支付接口);
- 阻塞风险:若二阶段中 TC 与某个 RM 通信失败,RM 会一直持有锁,直到超时才释放,可能导致长时间阻塞。
5.4 XA 模式的实现步骤
步骤 1:配置 Seata 模式为 XA
在 Nacos 的 shared-seata.yaml 中添加配置,指定 Seata 使用 XA 模式:
1 | seata: |
步骤 2:标记全局事务入口
与之前一致,在全局事务入口方法上添加 @GlobalTransactional 注解(替换 @Transactional):
java
1 |
|
步骤 3:测试
重启所有服务后测试,此时 Seata 会以 XA 模式协调分布式事务,确保强一致性。
6. Seata 的核心实现模式:AT 模式
AT 模式是 Seata 的默认模式,也是企业中使用最广泛的模式。它基于 “两阶段提交” 思想,但对 XA 模式进行了优化,解决了 XA 模式中资源锁定时间过长的问题,实现了 “最终一致性”,兼顾性能和一致性。
6.1 AT 模式的核心原理
AT 模式的核心创新是数据快照(undo-log) 和一阶段提交:
- 一阶段:RM 执行本地事务并提交,同时记录 undo-log(数据修改前的快照),释放数据库锁;
- 二阶段:
- 若所有分支事务成功:删除 undo-log(无需回滚,快照无用);
- 若任意分支事务失败:通过 undo-log 恢复数据到修改前的状态(回滚)。
6.2 AT 模式的完整流程
以 “扣减用户余额” 为例(表 tb_account,初始数据:id=1,money=100,业务 SQL:update tb_account set money = money - 10 where id = 1),详细流程如下:
一阶段(执行并提交本地事务)
- TM 发起全局事务:
- TM 向 TC 注册全局事务,获取全局事务 ID(如
xid=123); - TM 调用 “扣减余额” 的分支事务。
- TM 向 TC 注册全局事务,获取全局事务 ID(如
- RM 执行分支事务:
- 步骤 1:注册分支事务:RM 向 TC 注册分支事务,绑定全局事务 ID(
xid=123); - 步骤 2:生成数据快照(undo-log:RM 拦截业务 SQL,根据
where id=1查询原始数据(id=1,money=100),将快照存入undo_log表; - 步骤 3:执行业务 SQL 并提交:RM 执行
update语句(money从 100 变为 90),立即提交本地事务,释放数据库锁; - 步骤 4:报告状态:RM 向 TC 报告分支事务 “执行成功”。
- 步骤 1:注册分支事务:RM 向 TC 注册分支事务,绑定全局事务 ID(
二阶段(提交或回滚)
情况 1:所有分支事务成功(提交)
- TC 向所有 RM 发送 “提交指令”;
- RM 收到指令后,删除
undo_log表中对应的快照(快照已无用); - RM 向 TC 报告 “提交成功”,全局事务完成。
情况 2:任意分支事务失败(回滚)
假设 “扣减库存” 分支事务失败,TC 触发回滚:
- TC 向 “扣减余额” 的 RM 发送 “回滚指令”;
- RM 查询
undo_log表,获取数据快照(id=1,money=100); - RM 执行回滚 SQL:
update tb_account set money = 100 where id = 1(将数据恢复到修改前); - RM 删除
undo_log表中的快照,向 TC 报告 “回滚成功”; - 其他分支事务(如 “清空购物车”)也按同样逻辑回滚,全局事务回滚完成。
6.3 AT 模式与 XA 模式的核心区别
| 对比维度 | XA 模式 | AT 模式 |
|---|---|---|
| 一阶段事务状态 | 执行 SQL,不提交,持有锁 | 执行 SQL 并提交,释放锁 |
| 回滚机制 | 依赖数据库原生回滚(事务未提交,直接回滚) | 依赖 undo-log 快照恢复数据(事务已提交,需反向更新) |
| 数据一致性 | 强一致性(所有分支事务同时提交 / 回滚) | 最终一致性(部分分支事务先提交,失败后回滚,短暂不一致) |
| 性能 | 差(锁持有时间长,并发低) | 好(锁释放快,并发高) |
| 代码侵入 | 无 | 无(自动生成 undo-log,无需修改业务代码) |
| 数据库依赖 | 仅支持关系型数据库(需支持 XA) | 仅支持关系型数据库(需支持事务) |
6.4 AT 模式的优缺点
优点
- 性能优异:一阶段提交本地事务,立即释放数据库锁,锁持有时间极短,并发能力远超 XA 模式;
- 无代码侵入:undo-log 由 Seata 自动生成和管理,无需修改业务代码,开发成本低;
- 最终一致性:虽然是最终一致性,但通过 undo-log 回滚能快速恢复数据,实际业务中可接受;
- 适用场景广:企业中 90% 以上的分布式事务场景(如电商下单、支付、库存扣减)都可使用 AT 模式。
缺点
- 不支持非关系型数据库:undo-log 依赖关系型数据库的事务和表结构,无法用于 Redis、MongoDB 等;
- 存在短暂不一致:若二阶段需要回滚,在回滚完成前,已提交的分支事务数据是 “脏数据”(如用户余额已扣减,但订单未创建),但回滚后会恢复,属于 “最终一致”;
- 并发控制风险:若多个事务同时修改同一数据,可能导致回滚失败(需依赖 Seata 的全局锁机制解决)。
6.5 AT 模式的实现步骤
AT 模式是 Seata 的默认模式,无需额外配置(若未指定 data-source-proxy-mode,默认即为 AT),仅需完成以下步骤:
- 确保已创建 undo-log 表:每个服务的数据库中已创建
undo_log表(参考 4.3 节); - 标记全局事务入口:在全局事务方法上添加
@GlobalTransactional注解; - 测试:重启服务后测试,Seata 会自动以 AT 模式协调事务。
7. 总结
分布式事务的核心问题是 “跨服务 / 跨数据库的数据一致性”,Seata 通过 TC/TM/RM 三角色架构,提供了 XA(强一致)和 AT(最终一致,高性能)两种主流解决方案:
- XA 模式:适合对一致性要求极高、并发要求低的场景(如金融核心交易);
- AT 模式:适合大多数业务场景(如电商、零售),兼顾性能和最终一致性,是企业首选。
通过 Seata 的集成,我们可以轻松解决微服务架构中的分布式事务问题,确保业务数据的一致性。
-
感谢你赐予我前进的力量