spring_cloud_alibaba之seata使用

概述

https://seata.io/zh-cn/

https://seata.io/zh-cn/blog/seata-quick-start/

用户文档要看一遍

部署TC server

https://github.com/seata/seata/releases

下载对应版本的压缩包。

解压:

1
tar -zxvf seata-server-1.7.1.tar.gz

单机启动

查看脚本seata-setup.sh发现日志存放位置,为空时默认$HOME/logs/seata

1
2
mkdir /root/logs/seata
sh ./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m file

集群布署

存储使用DB

单机启动的是使用的file模式。

在集群时,多个 Seata TC Server 通过 db 数据库,实现全局事务会话信息的共享。

在解压的目录中例如:/root/seata/script/server/db,mysql.sql是mysql数据库的表。

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
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

新建seata数据库:

1
CREATE DATABASE `seata` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_0900_ai_ci';

并执行上面的mysql.sql内容。

修改配置文件:

3.0版本以前是conf/file.conf。

3.0版本是conf/application.yml 与 conf/application.example.yml

修改application.yml中

1
2
3
store:
# support: file 、 db 、 redis
mode: db

根据application.example.yml把下面内容添加到application.yml中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.31.80:3306/seata?rewriteBatchedStatements=true
user: root
password: 123456
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 1000
max-wait: 5000

这里要注意mysql的连接数

1
2
3
4
-- 查看连接数
show variables like "%max_connections%"
-- 设置连接数
SET GLOBAL max_connections = 1000;

服务注册与配置中心

同样是修改application.yml

1
2
3
4
5
6
7
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: file
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos

这里我只做服务注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
registry:
# support: nacos 、 eureka 、 redis 、 zk 、 consul 、 etcd3 、 sofa
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: DEFAULT_GROUP
namespace:
cluster: default
username: nacos
password: nacos
context-path:
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key:
#secret-key:

启动

1
nohup sh bin/seata-server.sh -p 18091 -n 1 & 

-p 为Seata TC Server 监听的端口。

-n Server node。在多个 TC Server 时,需区分各自节点,用于生成不同区间的 transactionId 事务编号,以免冲突。

修改application.yml中 server.port=7091 改为 其他的端口,这个端口是spring boot项目的端口。

1
nohup sh bin/seata-server.sh -p 28091 -n 2 & 

java应用

https://seata.io/zh-cn/docs/user/quickstart

https://seata.io/zh-cn/docs/ops/deploy-guide-beginner

SQL

1
2
3
CREATE DATABASE `seata_storage` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_0900_ai_ci';
CREATE DATABASE `seata_order` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_0900_ai_ci';
CREATE DATABASE `seata_account` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_0900_ai_ci';
1
2
3
4
5
6
7
8
9
10
use seata_storage;
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `seata_storage`.`storage_tbl`(`id`, `commodity_code`, `count`) VALUES (1, 'G0001', 100);
1
2
3
4
5
6
7
8
9
10
use seata_order;
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1
2
3
4
5
6
7
8
9
use seata_account;
DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `seata_account`.`account_tbl`(`id`, `user_id`, `money`) VALUES (1, '1', 10000);

每个库同样加上seata使用的表:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=utf8;

应用搭建

先创建父工程springcloud-alibaba-seata:

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>springcloud-alibaba-seata</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<properties>
<spring-boot.version>3.0.2</spring-boot.version>
<spring-cloud.version>2022.0.0</spring-cloud.version>
<spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>

<dependencyManagement>
<dependencies>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</dependency>

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>
</dependencies>
</dependencyManagement>

</project>

新建模块storage-service、account-service、order-service:

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>springcloud-alibaba-seata</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>order-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>order-service</name>
<description>order-service</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>

<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>


<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
1
2
3
4
5
6
spring.application.name=order-service

spring.cloud.nacos.config.server-addr=192.168.80.3:8848
spring.cloud.nacos.config.username=nacos
spring.cloud.nacos.config.password=nacos
spring.cloud.nacos.config.file-extension=properties

nacos中存储的配置有:

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
server.port=7002
server.servlet.context-path=/order

spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/seata_order?useSSL=true\
&useUnicode=true\
&characterEncoding=UTF-8\
&useServerPrepStmts=true\
&serverTimezone=Asia/Shanghai\
&zeroDateTimeBehavior=CONVERT_TO_NULL
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver


mybatis.type-aliases-package=com.example.orderservice.mapper
mybatis.mapper-locations=classpath:mapper/*.xml

logging.level.org.mybatis.spring=DEBUG
logging.level.com.example.orderservice.mapper=DEBUG

spring.cloud.nacos.discovery.server-addr=192.168.80.3:8848
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos

seata.tx-service-group=default_tx_group
seata.service.vgroup-mapping.default_tx_group=default
seata.registry.type=file
seata.service.grouplist.default=192.168.80.3:8091

其中重要的seata配置:

1
2
3
4
5
6
# 事务分组名
seata.tx-service-group=default_tx_group
# 事务组对应的集群
seata.service.vgroup-mapping.default_tx_group=default
seata.registry.type=file
seata.service.grouplist.default=192.168.80.3:8091

seata默认是AT模式。

集群时:

1
2
3
4
5
6
7
seata.tx-service-group=default_tx_group
seata.service.vgroup-mapping.default_tx_group=default
seata.registry.type=nacos
seata.registry.nacos.group=DEFAULT_GROUP
seata.registry.nacos.server-addr=192.168.80.3:8848
seata.registry.nacos.username=nacos
seata.registry.nacos.password=nacos

mapper

StorageTblMapper.xml:

1
2
3
4
5
6
<update id="deduct">
update storage_tbl
set
count = count - #{count}
where commodity_code = #{commodityCode}
</update>

StorageTblMapper:

1
int deduct(@Param("commodityCode") String commodityCode, @Param("count") int count);

AccountTblMapper.xml:

1
2
3
4
5
6
<update id="debit">
update account_tbl
set
money = money - #{money}
where user_id = #{userId}
</update>

AccountTblMapper:

1
int debit(@Param("userId") String userId, @Param("money")int money);

OrderTblMapper.xml:

1
2
3
4
5
6
7
<insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.example.orderservice.domain.OrderTbl" useGeneratedKeys="true">
insert into order_tbl
( id,user_id,commodity_code
,count,money)
values (#{id,jdbcType=INTEGER},#{userId,jdbcType=VARCHAR},#{commodityCode,jdbcType=VARCHAR}
,#{count,jdbcType=INTEGER},#{money,jdbcType=INTEGER})
</insert>

OrderTblMapper:

1
int insert(OrderTbl record);

service及contoller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class StorageServiceImpl implements StorageService {

@Autowired
private StorageTblMapper storageTblMapper;


@Override
public void deduct(String commodityCode, int count) {
int i = 1/0;
storageTblMapper.deduct(commodityCode, count);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class StorageController {

@Autowired
private StorageService storageService;

@PostMapping("/deduct")
public Map<String, Object> deduct(@RequestBody Map<String, Object> map) {
Map<String, Object> result = new HashMap<>();
storageService.deduct(String.valueOf(map.get("commodityCode")), (Integer) map.get("count"));
result.put("code", 200);
result.put("msg", "成功");
return result;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountTblMapper accountTblMapper;

@Override
public void debit(String userId, int money) {
accountTblMapper.debit(userId, money);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class AccountController {

@Autowired
private AccountService accountService;

@PostMapping("/debit")
public Map<String, Object> debit(@RequestBody Map<String, Object> map) {
Map<String, Object> result = new HashMap<>();
accountService.debit(String.valueOf(map.get("userId")),
(Integer) map.get("money"));
result.put("code", 200);
result.put("msg", "成功");
return result;
}

}
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
@Service
public class OrderServiceImpl implements OrderService {

@Autowired
private OrderTblMapper orderTblMapper;

@Autowired
private AccountFeign accountFeign;

@Autowired
private StorageFeign storageFeign;

@Override
@GlobalTransactional
public void create(String userId, String commodityCode, int orderCount) {

int orderMoney = 100;

Map<String, Object> map = new HashMap<>();
map.put("userId", userId);
map.put("money", orderMoney);
accountFeign.debit(map);

Map<String, Object> map1 = new HashMap<>();
map1.put("commodityCode", commodityCode);
map1.put("count", orderCount);
storageFeign.deduct(map1);

OrderTbl order = new OrderTbl();
order.setUserId(userId);
order.setCommodityCode(commodityCode);
order.setCount(orderCount);
order.setMoney(orderMoney);

// INSERT INTO orders ...
orderTblMapper.insert(order);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class OrderController {

@Autowired
private OrderService orderService;

@PostMapping("/create")
public Map<String, Object> deduct(@RequestBody Map<String, Object> map) {
Map<String, Object> result = new HashMap<>();
orderService.create(String.valueOf(map.get("userId")),
String.valueOf(map.get("commodityCode")),
(Integer) map.get("count"));
result.put("code", 200);
result.put("msg", "成功");
return result;
}

}

测试

启动 nacos server、seata-server、启动应用。

访问 post请求

http://localhost:7002/order/create

1
2
3
4
5
{
"userId": "1",
"commodityCode": "G0001",
"count": 2
}

查看数据库,account库里数据没有变更,说明分布事布生效。


spring_cloud_alibaba之seata使用
http://hanqichuan.com/2023/10/30/spring_cloud/spring_cloud_alibaba之seata使用/
作者
韩启川
发布于
2023年10月30日
许可协议