本文简单记录了一下在项目中引入Seata时出现的一些问题和解决方法。希望可以给遇到同样问题的小伙伴提供一点帮助或思路。
1.数据库无法连接
Seata启动时报数据库too many connect错误,或启动后突然有服务无法连接数据库了。可能是因为启动seata后占用太多mysql的连接数导致的。
在mysql中执行show variables like '%max_connection%'可以查看当前mysql支持的最大连接数。执行show processlist可以查看当前mysql正在使用的连接情况。
seata使用的连接数在seata的配置文件file.conf中可以进行配置,当上限配置过高时,seata会在mysql中注入很多的Sleep空闲连接,在超过mysql支持的最大连接数后,就会导致mysql拒绝外部连接。
出现该问题时可以检查一下该配置是否合理,并适当调整seata或mysql的连接数配置。
2.RM调用时分布式事务失效
在TM服务fegin调用RM服务时,在RM服务抛出异常,无法使TM回滚。
举个例子:我有一个order服务(TM),本地在order库保存订单,然后fegin调用storage服务(RM),在storage库扣库存。当我在storage服务中抛出异常时,order库写入的内容却不会回滚。
情况一:XID未传递
seata在创建全局事务时会生成一个全局唯一的XID,分支事务只有持有了该XID,才会被识别为同一事务并保持提交、回滚同步,否则分支事务的状态无法被seata感知,对于seata来说,等于没有这个分支事务。
在排查问题的过程中,出现了order服务写入了undo_log,而storage服务未写入undo_log的情况,说明storage的分支事务未被纳入seata管理;然后进一步调用RootContext.getXID()方法打印了下当前RM的XID,发现为null,从而定位了问题。
正常情况下,事务XID是保存在请求头中进行传递的。如果我们发现XID并未正常传递,快速解决方法是通过主动传参的形式来保证服务调用XID的全局一致。
先通过在order服务(TM)中调用RootContext.getXID()方法获取XID,然后显式通过@RequestParam(value = "XID") String XID的方式将XID作为参数传递到被调用的服务中,再通过RootContext.bind(XID);方法,将该XID绑定到该分支事务上,以保证全局事务的正常进行。
后续发现原来是maven依赖有问题,导致seata内置的拦截器没有生效,所以没有向header写入XID,在调整maven后问题解决。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<!--加入spring-cloud-alibaba-seata,解决xid不传递问题-->
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.2.0.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
情况二:异常未被感知
出现order服务、storage服务都写入了undo_log,但是最终事务并未回滚,且数据正常提交了的情况,说明seata事务是正常处理了的,但抛出的异常并未被seata感知到。
一般项目中都会做全局的异常处理,以防止后台报的错误直接返回到用户界面,而当storage服务被调用时抛出的异常被全局异常处理捕获,返回一个自定义的类时,Seata就认为这个异常已经被处理了,而无法感知到这个异常,导致全局事务无法回滚。
解决方法一:
可以在调用storage服务的地方,校验服务调用的返回值,手动判断是否出现了异常,如果出现异常,就再次手动抛出,这时就能被seata识别到,并对全局事务进行回滚。
result = fegin调用服务方法;
if (result.getCode().equals('500')) {
throw new RuntimeException("系统繁忙,请稍后再试!");
}
解决方法二:
可以在全局异常处理的地方、或RM服务处try catch,调用下述方法来手动回滚全局异常:
if (StringUtils.isNotBlank(RootContext.getXID())) {
GlobalTransactionContext.reload(RootContext.getXID()).rollback();
}
3.多数据源切换时分布式事务回滚失效
在集成dynamic多数据源(master库为user库,slave库为order库)的情况下,出现order写入订单数据时,在order库写入undo_log,然而在回滚的的时候,order库的undo_log不仅没删除,还在user库里写入了一条branch_id相同的undo_log。
经过打点追踪源码发现,在seata回滚查询undo_log时获取的数据源是默认获取的master数据源,而不是我们打上@DS("slave")期望的slave数据源,在本项目中是因为dynamic的版本过低,导致dynamic的数据源代理与seata的数据源代理起冲突,在seata获取数据源时无法识别获取dynamic的注解声明导致的。
最终的解决方法是升级dynamic的依赖版本,提高dynamic和seata的兼容性,并停用seata自己的数据源代理。
引入依赖(这里将dynamic的依赖版本从2.5.1提升到3.4.1)
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
在yml中配置dynamic启用seata
spring:
datasource:
dynamic:
# 启用 seata
seata: true
# 模式是 at 模式
seata-mode: at
并在yml中配置停用seata自己的数据源代理
seata:
enable-auto-data-source-proxy: false
然后在业务流程中使用@DS来切换数据源,且注意要避免使用@Transaction来回滚本地事务,因为事务的提交、回滚会由Seata来全局管理执行,如果再在本地@Transaction回滚事务,可能会争抢资源、产生冲突,反而导致回滚失败。
至此就是本次项目过程中继承Seata遇到的全部问题,后续如果遇到其他问题,会再在本文进行补充。