关于mysql:建议自查MySQL驱动Bug引发的事务不回滚问题也许你正面临该风险

5次阅读

共计 5828 个字符,预计需要花费 15 分钟才能阅读完成。

明天分享一个 开源文档在线预览我的项目解决方案 kkFileView作者发现的最新发现。

如题目,最终查明问题是因为 mysql-connector-java:8.0.28 的一个 bug 导致的。然而在假相未浮出之前,整个问题堪称错综复杂,博主良久没有排查过如此得劲的 bug,随着一层层的 debug 深刻,假相也随之浮出水面。这个问题属于底层 jdbc 驱动的问题,具备普遍性,可能人不知; 鬼不觉中,你的利用也在线上蒙受这个 bug 的残害,所以,请急躁听我讲完这个故事,而后回去查看下你的利用状态,是否也踩坑了。喜爱的能够间接拉到文末结语看后果。

背景

讲故事个别先介绍人物、背景。这里也不列外,先把相干方介绍下。通常,故事情节越丰盛越精彩,然而这里博主会思考篇幅 (不讲废话) 会把一些与后果无关的细节疏忽掉,力求叙述残缺就好。

  • commons-db : 咱们外部保护的,是一个采纳注解驱动的 Spring 生态下的大多数资源管理组件。组件给每个 DataSource 预设了些性能优化的默认值,没有全副列出,不过蕴含了影响问题走向的属性(useLocalSessionState),如下:
Properties defaultProperties = new Properties();
defaultProperties.put("prepStmtCacheSize", 300);
defaultProperties.put("prepStmtCacheSqlLimit", 2048);
defaultProperties.put("useLocalSessionState", true);
defaultProperties.put("cacheResultSetMetadata", true);
defaultProperties.put("elideSetAutoCommits", true);
  • java-project : 用来测试组件性能的我的项目,会作为和呈现问题的我的项目做行为测试比照。spring-boot:2.5.4、mysql-connector-java:8.0.26
  • store:游戏库我的项目,正是这个我的项目发现了问题。spring-boot:2.6.6、mysql-connector-java:8.0.28
  • 阿里云 RDS (MySQL): 阿里云 MySQL 默认的隔离级别为 READ_COMMITTED,而 MySQL 默认的隔离级别为 REPEATABLE_READ

阐明:java-project 和 store 的 commons-db 版本其实不一样,因为不会影响后果。这里与他们版本统一。

问题

一天,开发反馈,在 store 我的项目里应用 commons-db 组件时,呈现了事务回滚不失效的问题。如下图代码所示:

@Transactional
@DataSource(type = Type.MASTER,value = "developer")
public void addUser(ApolloUser user){userRepository.save(user);
    int i = 1/0; // 抛异样
}
  • 具体表现为:执行 addUser 办法,当 1/0 抛出 RuntimeException 类型异样时,user 对象还是增加胜利了。一句话总结就是,【事务回滚不失效了】。

假如

  • 假如 1:曾假如过是不是 @Transactional 的 aop 没失效,导致并未开启显式事务。
  • 假如 1 不成立,因为在开启了 debug 日志模式后,清晰地输入了事务每个阶段的行为日志,如:
  • 假如 2:思考到应用了 commons-db , 如果框架层连贯治理问题,导致了事务的开启、事务回滚时获取到的连贯不统一,也有可能导致这个问题。
  • 假如 2 不成立:马上就否了,因为从下面日志上能够看到连贯是同一个连贯。而且不同连贯执行非预期的开启、回滚事务操作应该会有异样才是。

那么到这里,问题就陷入了僵局。不禁深思,一个看上去人畜有害的代码,一个看上去逻辑清晰的事务日志,为什么会事务回滚生效呢?????

转折

转折 1

随后,我在 java-project 我的项目里,应用雷同的 MySQL 测试了下,发现事务回滚胜利了。阐明这个问题仅仅影响特定的环境,而且能够通过比照两个我的项目的差别找到问题,离假相更近了。

转折 2

开发那边又传来一个要害的信息,在 store 我的项目中,当设置隔离级别为 REPEATABLE_READ 时,事务回滚失效了。代码如:

   @Transactional(isolation = Isolation.REPEATABLE_READ)
   @DataSource(type = Type.MASTER,value = "developer")
   public void addUser(ApolloUser user){userRepository.save(user);
       int i = 1/0;
   }

到这里,难道要狐疑是隔离级别的问题么?显然是不成立的,因为对事务的认知字典里,就没呈现过隔离级别影响事务回滚的字条。而后从 java-project 的测试也能够看出,在雷同的 RC 隔离级别下,java-project 能够胜利。

第一个解决办法

而后终归是向后退了一步了,能够长期用设置隔离级别的方法来解决【事务回滚不失效问题】。不过,不同的隔离级别,对事务锁、并发性能是不一样的,这个在调整前必须要有预期。

转折 3

事出反常必有妖,本着不信是隔离级别导致的问题,我在 store 我的项目里将 isolation 设置成 Isolation.READ_UNCOMMITTED,发现事务回滚也失效了。这也阐明了和隔离级别没有间接的关系。而后本着探索【为啥默认的起因 READ_COMMITTED 导致事务不失效?】的思路排查了下,发现了些问题,如下代码是事务逻辑中的一部分(源码见:DataSourceUtils.prepareConnectionForTransaction ()):

发现,相比 RR、RU , 差异就是当隔离级别是 READ_COMMITTED 时,不会再对 session 有更新操作了。到这一步也只是多了一个明确的景象,能够解释晓得假相后的行为,并没有触达假相边缘。

剖析

上文整了一堆,还没发现实在问题。所以先不做其余测试了,先剖析下有何预期后,再针对性去验证。

先来看下广泛的失常的 Spring Transactional 残缺的事务回滚的过程,广泛的指的是没有做过非凡参数配置的,个别这些参数也不会配置。

  • 1、在增加了 @Transactional 的办法执行前,会执行事务管理器(DataSourceTransactionManager)的 doBegin 办法创立一个事务,在 doBegin 办法里,会设置 autoCommit = false。会判以后隔离级别是否和用户定义的统一,否则就更新隔离级别。
  • 2、办法执行失败后,会执行事务管理器(DataSourceTransactionManager)的 doRollback 办法回滚事务。

从 Spring Transactional 的事务日志没看进去问题,创立事务、设置手动提交事务、回滚事务都有日志打印。那么咱们就深刻到驱动层、或者抓包看,是否这些指令都发到 MySQL Server 了。

定位问题

如剖析,在 store 我的项目中,将断点打在 mysql-connector-java 驱动的 NativeSession.execSQL () 办法里,和 MySQL Server 交互的所有指令,最终都会调用这个办法执行。果然发现了问题:

  • 事务回滚失败时,事务流程并未执行 SET autocommit=0 指令。

等于说事务回滚失败时,事务始终是主动提交的模式,所以,异样回滚操作并不会回滚曾经长久化了的数据。

发现这个问题后,接着定位为什么 Spring 执行了 Set autoCommit=false , 而最终确并未执行的问题,这里再次通过【转折 1】的 java-project 我的项目做单步调试比照,发现一段要害代码(ConnectionImpl.setAutoCommit ())两个我的项目里的代码不统一:

java-project,mysql-connector-java:8.0.26(事务回滚失效)

store,mysql-connector-java:8.0.28(事务回滚不失效)

这里略微介绍下这个参数

  • useLocalSessionState:保护本地 sessionState , 在须要判断【事务提交模式】、【隔离级别】设置时,获取本地状态,而不是每次像 MySQL Server 发动询问。

这个参数有助于缩小和 MySQL 的交互,能够晋升写数据性能。所以在参数性能优化时,被默认设置为 true 了。这里,如果 useLocalSessionState=false,则正好会覆盖这个 bug。

解密

因为在 store,mysql-connector-java:8.0.28 有问题的版本的 isAutocommit () 行为逻辑和 isAutoCommit () 不统一,本该调用判断 isAutocommit 返回 true 时,却返回了 false。最终才导致了 store 在接管到 Spring Transactional 设置 autoCommit=false 的申请时,因为 needsSetOnServer=false , 间接跳过了真正的发动 Set autocommit=0 指令的执行。导致以后事务模式是主动提交模式,所以当事务里有任何增删改操作时,会在执行完后立马 commit 长久化。这时如果异样而发动事务 rollback,天然不会回滚之前曾经主动提交的事务。这个很好的解释了结尾贴出的事务日志很残缺,然而事务就是回滚不失效的问题。

第二个解决办法

排查到这里,第二个解决问题的办法就呈现了,只须要让判断是否须要执行 Set autocommit=0 时的 needsSetOnServer=true 成立就行了。所以,只有对 store 利用做如下两个参数任一参数配置调整,则能够解决问题了。这个办法比第一个办法要适合些:

useLocalSessionState=false
auto-commit=false

解释为啥 isolation 设置成 Isolation.REPEATABLE_READ 会失效

所以到这里就完结了吗?并没有,预期是即便 useLocalSessionState=ture,事务也应该残缺。而后别忘了 isAutoCommit () 和 isAutocommit () 的差别。先来看下他们的定义:

public boolean isAutocommit() {return (this.statusFlags & 2) != 0;
}

public boolean isAutoCommit() {return this.autoCommit;}

原来在 mysql-connector-java:8.0.28 驱动里,应用 statusFlags 状态代替了 autoCommit 的标识(这里先不讲究为什么做这个改变),这个解释了

  • 转折 2:当设置隔离级别为 REPEATABLE_READ 时,事务回滚失效了。是因为当用户定义的隔离级别 RR 和默认的 RC 不统一时,会触发 session 设置新的隔离级别,此时也会将 statusFlags = 0 更新为 statusFlags = 2. 故在调用 isAutocommit () 返回 true,满足了执行 SET autocommit=0 指令的条件。

这里尽管晓得了起因,也确切晓得 isAutoCommit () != isAutocommit (),然而为啥做如此改变确并不分明。这里具体问题暂且不表,先来复现下问题。

复现问题

既然问题曾经大差不差的定位到了,那么按惯例排查流程,按预期的问题场景复现下,明确下问题边界。因为还还有可能有其余的影响因素一起导致的问题。在 java-project 我的项目中,做如下依赖的版本调整

  • 降级 spring-boot:2.6.6 版本和 store 保持一致:问题复现了
  • 放弃 spring-boot:2.5.4,调整 mysql-connector-java:8.0.28:问题也复现了

到这里,根本排除了 Spring Transactional 的嫌疑了。而后将锋芒锁定到了 mysql-connector-java:8.0.28 身上。

确认 bug

思考到从 mysql-connector-java:8.0.26 的 isAutoCommit 更改到了 mysql-connector-java:8.0.28 的 isAutocommit 必定是有起因的,带着弄清楚代码作者提交这个改变的用意,去翻了下 github。

  • https://github.com/mysql/mysq…

找了下 github 的提交记录 commit,发现,最新版本的又改回了 isAutoCommit () 了,而后 Commit Message 明确阐明了这是 8.0.28 版本的 bug,如。

至此,终于水落石出了。

修复

  • 8.0.29 release:https://dev.mysql.com/doc/rel…
  • A connection did not maintain the correct autocommit state when it was used in a pool with useLocalSessionState=true. (Bug #106435, Bug #33850099)

最终解决办法

如 8.0.29 release 布告阐明,曾经修复了 8.0.28 在设置 useLocalSessionState=true 的状况下,autoCommit 状态设置的问题。所以,利用降级到 mysql-connector-java:8.0.29 版本即可

结语

先总结下问题表像为 Spring Transactional【事务回滚不失效,回滚前提交的数据不会回滚】,根本原因是【mysql-connector-java:8.0.28 版本提交的一个改变 bug,导致在启用 useLocalSessionState=true 的状况下,autoCommit 状态设置有问题】。

而后因为 spring-boot:2.6.3 ~ 2.6.7,这五个版本默认的 MySQL 驱动就是 mysql-connector-java:8.0.28,而 useLocalSessionState=true 简直是 Java JDBC DataSource 里的标配,所以这个 bug 预计会影响一大波人。而后因为只是影响回滚操作,所以这个问题会暗藏的很深,不容易觉察,所谓影响深远。

正文完
 0