Seata 入门教程 - 实战篇(2024 年)

大纲

前言

官方资源

实战案例一

1、案例说明

本案例是电商实战项目,使用了 Seata 的 AT 模式,并基于 Seata 分布式交易解决方案(如下图所示)。

1.1、版本说明

在本文的所有案例中,各个组件统一使用以下版本:

组件版本说明
MySQL8.4.2
Nacos Server2.4.3
Seata Server(TC)2.2.0
Spring Boot3.2.9
Spring Cloud2023.0.1
Spring Cloud Alibaba2023.0.1.3

若希望降低 SpringBoot 与 Spring Cloud 的版本,可以尝试以下版本组合(仅供参考):

组件版本组合一版本组合二说明
MySQL8.4.28.4.2
Nacos Server2.2.32.2.3
Seata Server(TC)2.0.02.0.0
Spring Boot3.1.73.0.9
Spring Cloud2022.0.42022.0.2
Spring Cloud Alibaba2022.0.0.02022.0.0.0

特别注意

Spring Boot 和 Spring Cloud 以及 Spring Cloud Alibaba 的版本号需要互相匹配,还有 Seata Server(TC)的版本也要匹配,否则可能会存在各种兼容问题,具体可以参考官方的版本说明

1.2、案例目标

本案例将创建三个服务,分别是订单服务、库存服务、账户服务,各服务之间的调用流程如下:

  • 1)当用户下单时,调用订单服务创建一个订单,然后通过远程调用(OpenFeign)让库存服务扣减下单商品的库存
  • 2)订单服务再通过远程调用(OpenFeign)让账户服务来扣减用户账户里面的余额
  • 3)最后在订单服务中修改订单状态为已完成

上述操作跨越了三个数据库,有两次远程调用,很明显会有分布式事务的问题,项目的整体结构如下:

1
2
3
4
5
seata-transaction-demo
├── seata-common-api # API模块
├── seata-account-service # 账户模块,端口:8085
├── seata-storage-service # 库存模块,端口:8086
└── seata-order-service # 订单模块,端口:8087

1.3、代码下载

  • 由于篇幅有限,本案例只给出各个模块的核心代码和配置,完整的案例代码可以从 这里 下载得到。

2、准备工作

2.1、安装 Seata

特别注意

严格根据上述的教程内容,通过 Docker 安装好 Seata Server(TC)后,可能需要额外执行以下步骤,保证 SpringBoot 应用(即 Seata Client)可以正常连接 Seata Server(TC)。

若 Spring Boot 应用不是部署 Docker 容器内(即与 Seata 容器处于不同的网络),那么在 docker-compose.yml 文件中,需要添加 SEATA_IP 环境变量来指定 Seata Server(TC) 注册到 Nacos 注册中心的 IP 地址,并且这里的 IP 地址必须是 SpringBoot 应用可以正常访问的(比如指定为宿主机的 IP),否则 Docker 容器外的 SpringBoot 应用在启动时会无法连接 Seata Server(TC),导致启动失败。配置示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
version: '3.5'

service:

seata:
image: seataio/seata-server:2.0.0
container_name: spring-cloud-2024-seata-server
restart: always
ports:
- 8091:8091
- 7091:7091
environment:
- SEATA_IP=127.0.0.1 # 指定 Seata Server(TC)注册到 Nacos 注册中心的 IP 地址
- SEATA_PORT=8091
- TZ=Asia/Shanghai
volumes:
- /usr/local/docker/docker-volumes/seata/config/resources:/seata-server/resources
- /usr/local/docker/docker-volumes/seata/logs:/var/log/seata
depends_on:
mysql8:
condition: service_healthy
nacos:
condition: service_healthy
networks:
- cloud-network

2.2、初始化数据库

这里使用 MySQL 数据库来存储 Seata Server(TC)的全局事务会话信息,因此需要执行 SQL 初始化脚本(完整) 来创建 Seata 所需的数据库,还有业务所需的数据库。由于 Seata 的 AT 模式需要用到 UNDO_LOG 回滚日志表,因此在每个业务数据库里都要单独创建 UNDO_LOG 回滚日志表,最终所有用到的数据库和业务表如下图所示:

提示

  • 针对 Seata 2.x 版本,Seata Server(TC)所需的多个表结构可以从 这里 获取得到。
  • 针对 Seata 2.x 版本,Seata Client 所需的 UNDO_LOG 表结构可以从 这里 获取得到。

2.3、初始化 Nacos 配置

在 Nacos 配置中心里面,添加 Seata Client 所需的配置信息,SpringBoot 应用(即 Seata Client)在启动时会读取相应的配置信息,如下图所示:

  • 命名空间: public
  • 分组名称: SEATA_GROUP
  • Data ID:seata-client.properties
  • 配置信息:service.vgroupMapping.default_tx_group=default

3、创建 Maven 父工程

创建 Maven 父工程,配置好工程需要的父级依赖,目的是为了更方便管理与简化配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<modules>
<module>seata-order-service</module>
<module>seata-storage-service</module>
<module>seata-account-service</module>
<module>seata-common-api</module>
</modules>

<properties>
<mysql.version>8.0.11</mysql.version>
<druid.version>1.1.20</druid.version>
<mybatis.springboot.version>3.0.3</mybatis.springboot.version>
<fastjson2.version>2.0.40</fastjson2.version>
<hutool.version>5.8.22</hutool.version>
<lombok.version>1.18.26</lombok.version>
<spring.boot.version>3.2.9</spring.boot.version>
<spring.cloud.version>2023.0.1</spring.cloud.version>
<spring.cloud.alibaba.version>2023.0.1.3</spring.cloud.alibaba.version>
<spring.boot.test.version>3.1.5</spring.boot.test.version>
</properties>

<dependencyManagement>
<dependencies>
<!--SpringBoot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--SpringCloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--SpringCloud Alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--MySQL数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--Druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!--MyBatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.springboot.version}</version>
</dependency>
<!--Fastjson2-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!--HuTool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!--SpringBoot Test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.test.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>

4、创建订单工程

4.1、创建 pom.xml

订单工程的 Maven 配置内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<dependencies>
<!--seata-common-api-->
<dependency>
<groupId>com.clay</groupId>
<artifactId>seata-common-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--OpenFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--LoadBalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--Nacos Discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--Nacos Config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--Seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--MySQL数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--Druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!--MyBatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Fastjson2-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--HuTool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

4.2、创建 application.yml

提示

Seata Server(TC) 从 1.5.0 版本开始,配置文件改为 application.yml,同时向下兼容旧的 register.conffile.conf 两个配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
server:
port: 8085

spring:
application:
name: seata-order-service

# 注册中心
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848

# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: seata
password: seata

# Druid 连接池
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall,log4j
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

# MyBatis
mybatis:
mapper-locations: classpath*:mybatis/mapper/*.xml
type-aliases-package: com.clay.entities
configuration:
map-underscore-to-camel-case: true

# Seata
seata:
# 指定从 Nacos 注册中心获取 Seata Server(TC)的地址
registry:
type: nacos
nacos:
application: seata-server # Seata Server(TC)的服务名称
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP
cluster: default
# 指定从 Nacos 配置中心获取 Seata Client 的配置信息
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP
data-id: seata-client.properties
username:
password:
tx-service-group: default_tx_group # 事务组,由它获得 Seata Server(TC)服务的集群名称
service:
vgroup-mapping:
default_tx_group: default # 事务组与 Seata Server(TC)服务集群的映射关系
data-source-proxy-mode: AT
application-id: ${spring.application.name}

# 日志
logging:
level:
io:
seata: info

4.3、添加全局事务注解

在订单创建的入口方法上面添加 @GlobalTransactional 来控制分布式事务,这里使用 OpenFeign 去调用库存服务和账户服务的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

@Resource
private OrderMapper orderMapper;

@Resource
private StorageFeignApi storageFeignApi;

@Resource
private AccountFeignApi accountFeignApi;

@Override
@GlobalTransactional(name = "seata-create-order", rollbackFor = Exception.class)
public ResultData create(Order order) {
// 查看 Seata 的 XID
String xid = RootContext.getXID();
log.info("==================>开始创建订单" + "\t" + "xid: " + xid);

// 1. 插入订单
order.setStatus(0);
int result = orderMapper.insert(order);

Order orderFromDB = null;
if (result > 0) {
// 插入订单成功后,获取插入MySQL的订单实体对象
orderFromDB = orderMapper.selectById(order.getId());
log.info("-------> 创建订单成功,orderFromDB info: " + orderFromDB);
System.out.println();

// 2. 扣减库存
log.info("-------> 订单微服务开始调用Storage库存服务,开始扣减库存");
ResultData storageResult = storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());
Assert.isTrue(storageResult.isSuccess(), storageResult.getMessage());
log.info("-------> 订单微服务结束调用Storage库存服务,扣减库存完成");
System.out.println();

// 3. 扣减账户余额
log.info("-------> 订单微服务开始调用Account账户服务,开始扣减账户余额");
ResultData accountResult = accountFeignApi.decrease(orderFromDB.getUserId(), orderFromDB.getMoney());
Assert.isTrue(accountResult.isSuccess(), accountResult.getMessage());
log.info("-------> 订单微服务结束调用Account账户服务,扣减账户余额完成");
System.out.println();

// 4. 修改订单状态
log.info("-------> 开始修改订单状态");
int updateResult = orderMapper.updateStatus(orderFromDB.getId(), 1);
log.info("-------> 修改订单状态完成" + "\t" + "修改结果: " + updateResult);
} else {
log.info("-------> 订单创建失败");
}

System.out.println();
log.info("==================>结束创建订单" + "\t" + "xid: " + xid);

return ResultData.success(orderFromDB);
}

}

4.4、创建主启动类

1
2
3
4
5
6
7
8
9
10
11
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.clay.mapper")
public class SeataOrderApplication {

public static void main(String[] args) {
SpringApplication.run(SeataOrderApplication.class, args);
}

}

5. 创建库存工程

5.1、创建 pom.xml

库存工程的 Maven 配置内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<dependencies>
<!--seata-common-api-->
<dependency>
<groupId>com.clay</groupId>
<artifactId>seata-common-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--OpenFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--LoadBalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--Nacos Discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--Nacos Config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--Seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--MySQL数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--Druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!--MyBatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Fastjson2-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--HuTool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

5.2、创建 application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
server:
port: 8086

spring:
application:
name: seata-storage-service

# 注册中心
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848

# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: seata
password: seata

# Druid 连接池
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall,log4j
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

# MyBatis
mybatis:
mapper-locations: classpath*:mybatis/mapper/*.xml
type-aliases-package: com.clay.entities
configuration:
map-underscore-to-camel-case: true

# Seata
seata:
# 指定从 Nacos 注册中心获取 Seata Server(TC)的地址
registry:
type: nacos
nacos:
application: seata-server # Seata Server(TC)的服务名称
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP
cluster: default
# 指定从 Nacos 配置中心获取 Seata Client 的配置信息
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP
data-id: seata-client.properties
username:
password:
tx-service-group: default_tx_group # 事务组,由它获得 Seata Server(TC)服务的集群名称
service:
vgroup-mapping:
default_tx_group: default # 事务组与 Seata Server(TC)服务集群的映射关系
data-source-proxy-mode: AT
application-id: ${spring.application.name}

# 日志
logging:
level:
io:
seata: info

5.3、创建业务处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {

@Resource
private StorageMapper storageMapper;

@Override
public ResultData decrease(Long productId, Integer count) {
Storage storage = storageMapper.selectByProductId(productId);
Integer residue = storage.getResidue();
// 校验参数
if (count == null || count <= 0) {
return ResultData.fail(ReturnCodeEnum.RC380);
}
// 判断库存是否足够
if (count > residue) {
return ResultData.fail(ReturnCodeEnum.STORAGE_NOT_ENOUGH);
}

log.info("------->库存服务中扣减库存开始");
storageMapper.decrease(productId, count);
log.info("------->库存服务中扣减库存结束");

return ResultData.success();
}

}

5.4、创建主启动类

1
2
3
4
5
6
7
8
9
10
11
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.clay.mapper")
public class SeataStorageApplication {

public static void main(String[] args) {
SpringApplication.run(SeataStorageApplication.class, args);
}

}

6、创建账户工程

6.1、创建 pom.xml

账户工程的 Maven 配置内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<dependencies>
<!--seata-common-api-->
<dependency>
<groupId>com.clay</groupId>
<artifactId>seata-common-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--OpenFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--LoadBalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--Nacos Discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--Nacos Config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--Seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--MySQL数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--Druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!--MyBatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Fastjson2-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--HuTool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

6.2、创建 application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
server:
port: 8087

spring:
application:
name: seata-account-service

# 注册中心
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848

# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: seata
password: seata

# Druid 连接池
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall,log4j
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

# MyBatis
mybatis:
mapper-locations: classpath*:mybatis/mapper/*.xml
type-aliases-package: com.clay.entities
configuration:
map-underscore-to-camel-case: true

# Seata
seata:
# 指定从 Nacos 注册中心获取 Seata Server(TC)的地址
registry:
type: nacos
nacos:
application: seata-server # Seata Server(TC)的服务名称
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP
cluster: default
# 指定从 Nacos 配置中心获取 Seata Client 的配置信息
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP
data-id: seata-client.properties
username:
password:
tx-service-group: default_tx_group # 事务组,由它获得 Seata Server(TC)服务的集群名称
service:
vgroup-mapping:
default_tx_group: default # 事务组与 Seata Server(TC)服务集群的映射关系
data-source-proxy-mode: AT
application-id: ${spring.application.name}

# 日志
logging:
level:
io:
seata: info

6.3、创建业务处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {

@Resource
private AccountMapper accountMapper;

@Override
public ResultData decrease(Long userId, BigDecimal money) {
Account account = accountMapper.selectByUserId(userId);
BigDecimal residue = account.getResidue();
// 校验参数
if (money == null || money.compareTo(BigDecimal.ZERO) < 1) {
return ResultData.fail(ReturnCodeEnum.RC380);
}
// 判断余额是否足够
if (money.compareTo(residue) == 1) {
return ResultData.fail(ReturnCodeEnum.ACCOUNT_NOT_ENOUGH);
}

log.info("------->账户服务中扣减账户余额开始");
accountMapper.decrease(userId, money);

// 模拟超时异常,全局事务回滚
myTimeOut();

log.info("------->账户服务中扣减账户余额结束");

return ResultData.success();
}

/**
* 模拟超时异常,全局事务回滚
*
* <p> OpenFeign 的默认超时时间是60秒
*/
private static void myTimeOut() {
try {
TimeUnit.SECONDS.sleep(65);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

/**
* 模拟业务处理抛出异常,全局事务回滚
*/
private static void myCompute() {
int a = 10 / 0;
}

}

6.4、创建主启动类

1
2
3
4
5
6
7
8
9
10
11
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.clay.mapper")
public class SeataAccountApplication {

public static void main(String[] args) {
SpringApplication.run(SeataAccountApplication.class, args);
}

}

7、案例代码测试

  • 1)首先启动 MySQL Server、Nacos Server、Seata Server,并按照上文介绍的准备工作进行初始化

  • 2)分别启动 seata-order-serviceseata-account-serviceseata-storage-service 服务

  • 3)浏览器访问 http://127.0.0.1:8848/nacos 打开 Nacos 的控制台,各服务成功启动后,在 Nacos 的控制台里可以看到有多个服务已注册(如下图所示)

  • 4)观察不同数据库中的 seata_account.t_accountseata_storage.t_storage 业务表的数据,如下图所示:

  • 5)使用 Postman 调用订单创建接口 http://127.0.0.1:8085/order/create?userId=1&count=3&money=20&productId=1,由于账户服务有阻塞等待的耗时操作,此时通过浏览器 http://127.0.0.1:7091 访问 Seata 的控制台页面,可以看到对应的事务信息和全局锁信息,如下图所示:

  • 6)由于订单服务使用 OpenFeign 远程调用账户服务来扣减账户余额时,抛出了请求超时的异常,因此订单创建接口的最终响应结果如下:
1
2
3
4
5
6
{
"timestamp": "2024-11-09T07:15:52.746+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/order/create"
}
  • 7)再观察不同数据库中的 seata_account.t_accountseata_storage.t_storage 业务表的数据是否发生了变更,若数据没有变更,则说明全局事务注解 @GlobalTransactional 生效了,否则注解没有生效

  • 8)创建订单的接口结束调用后,可以看到三个应用在控制台输出的日志如下:

1
2
3
4
5
6
7
8
9
################### seata-order 服务的日志 #####################

java.net.SocketTimeoutException: Read timed out
at java.base/sun.nio.ch.NioSocketImpl.timedRead(NioSocketImpl.java:283) ~[na:na]
at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:309) ~[na:na]
at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:350) ~[na:na]
at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:803) ~[na:na]
at java.base/java.net.Socket$SocketInputStream.read(Socket.java:966) ~[na:na]
at java.base/java.io.BufferedInputStream.fill(BufferedInputStream.java:244) ~[na:na]
1
2
3
4
5
6
7
8
9
10
11
12
################### seata-storage 服务的日志 #####################

[seata-storage-service] [nio-8086-exec-1] c.clay.service.impl.StorageServiceImpl : ------->库存服务中扣减库存开始
[seata-storage-service] [nio-8086-exec-1] o.a.seata.rm.AbstractResourceManager : branch register success, xid:127.0.0.1:8091:18598824581371085, branchId:18598824581371091, lockKeys:t_storage:1
[seata-storage-service] [nio-8086-exec-1] ServiceLoader$InnerEnhancedServiceLoader : Load [org.apache.seata.rm.datasource.undo.parser.ProtostuffUndoLogParser] class fail: io/protostuff/runtime/IdStrategy
[seata-storage-service] [nio-8086-exec-1] ServiceLoader$InnerEnhancedServiceLoader : Load compatible class io.seata.rm.datasource.undo.parser.spi.JacksonSerializer
[seata-storage-service] [nio-8086-exec-1] c.clay.service.impl.StorageServiceImpl : ------->库存服务中扣减库存结束
[seata-storage-service] [_RMROLE_1_1_144] o.a.s.c.r.p.c.RmBranchRollbackProcessor : rm handle branch rollback process:BranchRollbackRequest{xid='127.0.0.1:8091:18598824581371085', branchId=18598824581371091, branchType=AT, resourceId='jdbc:mysql://127.0.0.1:3306/seata_storage', applicationData='null'}
[seata-storage-service] [_RMROLE_1_1_144] org.apache.seata.rm.AbstractRMHandler : Branch Rollbacking: 127.0.0.1:8091:18598824581371085 18598824581371091 jdbc:mysql://127.0.0.1:3306/seata_storage
[seata-storage-service] [_RMROLE_1_1_144] o.a.s.r.d.undo.AbstractUndoLogManager : xid 127.0.0.1:8091:18598824581371085 branch 18598824581371091, undo_log deleted with GlobalFinished
[seata-storage-service] [_RMROLE_1_1_144] o.a.s.rm.datasource.DataSourceManager : branch rollback success, xid:127.0.0.1:8091:18598824581371085, branchId:18598824581371091
[seata-storage-service] [_RMROLE_1_1_144] org.apache.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
1
2
3
4
5
6
7
8
9
10
11
12
################### seata-account 服务的日志 #####################

[seata-account-service] [nio-8087-exec-1] c.clay.service.impl.AccountServiceImpl : ------->账户服务中扣减账户余额开始
[seata-account-service] [nio-8087-exec-1] o.a.seata.rm.AbstractResourceManager : branch register success, xid:127.0.0.1:8091:18598824581371085, branchId:18598824581371094, lockKeys:t_account:1
[seata-account-service] [nio-8087-exec-1] ServiceLoader$InnerEnhancedServiceLoader : Load [org.apache.seata.rm.datasource.undo.parser.ProtostuffUndoLogParser] class fail: io/protostuff/runtime/IdStrategy
[seata-account-service] [nio-8087-exec-1] ServiceLoader$InnerEnhancedServiceLoader : Load compatible class io.seata.rm.datasource.undo.parser.spi.JacksonSerializer
[seata-account-service] [_RMROLE_1_1_144] o.a.s.c.r.p.c.RmBranchRollbackProcessor : rm handle branch rollback process:BranchRollbackRequest{xid='127.0.0.1:8091:18598824581371085', branchId=18598824581371094, branchType=AT, resourceId='jdbc:mysql://127.0.0.1:3306/seata_account', applicationData='null'}
[seata-account-service] [_RMROLE_1_1_144] org.apache.seata.rm.AbstractRMHandler : Branch Rollbacking: 127.0.0.1:8091:18598824581371085 18598824581371094 jdbc:mysql://127.0.0.1:3306/seata_account
[seata-account-service] [_RMROLE_1_1_144] o.a.s.r.d.undo.AbstractUndoLogManager : xid 127.0.0.1:8091:18598824581371085 branch 18598824581371094, undo_log deleted with GlobalFinished
[seata-account-service] [_RMROLE_1_1_144] o.a.s.rm.datasource.DataSourceManager : branch rollback success, xid:127.0.0.1:8091:18598824581371085, branchId:18598824581371094
[seata-account-service] [_RMROLE_1_1_144] org.apache.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
[seata-account-service] [nio-8087-exec-1] c.clay.service.impl.AccountServiceImpl : ------->账户服务中扣减账户余额结束

参考资料