有道无术,术尚可求,有术无道,止于术。
本系列Seata 版本 2.0.0
本系列Spring Boot 版本 3.2.0
本系列Spring Cloud 版本 2023.0.0
源码地址:https://gitee.com/pearl-organization/study-seata-demo
1. 概述
在之前的案例中,我们在Spring Cloud
微服务场景下使用Seata
解决了分布式事务问题,在单体架构中,单个服务也可能存在跨库导致的分布式事务问题,例如下图中,因为分库导致下单请求需要连接多个数据库进行操作:
2. 多数据源环境搭建
这里使用MyBatis-Plus
开发团队提供的多数据源框架dynamic-datasource
,相关简介可参考官网。
2.1 数据库
使用的是Mysql 8.0.29
,首先创建三个数据库:
seata_account
:账户seata_order
:订单seata_stock
:库存
seata_account
库插入t_account
表:
CREATE TABLE `t_account` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`amount` double(14,2) DEFAULT '0.00',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO t_account
(user_id, amount)
VALUES(1, 10000.00);
seata_order
库插入t_order
表:
CREATE TABLE `t_order` (
`id` int NOT NULL AUTO_INCREMENT,
`order_no` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`user_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`commodity_code` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`count` int DEFAULT '0',
`amount` double(14,2) DEFAULT '0.00',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=64 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
seata_stock
库插入t_stock
表:
CREATE TABLE `t_stock` (
`id` int NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`count` int DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO t_stock
(commodity_code, name, count)
VALUES('IPHONE', '苹果手机', 10000);
2.2 项目搭建
注意:这里省略了一些简单代码,完整代码请参考案例源码
项目技术栈:
JDK 17
Maven 3.6.3
Seata 2.0
Mybatis Plus 3.5.5
Dynamic Datasource 4.3.0
Spring Boot 3.2.0
使用Spring Initializr
创建一个Spring Boot
服务项目,引入相关依赖:
<dependencies>
<!--Spring Boot Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--工具-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.21</version>
</dependency>
<!--Mybatis Plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/dynamic-datasource-spring-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
添加多数据源配置:
server:
port: 9000
spring:
application:
name: dynamic-datasource-seata-demo
datasource:
dynamic:
hikari:
minimum-idle: 5
idle-timeout: 30000
maximum-pool-size: 20
max-lifetime: 1800000
connection-timeout: 50000
primary: d_account
datasource:
d_account:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata_account?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true
username: root
password: 123456
d_order:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata_order?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true
username: root
password: 123456
d_stock:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata_stock?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true
username: root
password: 123456
各个Mapper
接口添加DS
注解:
@DS("d_account")
@DS("d_order")
@DS("d_stock")
BusinessService
服务添加下单功能:
public Object handleBusiness() {
// 1. 业务请求数据
BusinessDTO businessDTO =new BusinessDTO();
businessDTO.setUserId("1"); // 下单用户
businessDTO.setCount(1); // 数量
businessDTO.setCommodityCode("IPHONE"); // 商品编号
businessDTO.setAmount(new BigDecimal(1)); // 订单金额
log.info("业务请求数据:"+ businessDTO);
// 2. 扣减库存
CommodityDTO commodityDTO = new CommodityDTO();
commodityDTO.setCommodityCode(businessDTO.getCommodityCode()); // 商品编号
commodityDTO.setCount(businessDTO.getCount()); // 数量
Object stock = stockService.decreaseStock(commodityDTO);
log.info("调用库存服务:"+ stock);
// 3. 创建订单
OrderDTO orderDTO = new OrderDTO();
orderDTO.setUserId(businessDTO.getUserId());
orderDTO.setCommodityCode(businessDTO.getCommodityCode());
orderDTO.setOrderCount(businessDTO.getCount());
orderDTO.setOrderAmount(businessDTO.getAmount());
OrderDTO response = orderService.createOrder(orderDTO);
log.info("调用订单服务:"+ response);
return orderDTO;
}
3. Seata 集成
3.1 undo_log 表
在AT
模式中,需要在参与全局事务的数据库中添加undo_log
表:
-- seata_account.undo_log definition
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`branch_id` bigint NOT NULL COMMENT '分支事务ID',
`xid` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8_general_ci NOT NULL COMMENT '全局事务唯一标识',
`context` varchar(128) CHARACTER SET utf8mb3 COLLATE utf8_general_ci NOT NULL COMMENT '上下文',
`rollback_info` longblob NOT NULL COMMENT '回滚信息',
`log_status` int NOT NULL COMMENT '状态,0正常,1全局已完成(防悬挂)',
`log_created` datetime NOT NULL COMMENT '创建时间',
`log_modified` datetime NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='AT模式回滚日志表';
在seata_account
、seata_order
、seata_stock
库中,都新建undo_log
表。
3.2 引入依赖
这里演示的是单机模式,所以不需要注册中心、配置中心,引入seata
提供的Spring Boot
启动包:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
3.3 配置
application.yml
中配置使用file
作为注册中心、配置中心:
seata:
# 配置中心
config:
type: file
file:
name: file.conf
# 注册中心
registry:
type: file
多数据源配置开启Seata
,并设置事务模式:
datasource:
dynamic:
seata: true # 启用Seata
seata-mode: at # 事务模式,支持AT、XA
hikari:
minimum-idle: 5
在resources
目录下添加file.conf
配置文件,内容如下:
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = true
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#transaction service group mapping
vgroupMapping.default_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
file.conf
中需要注意配置TC
地址:
3.4 添加注解
添加分布式事务注解@GlobalTransactional
:
@GlobalTransactional
public Object handleBusiness() {//......}
3.5 测试
启动Seata
、后台服务,查看日志,可以看到成功连接TC
,并在AT
模式下自动代理了数据源:
当发生业务异常时,可以看到多个RM
都进行了回滚操作,查看数据库也发现数据一致,说明集成成功: