java进阶:seata分布式事务未生效问题排查纪实|主事务回滚成功,分支事务未回滚
0.引言
近期在做公司老项目和新系统的模块打通时出现了一个分布式事务不生效的问题,主要表现为主事务中出现报错,主事务回滚成功了,但是分支事务未回滚。特此记录,以供后续参考。
1. 背景
首先声明一下我的业务背景,项目是基于dubbo+zookeeper进行服务间通信的,seata版本为1.5.2,且采用的是file形式作为注册和配置中心。
业务流程为主服务A调用服务B做数据更新,B更新成功后服务A再执行剩下的更新操作。其调用伪代码如下:
// 被调用服务B
@DubboReference
private CdmService cdmService;
// 主服务A
@Resource
private ApiMapper apiMapper;
@GlobalTransactional(timeoutMills = 30000)
public boolean syncToCdm(T data) {
// 调用服务B cdmService
Result result = cdmService.insertApi(data);
if(result.isSuccess()){
// 主服务A 进行数据更新
apiMapper.update(result);
}
}
被调用服务B中,注意这里没有加@Transactional
,这里加不加都可以,不影响全局事务执行
(这里我后面验证,添加了@Transactional的,该服务的undo_log没有数据,但是全局事务可以正常回滚执行)
public BaseResult<String> insertApi(T data) {
...
}
2. 排查步骤
1、首先检查配置,初步怀疑是seata server端配置问题,查看seata服务端配置
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: file
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: file
store:
# support: file 、 db 、 redis
mode: file
可以看到是采用了file作为注册中心和配置中心,没有连接其他的注册中心组件,那么再看一下客户端配置。在各个使用seata的服务中看到配置了file.conf和registry.conf文件。并且通过在启动参数中添加-Dseata-server-address=seata服务端ip:8091
来指定seata地址,并且引入了seata依赖。
同时配置文件中也设置了seata.enableAutoDataSourceProxy=false
,来取消datasource自动代理
另外再检查下各个服务之间的tx-service-group
设置的是否一致,发现也都一样
如此看起来seata配置都没有什么问题。
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>${seata.version}</version>
</dependency>
2、这里因为B服务是我新增的,不太想用原来spring的这种配置方式,感觉太繁琐,所以我额外调整了配置,改为spring-boot-starter形式
引入依赖
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
调整配置项
seata:
enabled: true
# 你的服务名,这里用了dubbo服务名
applicationId: ${dubbo.application.name}
data-source-proxy-mode: AT
registry:
type: file
tx-service-group: my_test_tx_group
enable-auto-data-source-proxy: false
service:
vgroup-mapping:
my_test_tx_group: default
grouplist:
default: seata-server:18091
3、这里我首先在被调用方(服务B)和调用方(服务A)都通过RootContext.getXID()
打印了全局事务Id,发现全局事务ID都是一致的,说明事务是进来了的。
这里查阅了另外一篇文章中说明了如果全局事务ID不一致时的一种排查情况,大家可以参考
Seata做分布式事务时,报错后事务不回滚的问题
如果调用链比较复杂,全局ID丢失的,需要手动绑定全局事务id,可以通过RootContext.bind(xid);
实现
4、事务id一致,那么就是继续看回滚日志数据是否有正常添加,断点查看调用方(服务A)undo_log数据情况,发现调用方正常新增
于是继续查看被调用方undo_log,发现被调用方忘记创建undo_log表了,但奇怪的是并没有报错(这里提醒,所有参与seata全局事务的业务所涉及的数据库中都要创建undo_log表,表结构参考如下)
create table undo_log
(
id bigint auto_increment
primary key,
branch_id bigint not null,
xid varchar(100) not null,
context varchar(128) not null,
rollback_info longblob not null,
log_status int not null,
log_created datetime not null,
log_modified datetime not null,
ext varchar(100) null,
constraint ux_undo_log
unique (xid, branch_id)
)
charset = utf8;
5、于是怀疑是B服务完全没有执行到全局事务回滚日志新增的方法,seata server端控制undo_log新增,肯定是根据各服务的数据库驱动类来实现的,于是考虑被调用方是mysql8.0, 调用方是mysql5.7,检查是否是驱动类声明的问题,一定要求你的驱动器与数据库版本保持一致,比如mysql8.0驱动要求com.mysql.cj.jdbc.Driver
,另外要检查所涉及数据库是否支持事务性操作,如果数据库本身不支持,那么自然不成功。
6、这里检查发现调用方服务是通过多源数据库组件dynamic-datasource
控制的,于是怀疑是不是这个组件本身和seata有什么冲突
查看官方说明,在3.x
后支持seata, 如果版本低的可以升级版本,但目前使用的应该都是3.x版本以上
于是继续查看其配置seata的配置项
7、最终在application.yml中的dynamic-datasource部分开启seata,并指定其模式为AT
spring.datasource.dynamic.seata=true
spring.datasource.dynamic.seata-AT
spring:
# 数据库配置
datasource:
# 指定使用 Druid 数据源
type: com.alibaba.druid.pool.DruidDataSource
# driverClassName: com.mysql.cj.jdbc.Driver # 注释掉默认驱动
dynamic:
seata: true
seata-mode: AT
primary: master # 主数据源
strict: false # 严格模式,是否在启动时检查数据源是否可用
datasource:
# 主库数据源
master:
url: jdbc:mysql://xxx/xxx
username: xxx
password: xxx
seata:
enabled: true
applicationId: xxx
data-source-proxy-mode: AT
registry:
type: file
tx-service-group: my_test_tx_group
enable-auto-data-source-proxy: false
service:
vgroup-mapping:
my_test_tx_group: default
grouplist:
default: seata-server:18091
8、最终重启项目,继续断点执行操作,发现服务B的undo_log可以正常添加日志了,事务也回滚了,问题解决!
4. 原因汇总
1、检查seata-server, seata-client配置是否正确,是否在同一个group中
2、检查数据库驱动声明是否符合版本,数据库本身是否支持事务,所有涉及数据库中是否都有创建undo_log表
3、如有使用dynamic-datasource,检查是否其配置是否开启seata
4、seata事务模式是否是AT,其他模式本文未验证,可能有未知错误问题。