Seata整合Sharding-JDBC后分支事务回滚失败原因分析(高质量)

大家好,我是爱读源码的大都督,最近在做seata的升级改造,由于项目中既用到了seata,也用了sharding-jdbc,并且之前同事对他们进行改造,为了知道改造的原因,我做了一些测试,发现seata和sharding-jdbc同时使用时确实会出现问题,所以写这篇文章来记录一下,并分享给有需要的朋友。

如果时间不够,并且对seata和sharding-jdbc的原理有一定了解的话,可以直接看最后的总结

问题描述

在使用sharding-jdbc时,如果配置了主从,就算主库和从库是同一个库(开发的时候基本就是这个场景,实际如果主库和从库是不同的库也会有问题,后面会分析),在使用seata时,当全局事务需要回滚时,分支事务却回滚不了 ,此时将看到如下日志: 中间的空指针异常是我自己的Service中抛的,并不是seata抛的,我的Service为:

从日志可以发现,确实触发了分支事务回滚,但是最终确回滚失败了,排查这个问题我是花了点时间的,最终从Mysql侧找到了突破点。

分析问题

在Mysql中利用show processlist可以查看Mysql中有哪些进程在运行,看下图,发现其中一个delete语句执行挺久了,在我的业务逻辑中的是往my_entity表中insert 数据,而这里是delete ,正好就是回滚逻辑,再次发现分支事务确实在回滚,而且已经在执行delete语句了,关键问题就在于delete语句为什么卡住了。

show processlist的结果中没有锁相关的信息,我们可以执行select * from sys.innodb_lock_waits直接查看锁相关的信息: 从这个结果,可以得到非常重要的信息:

  1. locked_type:表示是行锁
  2. waiting_trx_id,waiting_pid:表示正在等待锁的事务和进程
  3. waiting_query:表示哪个SQL在等待锁
  4. waiting_trx_id,waiting_pid:表示正在占用当前这把锁的事务和进程

所以,从这个结果可以知道,delete语句执行时在等待锁,而该锁被进程244占用了,那么进程244中是在执行什么SQL语句从而占用了锁呢?

我们可以执行select * from performance_schema.threads where PROCESSLIST_ID = '244',244就是blocking_pid,也就是占用了锁的进程,执行这个SQL就能得到这个进程中有哪些线程:

取第一个字段THREAD_ID,继续select * from performance_schema.events_statements_history where THREAD_ID = '284'得到下图结果 从这个结果发现,进程244执行了my_entity的select...for update语句,而就是这个语句加了锁并且事务一直没有提交,从而锁一直没有释放,从而导致分支事务在回滚时,想要delete时一直获取不到锁,从而导致分支事务一直回滚失败。

但这是表面原因,业务逻辑中是insert,回滚逻辑是delete,但为什么要查my_entity呢,而且为什么查询还要加for update呢,而且为什么锁一直不释放呢?这就需要来分析seata的分支事务回滚的原理了。

分支事务回滚原理

大概熟悉seata的同学都知道,分支事务回滚时,需要查询undo_log得到beforeImage和afterImage,回滚其实就是把afterImage替换回beforeImage,但是在替换之前,seata需要判断,在分支事务提交后,全局事务回滚时,中间这段时间内有没有其他事务修改了数据,如果分支事务回滚时,发现待回滚的数据发生了变化,则不能进行分支事务的回滚,不然就会覆盖掉最新的数据,举个例子。

如果业务逻辑是:a原本是1,业务逻辑把a改成了2,现在分支事务要回滚,seata预期是把a从2改回1,但是在改之前发现a=3了,也就是中间被其他事务改了,此时seata是不会回滚的,因为一旦回滚就把3改成了1,是有问题的。

所以seata在回滚时,需要查询出my_entity中当前的数据(3)和afterImage(2)进行比较,从而检查数据是否发生了改变,并且要保证查出来my_entity数据之后,该数据不会被修改,所以在查询的时候就加了for update,把数据查出来之后就和afterImage进行比较,如果发现数据没有发生改变,就在当前事务中继续执行afterImage的逻辑,对应我的业务逻辑就是delete掉my_entity中的数据,最后事务提交(不熟悉seata的可能这里有点晕,怎么分支事务回滚的时候还提交?因为这里说的分支事务回滚,就是执行反向SQL,所以最终是要提交的),事务提交后就会释放掉锁了。

以上是正常的分支事务回滚逻辑,也是重点,当使用sharding-jdbc的主从时之所以会出现问题,也在上面这个流程中,我们继续分析。

问题的根因

不管是seata还是sharding-jdbc,它们都有自定义的DataSource,比如seata中的DataSourceProxy,sharding-jdbc中的MasterSlaveDataSource,它们如果分别使用,那么它们都会分别代理更底层的DataSource,比如DruidDataSource,但是如果seata和sharding-jdbc同时使用,则会是seata中的DataSourceProxy代理sharding-jdbc中的MasterSlaveDataSource,sharding-jdbc中的MasterSlaveDataSource代理DruidDataSource。 为什么是这种结构,以后再单独写文章来分析seata的工作原理吧,这边文章就先不展开了,大家可以关注我的公众号:Hoeller。

以下是重点中的重点。

就是因为是这种结构,导致seata的DataSourceProxy创建出来的连接对象是MasterSlaveConnection(MasterSlaveDataSource创建的),而MasterSlaveConnection最终会利用DruidDataSource来获取数据库连接,关键就在于,MasterSlaveConnection会根据当前要执行的SQL来判断到底是获取主库的数据库连接,还是从库的数据库连接,尽管我们目前主从是同一个库,但是对于MasterSlaveConnection或者sharding-jdbc来说它不管关心主从是不是同一个库,它只会根据要执行的SQL分别去跟主库或从库建立数据库连接,比如如果是select就和从库建立数据库连接,如果是insert、delete、update就和主库建立数据库连接。

回到上面的分支事务回滚的流程,在select...for update时,获取的从库数据库连接 ,而在后续delete时获取的是主库数据库连接,这块我debug确认过,确实是这样,由于是不同的数据库连接,所以最终这两个SQL处于不同的Mysql事务,从而导致了死锁,前一个事务加了锁,但是事务又没有提交(按seata的逻辑要delete之后才提交),从而导致另一个事务delete时一直加不到锁,无法继续支持,从而导致分支事务回滚失败,这就是问题的根本。

解决方式

所以,大家如果能够理解我上面分析的,就能扩展想到,seata的DataSourceProxy是不能代理MasterSlaveDataSource,或者不建议代理sharding-jdbc的,因为一旦代理了,那么seata在执行一些逻辑时,就可能导致原本需要在一个Mysql事务中执行的众多操作被分散到多个数据库连接中去了,从而导致问题,所以我们改造的方式就是替换两者的代理顺序,让MasterSlaveDataSource去代理DataSourceProxy(具体如何实现后续文章进行分析),这样就能保证,seata中的操作会在同一个数据库连接中执行。

顺便提一句,就算上述场景改为主从是不同的数据库,也可能会出现问题,比如seata中在提交分支事务时会插入undolog,此时插入是主库,但是回滚分支事务需要查询undolog,此时查询时从库,如果主从同步延时比较大,那么很有可能查不到undolog,从而导致分支事务回滚失败。

好了,希望大家有所收获,记得帮我点个赞,帮我分享出去,谢谢。

我是大都督周瑜,之前是一名讲师,现在是一名架构师,实践才能出真知,这是我重回一线的原因!如果大家觉得有所收获,不想错过更多实战干货高质量技术文章 ,可以关注我的公众号:Hoeller

相关推荐
Re.不晚14 分钟前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
雷神乐乐20 分钟前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven
码农派大星。23 分钟前
Spring Boot 配置文件
java·spring boot·后端
顾北川_野30 分钟前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
江深竹静,一苇以航33 分钟前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
confiself1 小时前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言
Wlq04151 小时前
J2EE平台
java·java-ee
XiaoLeisj1 小时前
【JavaEE初阶 — 多线程】Thread类的方法&线程生命周期
java·开发语言·java-ee
杜杜的man1 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*1 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go