分布式事务

在微服务架构中,业务往往需要跨多个服务和数据库协作完成,此时传统的单体事务(ACID)无法保证全局数据一致性,分布式事务问题由此产生。本文将结合电商下单业务场景,详细讲解分布式事务的本质、解决方案(以 Seata 为例)及核心实现模式。

1. 分布式事务问题引入

1.1 下单业务场景分析

以电商平台的下单业务为例,完整流程涉及 3 个独立的微服务,每个服务对应独立数据库:

  • 交易服务(trade-service:负责创建订单,操作 hm-trade 数据库;
  • 购物车服务(cart-service):负责清空用户购物车中已下单的商品,操作 hm-cart 数据库;
  • 库存服务(item-service):负责扣减商品库存,操作 hm-item 数据库。

每个服务内部的操作都是本地事务(分支事务),例如:

  • 交易服务的 “创建订单” 是一个分支事务;
  • 购物车服务的 “清空购物车” 是一个分支事务;
  • 库存服务的 “扣减库存” 是一个分支事务。

这些分支事务共同构成一个全局事务,业务要求:所有分支事务必须同时成功或同时失败(例如:若库存不足导致下单失败,购物车已清空的数据必须回滚)。

1.2 分布式事务问题复现

我们通过实际测试验证分布式事务的问题:

  1. 初始状态:用户购物车中有 4 件商品,商品 A 库存为 10;
  2. 操作步骤
    • 用户选择购物车中所有商品结算,进入订单确认页;
    • 手动将商品 A 的库存修改为 0(模拟库存不足场景);
    • 用户提交订单,库存服务因 “库存不足” 返回失败,交易服务下单失败;
  3. 异常结果:查看购物车时发现,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 的工作流程(简化)

  1. TM 发起全局事务,向 TC 注册全局事务,获取全局事务 ID;
  2. TM 调用各个分支事务(如调用购物车服务清空接口、库存服务扣减接口);
  3. 每个 RM 在执行本地分支事务前,向 TC 注册分支事务(绑定全局事务 ID);
  4. RM 执行本地分支事务,向 TC 报告执行结果(成功 / 失败);
  5. TC 汇总所有分支事务状态:
    • 若全部成功:通知所有 RM 提交分支事务;
    • 若任意失败:通知所有 RM 回滚分支事务;
  6. 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
2
3
4
# 将nacos容器加入hm-net网络(容器名替换为实际名称)
docker network connect hm-net nacos
# 将mysql容器加入hm-net网络
docker network connect hm-net mysql

执行部署命令

在虚拟机 /root 目录下执行 Docker 命令,启动 Seata TC 服务:

1
2
3
4
5
6
7
8
9
docker run --name seata \
-p 8099:8099 \ # TC服务的HTTP端口(微服务与TC通信)
-p 7099:7099 \ # TC服务的RPC端口(内部通信)
-e SEATA_IP=192.168.150.101 \ # 虚拟机IP(TC服务的地址)
-v ./seata:/seata-server/resources \ # 挂载配置文件目录(覆盖默认配置)
--privileged=true \ # 赋予容器权限,避免配置文件读写问题
--network hm-net \ # 加入与Nacos、MySQL相同的网络
-d \ # 后台运行
seataio/seata-server:1.5.2 # 镜像名称与版本

镜像加载备选方案

若 Docker Hub 下载镜像缓慢,可通过课前资料提供的镜像文件本地加载:

bash

1
2
# 加载本地镜像文件(xxx.tar为镜像文件名)
docker load -i xxx.tar

4. 微服务集成 Seata

所有参与分布式事务的微服务(交易、购物车、库存服务)都需集成 Seata 的 TM 和 RM 客户端。以下以 trade-service(交易服务) 为例,其他服务步骤类似。

4.1 引入依赖

trade-servicepom.xml 中添加 Seata 和 Nacos 配置依赖(Nacos 用于共享 Seata 配置,避免每个服务重复配置):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 1. Nacos统一配置管理:用于加载共享的Seata配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 2. 读取bootstrap.yaml配置文件(优先级高于application.yaml) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 3. Seata客户端:集成TM和RM功能 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

4.2 改造配置

步骤 1:在 Nacos 添加共享 Seata 配置

为避免每个服务重复配置 Seata,在 Nacos 中创建共享配置文件 shared-seata.yaml,所有服务可直接引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
seata:
registry: # TC服务注册中心配置(微服务通过此配置找到TC)
type: nacos # 注册中心类型为Nacos
nacos:
server-addr: 192.168.150.101:8848 # Nacos地址(需与实际一致)
namespace: "" # Nacos命名空间(默认为空,若有自定义需修改)
group: DEFAULT_GROUP # 配置分组(默认DEFAULT_GROUP)
application: seata-server # TC服务在Nacos中的服务名(需与TC部署时一致)
username: nacos # Nacos用户名
password: nacos # Nacos密码
tx-service-group: hmall # 事务组名称(自定义,所有服务需一致)
service:
vgroup-mapping: # 事务组与TC集群的映射(默认映射到"default"集群)
hmall: "default"

步骤 2:改造 trade-service 的配置文件

  • 添加 bootstrap.yaml:用于加载 Nacos 中的共享配置(包括 Seata 配置),内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    spring:
    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
    13
    server:
    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(库存服务):

  1. 引入相同的 Seata 和 Nacos 依赖;
  2. 添加 bootstrap.yaml,加载 shared-seata.yaml 等共享配置;
  3. 确保 tx-service-group(事务组名称)与 trade-service 一致(均为 hmall)。

4.3 添加数据库表(Seata 客户端)

Seata 的 RM 客户端在执行分支事务时,需要记录数据快照(undo-log) 用于回滚(AT 模式),因此需在每个服务对应的数据库中创建 undo_log 表。

执行课前资料中的 seata-at.sql 脚本,分别在 hm-tradehm-carthm-item 三个数据库中创建表:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`branch_id` bigint NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

4.4 标记全局事务入口

分布式事务的入口是发起全局事务的方法(如交易服务的 “创建订单” 方法),需将 Spring 的 @Transactional 注解替换为 Seata 的 @GlobalTransactional,标记该方法为全局事务起点。

trade-serviceOrderServiceImpl 类为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;

@Service
public class OrderServiceImpl implements IOrderService {

// 替换@Transactional为@GlobalTransactional,标记全局事务入口
@GlobalTransactional(rollbackFor = Exception.class)
@Override
public OrderVO createOrder(OrderCreateDTO orderDTO) {
// 1. 创建订单(本地分支事务)
Order order = createOrderInDB(orderDTO);
// 2. 调用购物车服务,清空已下单商品(远程分支事务)
cartFeign.clearCart(orderDTO.getUserId(), orderDTO.getCartItemIds());
// 3. 调用库存服务,扣减商品库存(远程分支事务)
itemFeign.deductStock(orderDTO.getItems());
// 4. 返回订单结果
return convert(order);
}

// 其他私有方法...
}

@GlobalTransactional 的作用:

  1. 告诉 TM:此方法是全局事务的入口,需要发起全局事务;
  2. 自动向 TC 注册全局事务,生成全局事务 ID;
  3. 后续所有远程调用(如 Feign 调用购物车、库存服务)会自动携带全局事务 ID,让其他服务的 RM 能关联到该全局事务。

4.5 测试验证

  1. 重启 trade-servicecart-serviceitem-service 三个服务;
  2. 重复之前的测试步骤(修改商品库存为 0,提交订单);
  3. 观察结果:
    • 库存服务因 “库存不足” 返回失败;
    • 交易服务的 “创建订单” 回滚(订单表无新数据);
    • 购物车服务的 “清空购物车” 回滚(购物车数据未被删除);
    • 所有分支事务均回滚,全局数据一致。

5. Seata 的核心实现模式:XA 模式

Seata 支持 4 种分布式事务模式(XA、AT、TCC、SAGA),其中 XA 模式是基于数据库原生 XA 规范实现的强一致性方案,几乎所有主流数据库(MySQL、Oracle、PostgreSQL)都支持。

5.1 XA 规范与两阶段提交

XA 规范是 X/Open 组织定义的分布式事务标准,核心思想是两阶段提交(2PC),将分布式事务分为 “准备阶段” 和 “提交阶段”,确保所有分支事务要么同时成功,要么同时失败。

正常流程(所有分支事务成功)

  1. 一阶段(准备阶段
    • TC 向所有 RM 发送 “准备指令”;
    • 每个 RM 执行本地分支事务(如 “扣减库存”“清空购物车”),但不提交事务,仅将事务状态设置为 “待提交”,并持有数据库锁;
    • 每个 RM 向 TC 报告 “准备成功” 或 “准备失败”。
  2. 二阶段(提交阶段)
    • TC 收到所有 RM 的 “准备成功” 报告,向所有 RM 发送 “提交指令”;
    • 每个 RM 执行事务提交,释放数据库锁;
    • 每个 RM 向 TC 报告 “提交成功”,全局事务完成。

异常流程(任意分支事务失败)

  1. 一阶段(准备阶段
    • TC 向所有 RM 发送 “准备指令”;
    • 其中一个 RM 执行本地事务失败(如库存不足),向 TC 报告 “准备失败”;
  2. 二阶段(回滚阶段)
    • 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 模式的优缺点

优点

  1. 强一致性:严格遵循 ACID 原则,所有分支事务要么同时提交,要么同时回滚,数据无不一致风险;
  2. 无代码侵入:基于数据库原生 XA 规范,无需修改业务代码,仅需配置 Seata 模式和添加注解;
  3. 兼容性好:几乎所有主流关系型数据库都支持 XA 规范,无需额外改造数据库。

缺点

  1. 性能差:一阶段执行完成后,数据库锁会一直持有,直到二阶段结束(提交或回滚)。若分支事务较多或执行时间长,会导致数据库锁竞争激烈,影响并发性能;
  2. 依赖数据库:仅支持关系型数据库,无法用于非关系型数据库(如 Redis、MongoDB)或第三方 API(如支付接口);
  3. 阻塞风险:若二阶段中 TC 与某个 RM 通信失败,RM 会一直持有锁,直到超时才释放,可能导致长时间阻塞。

5.4 XA 模式的实现步骤

步骤 1:配置 Seata 模式为 XA

在 Nacos 的 shared-seata.yaml 中添加配置,指定 Seata 使用 XA 模式:

1
2
3
seata:
# 其他配置...(registry、tx-service-group等)
data-source-proxy-mode: XA # 配置Seata的数据源代理模式为XA

步骤 2:标记全局事务入口

与之前一致,在全局事务入口方法上添加 @GlobalTransactional 注解(替换 @Transactional):

java

1
2
3
4
@GlobalTransactional(rollbackFor = Exception.class)
public OrderVO createOrder(OrderCreateDTO orderDTO) {
// 业务逻辑...
}

步骤 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),详细流程如下:

一阶段(执行并提交本地事务)

  1. TM 发起全局事务
    • TM 向 TC 注册全局事务,获取全局事务 ID(如 xid=123);
    • TM 调用 “扣减余额” 的分支事务。
  2. 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:所有分支事务成功(提交)
  • 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 模式的优缺点

优点

  1. 性能优异:一阶段提交本地事务,立即释放数据库锁,锁持有时间极短,并发能力远超 XA 模式;
  2. 无代码侵入:undo-log 由 Seata 自动生成和管理,无需修改业务代码,开发成本低;
  3. 最终一致性:虽然是最终一致性,但通过 undo-log 回滚能快速恢复数据,实际业务中可接受;
  4. 适用场景广:企业中 90% 以上的分布式事务场景(如电商下单、支付、库存扣减)都可使用 AT 模式。

缺点

  1. 不支持非关系型数据库:undo-log 依赖关系型数据库的事务和表结构,无法用于 Redis、MongoDB 等;
  2. 存在短暂不一致:若二阶段需要回滚,在回滚完成前,已提交的分支事务数据是 “脏数据”(如用户余额已扣减,但订单未创建),但回滚后会恢复,属于 “最终一致”;
  3. 并发控制风险:若多个事务同时修改同一数据,可能导致回滚失败(需依赖 Seata 的全局锁机制解决)。

6.5 AT 模式的实现步骤

AT 模式是 Seata 的默认模式,无需额外配置(若未指定 data-source-proxy-mode,默认即为 AT),仅需完成以下步骤:

  1. 确保已创建 undo-log 表:每个服务的数据库中已创建 undo_log 表(参考 4.3 节);
  2. 标记全局事务入口:在全局事务方法上添加 @GlobalTransactional 注解;
  3. 测试:重启服务后测试,Seata 会自动以 AT 模式协调事务。

7. 总结

分布式事务的核心问题是 “跨服务 / 跨数据库的数据一致性”,Seata 通过 TC/TM/RM 三角色架构,提供了 XA(强一致)和 AT(最终一致,高性能)两种主流解决方案:

  • XA 模式:适合对一致性要求极高、并发要求低的场景(如金融核心交易);
  • AT 模式:适合大多数业务场景(如电商、零售),兼顾性能和最终一致性,是企业首选。

通过 Seata 的集成,我们可以轻松解决微服务架构中的分布式事务问题,确保业务数据的一致性。