引入Seata的一些问题和解决方法

1,281 阅读5分钟

本文简单记录了一下在项目中引入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遇到的全部问题,后续如果遇到其他问题,会再在本文进行补充。