今天分享开源文档在线预览项目解决方案kkFileView作者的最新发现。如题,最后发现是mysql-connector-java:8.0.28的一个bug导致的问题。但在真相浮出水面之前,整个问题扑朔迷离。博主好久没有查到这么厉害的bug了。随着层层调试的深入,真相也随之浮出水面。该问题属于底层jdbc驱动问题,具有普遍性。你可能在不知不觉中,你的申请也上线了这个bug,所以请耐心听我讲完这个故事,然后回过头来查看你的申请状态,你是不是也踩坑了?喜欢的可以直接去文末结语看结果。背景故事讲述通常从介绍人物和背景开始。这里也不例外,先介绍一下相关人士。通常,故事情节越丰富越精彩,但这里博主会考虑篇幅(废话不多说),会忽略一些与结果无关的细节,力求完整的叙述。commons-db:我们内部维护的是注解驱动的Spring生态系统中的大部分资源管理组件。组件为每个DataSource预设了一些性能优化的默认值。没有全部列出来,但是包含了影响问题方向的属性(useLocalSessionState),如下:",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.26store:游戏库项目,就是这个项目发现了问题。spring-boot:2.6.6,mysql-connector-java:8.0.28阿里云RDS(MySQL):阿里云MySQL默认隔离级别为READ_COMMITTED,MySQL默认隔离级别为REPEATABLE_READ说明:java-projectandstore的commons-db版本实际上是不同的,因为它不影响结果。这与他们的版本相同。问题一天,开发者反映在store项目中使用commons-db组件时,出现事务回滚不生效的问题。如下代码所示:@Transactional@DataSource(type=Type.MASTER,value="developer")publicvoidaddUser(ApolloUseruser){userRepository.save(user);诠释我=1/0;//抛出异常}具体表现为:执行addUser方法,当1/0抛出RuntimeException类型的异常时,用户对象依然添加成功。一句话总结就是,【事务回滚不生效】。假设假设一:一直假设@Transactional的aop没有生效,导致没有显式开启事务。假设1不成立,因为开启debuglog模式后,清晰的输出了事务各个阶段的行为日志,如:假设2:考虑使用commons-db,如果framework层连接管理问题导致事务的开启,事务回滚时获取的连接不一致,也可能导致这个问题。假设2不成立:会立即被拒绝,因为从上面的日志可以看出连接是同一个连接。而且,当不同的连接执行意外的打开和回滚事务操作时应该有异常。那么到这里,问题就陷入了僵局。不禁想,一个看起来人畜无害的代码,一个看起来逻辑清晰的事务日志,为什么事务回滚会失败呢?????转折点转折点1后来在java-project项目中使用同样的MySQL测试,发现事务回滚成功。说明这个问题只影响特定的环境,通过比较两个项目的差异可以发现问题,比较接近真相。另一个关键信息来自于turnaround2的开发端,在store项目中,当隔离级别设置为REPEATABLE_READ时,事务回滚生效。代码如:@Transactional(isolation=Isolation.REPEATABLE_READ)@DataSource(type=Type.MASTER,value="developer")publicvoidaddUser(ApolloUseruser){userRepository.save(user);诠释我=1/0;}此时,你是不是要怀疑是隔离级别的问题了?显然不是这样的,因为在事务的认知字典中,并没有注意到隔离级别影响事务回滚。然后通过java-project的测试可以看出,在相同的RC隔离级别下,java-project是可以成功的。第一种方案毕竟是进了一步,可以暂时使用设置隔离级别的方法来解决【事务回滚不生效的问题】。但是不同的隔离级别有不同的事务锁和并发性能,这些在调整前必须有所预期。转机3出事必有妖。我不相信这是由隔离级别引起的。我在store项目中设置隔离为Isolation.READ_UNCOMMITTED,发现事务回滚也生效了。这也说明与隔离级别没有直接关系。那么为了探究【为什么默认原因READ_COMMITTED导致事务不生效?】查了一下思路,发现了一些问题。下面的代码是部分事务逻辑(查看源码:DataSourceUtils.prepareConnectionForTransaction()):与RR和RU相比,不同的是当隔离级别为READ_COMMITTED时,不会再有更新操作会议。至此,只有一个比较清晰的现象,可以解释知道真相后的行为,但还没有触及真相的边缘。分析了上面一堆,也没发现真正的问题。所以先不要做其他的测试,先分析期望的是什么,然后有针对性的去验证。我们先来看下一般正常的SpringTransactional完成事务回滚流程。一般来说就是没有做特殊的参数配置,一般不会配置这些参数。1、在执行添加了@Transactional的方法之前,会先执行事务管理器(DataSourceTransactionManager)的doBegin方法,创建一个事务。在doBegin方法中,将设置autoCommit=false。判断当前隔离级别是否与用户自定义的一致,或者更新隔离级别。2、方法执行失败后,会执行事务管理器(DataSourceTransactionManager)的doRollback方法,回滚事务。从SpringTransactional的事务日志看不出问题。有用于创建事务、设置手动提交的事务和回滚事务的日志打印。然后我们再深入驱动层或者抓包,看看这些命令是不是都发给了MySQLServer。针对分析等定位问题,在store项目中,在mysql-connector-java驱动的NativeSession.execSQL()方法中设置断点,所有与MySQLServer交互的指令最终都会调用该方法执行。果然发现了问题:事务回滚失败时,事务进程没有执行SETautocommit=0指令。相当于说当事务回滚失败时,事务一直处于自动提交模式,所以异常回滚操作不会回滚持久化的数据。发现这个问题之后,我们再定位为什么Spring实现了SetautoCommit=false,但是最终没有执行。这里我们再次对比了[Transition1]中java-project的一步步调试,发现一个关键代码(ConnectionImpl.setAutoCommit())两个项目中的代码不一致:java-project,mysql-connector-java:8.0.26(事务回滚生效)store,mysql-connector-java:8.0.28(事务回滚不生效)这里稍微介绍下这个参数useLocalSessionState:维护本地的sessionState,获取本地的需要判断【事务提交方式】和【隔离级别】设置时的状态,而不是像MySQLServer那样每次都去查询。该参数有助于减少与MySQL的交互,提高写入数据的性能。因此,在优化参数性能时,默认设置为true。在这里,如果useLocalSessionState=false,就正好掩盖了这个bug。解密因为问题版本mysql-connector-java:8.0.28中isAutocommit()的行为逻辑与store中的isAutoCommit()不一致,应该调用判断isAutocommit返回true时返回false。最后store在收到SpringTransactional请求设置autoCommit=false时,因为needsSetOnServer=false,直接跳过了Setautocommit=0指令的实际执行。因此,当前的事务模式是自动提交模式,所以当事务中有任何增删改查时,执行后会立即提交。这时候如果因为异常发起了事务回滚,那么之前已经自动提交的事务自然不会回滚。这就很好的解释了最开始贴的事务日志是完整的,但是事务是回滚不生效的问题。到这里已经勾选了第二种方案,出现了问题的第二种方案。在判断是否执行Setautocommit=0时只需要让needsSetOnServer=true成立即可。因此,只要为商店应用配置并调整以下两个参数中的任意一个,即可解决问题。这种方法比第一种方法更合适:useLocalSessionState=falseauto-commit=false解释一下为什么设置隔离为Isolation.REPEATABLE_READ会生效,所以这里就结束了吗?不,期望是即使使用useLocalSessionState=ture,交易也应该完成。然后别忘了isAutoCommit()和isAutocommit()的区别。先看他们的定义:publicbooleanisAutocommit(){return(this.statusFlags&2)!=0;}publicbooleanisAutoCommit(){returnthis.autoCommit;}原来在mysql-connector-java:8.0.28中driver,使用statusFlags状态代替了autoCommit标志(这里先不看为什么要这样改),这就解释了转折点2:当隔离级别设置为REPEATABLE_READ时,事务回滚生效。这是因为当用户定义的隔离级别RR与默认的RC不一致时,会触发session设置新的隔离级别,statusFlags=0会更新为statusFlags=2。因此调用isAutocommit()返回true,满足了执行SETautocommit=0命令的条件。虽然知道这里的原因,也很清楚isAutoCommit()!=isAutocommit(),但是实在是搞不清楚为什么要这样改。这里的具体问题暂不罗列,先复现一下问题。问题复现问题已经大致定位,接下来按照例行的排查流程,重现预期的问题场景,明确问题边界。因为可能还有其他因素共同导致问题。在java-project项目中,做如下依赖版本调整,升级spring-boot:2.6.6版本与store:一致,问题重现保持spring-boot:2.5.4,调整mysql-connector-java:8.0。28:这里问题也重现了,基本排除了SpringTransactional的嫌疑。然后把手指锁定在mysql-connector-java:8.0.28上。确认错误。考虑到从mysql-connector-java:8.0.26的isAutoCommit改为mysql-connector-java:8.0.28的isAutocommit,一定是有原因的。本着搞清楚提交这个改动的代码作者的想法,我翻了个github下载。https://github.com/mysql/mysq...找了github的commit记录,发现最新的版本又变回了isAutoCommit(),而且CommitMessage中明确写着这是8.0.28版本的bug,例如。至此,真相终于大白。针对8.0.29版本的修复:https://dev.mysql.com/doc/rel...在useLocalSessionState=true的池中使用连接时,连接未保持正确的自动提交状态。(Bug#106435,Bug#33850099)最终解决方案如8.0.29发布公告中所述,已修复8.0.28中设置useLocalSessionState=true时的自动提交状态设置问题。因此,应用可以升级到mysql-connector-java:8.0.29版本。结论首先总结问题表为SpringTransactional【事务回滚不生效,回滚前提交的数据不会回滚】,根本原因是【mysql-connector-java提交的一个changebug:version8.0.28在启用useLocalSessionState=true时导致自动提交状态设置出现问题]。然后因为spring-boot:2.6.3~2.6.7,这五个版本默认的MySQL驱动是mysql-connector-java:8.0.28,而useLocalSessionState=true几乎是JavaJDBCDataSource的标配,所以这个bug了估计会影响一大批人。然后,因为它只影响回滚操作,所以这个问题会隐藏得很深,不容易被发现。所谓影响深远。
