SpringCloud整合Seata1.4.0

版本说明

本例于windows基于Nacos配置注册、MybatisPlus、Hikari数据源,数据库为Mysql,示例代码为Seata的AT模式。案例代码GitHub地址spring-cloud-demo

具体版本:

  • seata 1.4.0

  • SpringBoot 2.3.6.RELEASE

  • SpringCloud Hoxton.SR9

  • SpringCloudAlibaba 2.2.3.RELEASE

  • MybatisPlus 3.3.2

下载资源

Seata Release下载目前最新版的1.4.0发行包和源码

Seata配置

Server

1、解压seata-server-1.4.0文件,进入/seata/conf,将register.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
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"

nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = "56c94dbe-7fbe-49c2-b456-170001455569"
cluster = "default"
username = "nacos"
password = "nacos"
}

}

config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"

nacos {
serverAddr = "127.0.0.1:8848"
namespace = "56c94dbe-7fbe-49c2-b456-170001455569"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}

此处为设置seata使用nacos进行注册与配置

registry.nacos.namespace为nacos中的命名空间,可以自行创建命名空间后,替换此处的namespace

进入/bin,打开cmd,运行

1
seata-server.bat -p 9000 -m file

运行成功后,将会在nacos服务列表中显示

2、数据库建seata库即表

数据库新建yuli-seata数据库,用来存放seata全局数据

进入script/server/db,在yuli-seata库中运行mysql.sql文件,最终得到三张表

3、初始化配置文件同步到nacos中,请先启动nacos,再执行此操作

解压seata的source code,进入script/config-center,修改config.txt配置文件

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
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
service.vgroupMapping.spring-cloud-demo=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=db
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/yuli-seata?useUnicode=true
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

修改项

  • service.vgroupMapping.spring-cloud-demo=default

    spring-cloud-demo为自定义的事务组

  • store.mode=db

    状态由数据库管理

    • store.db.url=jdbc:mysql://127.0.0.1:3306/yuli-seata?useUnicode=true

      数据库连接地址

    • store.db.user=root

      数据库用户名

    • store.db.password=root
      数据库密码

windows环境下可以通过python环境执行py脚本,也可通过git环境执行sh脚本,本文使用git bash

进入解压后的源码包中/script/config-center/nacos

打开git bash窗口,执行

1
sh nacos-config.sh -h localhost -p 8848 -t 56c94dbe-7fbe-49c2-b456-170001455569 -u nacos -w nacos
  • -h

    nacos’s host

  • p

    nacos’s 端口

  • -t

    nacos namespace命名空间id

  • -u

    nacos用户名

  • -w

    nacos密码

执行成功:

在nacos中查看配置

可以发现配置散落一地,别诧异,这是正常的。但是希望后期会有所优化吧,毕竟太散乱

Client

引入依赖,注意本例父pom中引入了spring-cloud-alibaba-dependencies,且定义了版本号,所以此处未手动定义版本号。需要查看完整代码的请移步文章头部提到的代码地址。

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

在项目的bootstrap.yml文件中增加如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: spring-cloud-demo #此处配置自定义的seata事务分组名称
enable-auto-data-source-proxy: true #开启数据库代理
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
group: SEATA_GROUP
registry:
type: nacos
nacos:
application: seata-server
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}

${}为获取本文件中定义变量

${spring.cloud.nacos.discovery.server-addr},在本例中已经有设置为localhost:8848

注意seata.tx-service-group和上文config.txt文件中service.vgroupMapping.spring-cloud-demo=default有对应关系,

关系为spring-cloud-demo

配置代理数据源

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
package cn.javayuli.mybatis.config;

import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

/**
* 对分布式事务seata配置的数据源代理
*
* @author hanguilin
*/
@Configuration
@ConditionalOnClass(HikariDataSource.class)
public class DataSourceProxyConfig {

/**
* 原生datasource前缀取"spring.datasource"
*
* @return
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource hikariDataSource() {
HikariDataSource hikariDataSource = new HikariDataSource();
return hikariDataSource;
}

/**
* 构造datasource代理对象,替换原来的datasource
*
* @param hikariDataSource
* @return
*/
@Primary
@Bean("dataSource")
public DataSourceProxy dataSourceProxy(DataSource hikariDataSource) {
return new DataSourceProxy(hikariDataSource);
}
}

启动类排除DataSourceAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package cn.javayuli;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.SpringCloudApplication;

/**
* 订单服务
*
* @author hanguilin
*/
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@SpringCloudApplication
public class OrderApplication {

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

DEMO

本例举很经典的例子:下订单时需要生成订单信息和扣减商品库存。

结构图

调用关系图:

项目结构图:

  • api-common

    公共调用,其中api-common-mybatis包含mybatis、seata、mysql-connect-java依赖,以及seata代理数据源配置,需要连接数据库的服务直接在pom中引入其项目即可

  • api-gateway

    基于springcloud gateway的网关服务

  • api-provider

    业务服务,由其发起远程调用,调用订单、库存服务

  • api-provider-order

    订单服务,可以创建订单信息

  • api-provider-stock

    库存服务,可以扣减商品数量

库表

项目库表

yuli-order库中tb_order表为订单信息表

yuli-stock库中tb_stock表为库存信息表

undo_log为seata事务中重要的表,在每个应用中都需要创建一张,如果是单库,就用一张undo_log表即可

undo_log表建表语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE `undo_log` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) NOT NULL COLLATE 'utf8_general_ci',
`context` VARCHAR(128) NOT NULL COLLATE 'utf8_general_ci',
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT(11) NOT NULL,
`log_created` DATETIME NOT NULL,
`log_modified` DATETIME NOT NULL,
`ext` VARCHAR(100) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `ux_undo_log` (`xid`, `branch_id`) USING BTREE
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=56;

tb_order

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `tb_order` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`no` VARCHAR(64) NULL DEFAULT NULL COMMENT '订单编号' COLLATE 'utf8_general_ci',
`remark` VARCHAR(255) NULL DEFAULT NULL COMMENT '备注' COLLATE 'utf8_general_ci',
`create_time` DATETIME NULL DEFAULT NULL COMMENT '创建时间',
`update_time` DATETIME NULL DEFAULT NULL COMMENT '修改时间',
`del_flag` CHAR(1) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
PRIMARY KEY (`id`) USING BTREE
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=39;

tb_stock

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE `tb_stock` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`goods` VARCHAR(255) NULL DEFAULT NULL COMMENT '物资名称' COLLATE 'utf8_general_ci',
`number` INT(10) NULL DEFAULT NULL COMMENT '库存数量',
`remark` VARCHAR(255) NULL DEFAULT NULL COMMENT '备注' COLLATE 'utf8_general_ci',
`create_time` DATETIME NULL DEFAULT NULL COMMENT '创建时间',
`update_time` DATETIME NULL DEFAULT NULL COMMENT '更新时间',
`del_flag` CHAR(1) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
PRIMARY KEY (`id`) USING BTREE
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=2;

接口

订单服务暴露接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 创建订单
*
* @param no 订单编号
* @return
*/
@PostMapping("/order/save")
public String doSaveOrder (@RequestParam("no") String no) {
Order order = new Order();
order.setNo(no);
// 保存订单
orderService.save(order);
return "success";
}

库存服务暴露接口

根据物资现有数量减去出售数量,如果库存不够就会抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 扣减库存
*
* @param goods 物资
* @param number 扣除量
* @return
*/
@GetMapping("/deduct")
public String doDeductionStock(@RequestParam("goods") String goods, @RequestParam("number") int number) {
Stock stock = orderService.getOne(Wrappers.lambdaQuery(Stock.class).eq(Stock::getGoods, goods));
Integer stockNumber = stock.getNumber();
Integer left = stockNumber - number;
if (stockNumber == 0 || left < 0) {
throw new RuntimeException("商品数量不足");
}
stock.setNumber(left);
orderService.updateById(stock);
return "success";
}

业务服务feign接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 远程调用库存服务
*
* @author hanguilin
*/
@FeignClient(contextId = "remoteOrderService", value = "api-provider-order",
fallbackFactory = RemoteOrderServiceFallbackFactory.class)
public interface RemoteOrderService {

/**
* 创建订单
*
* @param no 订单编号
* @return
*/
@PostMapping("/order/save")
String doSaveOrder (@RequestParam("no") String no);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 远程调用库存服务
*
* @author hanguilin
*/
@FeignClient(contextId = "remoteStockService", value = "api-provider-stock",
fallbackFactory = RemoteStockServiceFallbackFactory.class)
public interface RemoteStockService {

/**
* 扣减库存
*
* @param goods 物资
* @param number 扣除量
* @return
*/
@GetMapping("/stock/deduct")
String doDeductionStock(@RequestParam("goods") String goods, @RequestParam("number") int number);
}

业务服务暴露接口

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 下订单
*
* @param no 订单编号
* @param goods 物资
* @return
*/
@GlobalTransactional
@PostMapping("/business/create/order")
public String doCreateOrder (@RequestParam("no") String no, @RequestParam("goods") String goods) {
return businessService.createOrder(no, goods);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 下订单
*
* @param no 订单编号
* @param goods 物资
* @return
*/
@Override
public String createOrder(String no, String goods) {
remoteOrderService.doSaveOrder(no);
remoteStockService.doDeductionStock(goods, 1);
return "success";
}

@GlobalTransactional为seata的注解,表示开启全局事务

网关配置

nacos中api-gateway-dev.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
server:
port: 8001
spring:
cloud:
gateway:
routes:
- id: api-provider
uri: lb://api-provider
predicates:
- Path=/provider/**
filters:
- RewritePath=/provider/(?<segment>.*), /$\{segment}
- id: api-provider-order
uri: lb://api-provider-order
predicates:
- Path=/provider-order/**
filters:
- RewritePath=/provider-order/(?<segment>.*), /$\{segment}
- id: api-provider-stock
uri: lb://api-provider-stock
predicates:
- Path=/provider-stock/**
filters:
- RewritePath=/provider-stock/(?<segment>.*), /$\{segment}

场景测试

库存充足情况下会正常插入一条订单信息和将商品库存数减1

启动nacos->seata->(api-gateway、api-provider、api-provider-order、api-provider-stock)

端口说明

api-gateway:8001

api-provider:8000

api-provider-order:8002

api-provider-stock:8003

发送请求,路径http://localhost:8001/provider/business/create/order,走网关请求

订单成功插入一条数据

库存成功扣减(1->0)

此时将订单号修该一下,再请求,此时库存服务就会抛出异常

订单信息未插入

可看到订单服务控制台显示分支事务回滚,二阶段回滚,说明全局事务在其中起到了相应作用

查看评论