分布式事务–Seata
前面了解到一些分布式事务的解决方案,业内也涌现出不少解决分布式事务的优秀框架,如Atomikos、Seata等,本章来了解使用下Seata。
Seata的前身是Fescar,而后改名Seata,简单可扩展的自治分布式事务框架。Seata为用户提供了AT、TCC、SAGA和XA事务模式(默认使用AT
),致力打造的一站式分布式解决方案。
Seata是在传统的2PC方案上进行演进,它把一个分布式事务拆分为若干个分支事务的全局事务,全局事务协调管理若干个分支事务,使其达到一致,实现整个事务那么全部成功,要么全部失败,并且在项目中整合Seata几乎没有侵入性。
基本概念
Seata有几个基本的概念:
- Transaction ID XID:全局唯一事务ID
- TC(Transaction Coordinator):事务协调者,维护全局事务运行,驱动全局事务的提交和回滚
- TM(Transaction Manager):事务管理器,定义全局事务边界,负责开启全局事务,发起全局事务的提交或回滚决议
- RM(Resource Manager):资源管理器,管理分支事务,与TC(事务协调者)通信,决定对分支事务提交或回滚
Seata的下载和安装
Seata像Nacos一样,也有自己的服务端,需要下载服务端程序,地址是:https://github.com/seata/seata/releases
那么如何选择版本呢,还是要按照SpringCloudAlibaba的组件版本对应关系来使用.
笔者使用的alibaba是2021.0.4.0
,seata就使用1.5.2
版本即可,下载速度会挺慢,国外网址可以翻墙或者用迅雷等软件下载都可。
到conf
目录中,有一个application.example.yml
文件,内容是关于seata的注册和配置的示例,可以修改下且改名为application.yml。
- 可以看到配置文件中实现Seata的注册和配置方式有几种:File、Nacos、Eureka、Redis、Consul等等。我们使用Nacos即可。
application.yml
配置如下:
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
# 新增加的console控制台,
# 可通过访问http://localhost:7091进行登录,账号如下seata/seata
console:
user:
username: seata
password: seata
seata:
config:
# support: nacos 、 consul 、 apollo 、 zk 、 etcd3
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ded91f4b-04df-4c19-8006-755505a27c5e
group: SEATA_GROUP
username: nacos
password: nacos
# data-id: seataServer.properties
registry:
# support: nacos 、 eureka 、 redis 、 zk 、 consul 、 etcd3 、 sofa
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: ded91f4b-04df-4c19-8006-755505a27c5e
cluster: default
username: nacos
password: nacos
# seata的安全配置
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
- 到数据库中新增需要的数据表
【Seata 1.5.2版本mysql脚本】导入压缩包目录seata/script/db/mysql.sql
数据表创建完毕
- 启动nacos,创建一个dev的命名空间方便测试
- 修改压缩包目录seata/script/config-center/config.txt文件中几处内容:
# 存储模式
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
# 需要根据mysql的版本调整driverClassName
# mysql8及以上版本对应的driver:com.mysql.cj.jdbc.Driver
# mysql8以下版本的driver:com.mysql.jdbc.Driver
store.db.driverClassName=com.mysql.jdbc.Driver
# 注意根据生产实际情况调整参数host和port
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
# 数据库用户名密码
store.db.user=root
store.db.password=123456
# 微服务里配置与这里一致
service.vgroupMapping.dev_tx_group=default
TIPS📢:
配置事务分组service.vgroupMapping.dev_tx_group=default
dev_tx_group:需要与客户端保持一致 ,可以自定义
default:需要跟客户端和application.yml中的cluster保持一致
default 必须要等于 registry.conf cluster = “default”
- 官方推荐通过压缩包目录seatascript/config-center/nacos/nacos-config.sh将修改后的config.txt发布到nacos上
# 运行指令 ,通过 Git Bash Here
sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t ded91f4b-04df-4c19-8006-755505a27c5e
# 具体说明参见:http://seata.io/zh-cn/docs/user/configurations.html
# -h: nacos host,默认localhost
# -p: nacos端口,默认8848
# -g: nacos分组,默认'SEATA_GROUP'.
# -t: 租户信息Tenant information,对应nacos namespace ID,默认''
# -u: nacos用户名,默认''
# -w: nacos用户密码,默认''
可以看到,配置自动导入到了nacos中
到seata的bin目录下执行seata-server.sh或bat
即可执行服务端。
客户端
那么还是按照上篇文章中的订单服务
和配送服务
来实现。
- 项目中导入依赖
父工程
<properties>
<java.version>8</java.version>
<boot.version>2.6.11</boot.version>
<cloud.version>2021.0.4</cloud.version>
<cloud.alibaba.version>2021.0.4.0</cloud.alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!--springboot依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--cloud的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--cloud.alibaba依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 子工程(订单服务和配送服务都要)
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--seata starter -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</dependency>
- 为
订单数据库
和配送数据库
增加undo_log表
,脚本地址
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;
注意📢:每个业务数据库都要有UNDO_LOG
- 订单服务和配送服务的yml文件主要引入的配置
- seata.enabled:是否开启自动装配seata
- seata.application-id:应用id
- seata.tx-service-group:事务分组
- seata.enable-auto-datasouce-proxy:数据源自动代理
spring:
cloud:
nacos:
discovery:
group: SEATA_GROUP
server-addr: http://localhost:8848
# 必须填命名空间的ID
namespace: ded91f4b-04df-4c19-8006-755505a27c5e
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///order?useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
distribution:
name: distribution
# Seata 配置
seata:
application-id: order-server # 自定义
# 是否启用数据源bean的自动代理
enable-auto-data-source-proxy: false
tx-service-group: dev_tx_group # 必须和服务器配置一样
registry:
type: nacos
nacos:
# Nacos 服务地址
server-addr: http://localhost:8848
group: SEATA_GROUP
namespace: ded91f4b-04df-4c19-8006-755505a27c5e
application: seata-server # 必须和服务器配置一样
username: nacos
password: nacos
cluster: default
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
group: SEATA_GROUP
namespace: ded91f4b-04df-4c19-8006-755505a27c5e
service:
vgroup-mapping:
tx-service-group: dev_tx_group # 必须和服务器配置一样
disable-global-transaction: false
client:
rm:
# 是否上报成功状态
report-success-enable: true
# 重试次数
report-retry-count: 5
feign:
client:
config:
default:
connectTimeout: 2000 # 建立连接超时时间
readTimeout: 2000 # 读取资源超时时间
- 配置代理数据源
@Primary
@Bean
public DataSource dataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql:///distribution?useUnicode=true&characterEncoding=utf-8");
druidDataSource.setUsername("root");
druidDataSource.setPassword("123456");
DataSourceProxy dsp = new DataSourceProxy(druidDataSource);
return dsp;
}
- 在事务开始的地方使用注解
@GlobalTransactional
@Service
public class OrderServiceImpl implements OrderService {
@Resource
JdbcTemplate jdbcTemplate; // 操作数据库
@Resource
DistributionService distributionService; // 远程调用
// 模拟菜品数据
private final Map<Integer, String> shopMap = new HashMap<Integer, String>(){{
put(1,"菜品1");
put(2,"菜品2");
put(3,"菜品3");
}};
@Override
@Transactional // 加上事务
@GlobalTransactional // seata全局事务
public Integer createOrder(Integer id) {
if (shopMap.containsKey(id)) {
String orderId = UUID.randomUUID().toString().replace("-","");
// 增加订单
int update = jdbcTemplate.update("insert into t_order(order_id, shop_id) values(?,?)",
new Object[]{orderId, id});
// 调用配送服务
Integer result = distributionService.distribution(orderId);
if (result <= 0) {
throw new RuntimeException("分配配送员失败");
}
return update;
}
return null;
}
}
踩坑
-
前面在seata的yml中可以看到使用的是
druid
的连接池,mybatisPlus默认集成了druid和hikari两种连接池,而mybatis不是,因此需要在使用mybatis的项目中,另外集成druid,在application.yml中声明datasource.type为"com.alibaba.druid.pool.DruidDataSource" -
seata服务端的配置尽量和客户端做到一致,如driver_class_name:“com.mysql.cj.jdbc.Driver”
测试
curl localhost:9002/createOrder?id=1
{"timestamp":"2023-09-16T04:59:01.614+00:00","status":500,"error":"Internal Server Error","path":"/createOrder"}
可以看到返回报错,同样去看下两个服务的日志情况:订单服务
调用配送服务
可看到,订单服务在调用配送服务5s后,直接超时并且开始回滚数据。
配送服务日志:
由于本案例是一个超时情况,所以说这里报错是正常的,因为是配送服务还没执行完毕,订单服务就已经去回滚数据了,配送服务执行完毕后收到回滚的信号,也去进行回滚,发现这条xid的数据已经被订单服务回滚过了,报错没找到此XID的数据。