MySQL JDBC 5.1.25的一个坑(应该算是BUG)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
云数据库 RDS PostgreSQL,高可用系列 2核4GB
简介:

这是公司的一个重要项目中的真实案例(目前还未证实其它版本是否存在,不过刚看了最新版5.1 .26版本还是没有修复这个操作方式,不过用的小伙伴们要注意了哦):

【该BUG,官方目前最新版本已经修复,详细请参考文章最后,大家注意使用的版本和原因即可】


什么样的情况呢,当在代码中使用connection.close()方法的时候,神奇般的StackOverflow了!没错,这就是JDBC自己导致的死递归,堆栈输出的内容如下所示:


这个堆栈信息可以这样反推程序:

    ConnectionImpl.realClose()

-> ConnectionImpl.closeAllOpenStatements()

->StatementImpl.realClose()

->ResultSetImpl.close()

->ResultSetImpl.realClose()

->RowDataDynamic.close()

->StatementImpl.executeSimpleNonQuery()

->ConnectionImpl.execSQL()

->ConnectionImpl.cleanup()

->ConnectionImpl.realClose()//到这里回来了,于是乎接下来的事情,就按照这个顺序一发不可收拾,栈日志TNND几十米长,最后StackOverflowError是必然的了。


这是多么神奇的事情啊,MySQL的JDBC发布的时候难道也没有测试下,但是这种情况据同事介绍也不是每次都会发生,是偶然性情况。于是就要跟一下内在代码是怎么回事。

首先在MySQL JDBC现在的代码中,Connection接口是通过它内部的一个com.mysql.jdbc.ConnectionImpl的实现类来实现的,因此要跟踪close方法,就跟踪它就好了,如下图:


这里进入正轨,realClose方法进去了,这个方法很长,里面涉及到一些判定是否已经关闭、回滚、io关闭等等操作,在io关闭操作之前,需要关闭被打开的Statement信息,换句话说,MySQL在调用Connection.close的时候会自动关闭掉Statement信息,而无需业务代码来编写,不过你也写了也没错,其余代码我们忽略掉,因为不是问题的重点,关键是它内部确实调用了一个这样的方法,如下图所示:


在这个方法里面,会做什么动作呢?简单来说,就是循环,并调用对应的statement的close()方法



注意这里传入了两个参数,分别是false、true,第二个参数对这个问题是有用途的,第二个参数代表是否关闭掉Statement下面的ResultSet,在StatementImpl类具体的实现方法中的部分是:


第一个close自然是close掉当前的ResetSet、第二个是要获取generatedKey的结果集,最后一个closeAllOpenResults是会关闭掉内部记录的一个Set列表,这个列表会在获取GeneratedKeys、getMoreResults(java.sql.Statement.KEEP_CURRENT_RESULT)是增加ResultSet进去。


我们这里主要关心第一个,就是ResultSet的close方法,它ResultSetImpl实现类的close方法,如下所示:


这里依然调用了ResultSet的realClose方法,和日志中输出的内容一致,这个方法的finally部分会调用一个叫做rowData.close()方法:


它的类型是:RowData,是MySQL的一个接口:com.mysql.jdbc.RowData,它的实现类有:


具体使用哪一个,会由一些参数来决定,这个说起来又会涉及到许多源码,与本问题关系不大,暂时不扯开,从堆栈输出中可以看到使用的是第二个RowDataDynamic这个实现类,于是乎打开这个close方法的代码来看看,也很长,不过我们关注关键的部分,那就是它还调用了statement,如下图所示:


这里关闭的时候其实要执行一条语句,它创建了一个Statement,没有什么问题,因为这个Statement和当前ResultSet的Statement不是同一个(也就是这个Statement可能不是用的RowDataDynamic),但是它调用了一个executeSimpleNonQuery,这个方法需要传入Connection对象,显然一个Connection下面不论多少个Statement,这个Connection都是同一个。这个方法内部做了什么呢?


这个代码显然调用了connection的一个executeSQL方法(注意了,这里的MySQLConnection其实是一个接口,是MySQL自己继承于java.sql.Connection的接口,ConnectionImpl也是实现这个接口的,对象始终是同一个)。

接下来又回到ConnectionImpl的execSQL方法里面了,这段代码在内部的抛出异常的时候,且highAvailabilityAsBoolean为false的时候,会调用cleanup方法(默认为false,只有设置了autoReconnect才会变成true,这个参数在初始化Connection的时候被赋值):



这里只有抛出异常的时候会到这个里面来,但是异常确实发生了,而且这种发生往往是偶然的,而且一旦偶然发生,将一发不可收拾(例如网络闪了,或服务器端做了什么kill之类的操作),这个cleanup方法内部就会再次调用realClose方法:


显然,这里的Connection还没有关闭完,所以io不会为空,而且isClose也会返回false,自然会调用realClose方法,这个方法就回到前面的第二幅图的代码了,就这样,程序一发不可收的开始递归了。

使用类似代码的童鞋要注意了,换下版本就好,其余的版本还没看过代码,5.1.16代码路径有所不同,但也有类似的问题,打算抽时间看看5.1.26是否已经修复。

版本中5.1.16中在RowDataDynamic的close()方法也同样调用了realClose,里面并没有调用ConnectionImpl来操作,而是直接用本地的一个ResultSet将其关闭掉了:


另外,需要注意的是,其实StackOverflow往往没有平时Demo演示的那么简单,往往经过复杂的嵌套逻辑,以及希望大量的代码复用,在一些偶然的逻辑结构下导致递归起来,而且这种偶然一旦发生可能就形成一种必然了。

最后,小伙伴们不要认为最新的东西就是最好的哦,哈哈!这几天经过验证,可以很容易重现,测试了5.1.6、5.1.16、5.1.26、5.1.27(最新)全部会抛出错误,不过5.1.16以前的StackOverflowError代码路径不同而已。

测试的顺序:

1、在创建的Statement中,使用:((StatementImpl)statement).enableStreamingResults()

2、在发生close动作之前(可以使用断点或其它某种方式),将服务器端对应的session kill,或者将网络断开,或者直接server重启,相信这种操作线上发生是很正常的,不是故意用变态场景来模拟。

3、调用close方法,立即触发,小伙伴们可以自己模拟哈。


【更新于2015-12-18】:

官方邮件回复已经解决该问题,fix bug的版本为5.1.28,经过验证已经OK,并将同样的程序重现在5.1.27上会出现StackOverflowError。


不过值得注意的是,在5.1.28、5.1.29两个版本中,在连接断开和会话被kill的情况下,如果调用close方法会抛出异常:

com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after statement closed.

官方后来觉得对于close方法,没有必要抛出这样的异常,因为连接关闭了就关闭了,因为本来这个动作就是做关闭的,确实也应该是这样的,即使官方抛出异常,我们最多打个没用的log,然后忽略掉。所以在5.1.30及以后的版本中,在上述场景调用close方法时是不会抛出异常的。



相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
目录
相关文章
|
Java 关系型数据库 MySQL
mysql5.7 jdbc驱动
遵循上述步骤,即可在Java项目中高效地集成MySQL 5.7 JDBC驱动,实现数据库的访问与管理。
2486 1
|
SQL druid Java
Java数据库部分(MySQL+JDBC)(二、JDBC超详细学习笔记)(下)
Java数据库部分(MySQL+JDBC)(二、JDBC超详细学习笔记)
186 3
Java数据库部分(MySQL+JDBC)(二、JDBC超详细学习笔记)(下)
|
SQL 分布式计算 关系型数据库
Hadoop-24 Sqoop迁移 MySQL到Hive 与 Hive到MySQL SQL生成数据 HDFS集群 Sqoop import jdbc ETL MapReduce
Hadoop-24 Sqoop迁移 MySQL到Hive 与 Hive到MySQL SQL生成数据 HDFS集群 Sqoop import jdbc ETL MapReduce
321 0
|
SQL 分布式计算 关系型数据库
Hadoop-23 Sqoop 数据MySQL到HDFS(部分) SQL生成数据 HDFS集群 Sqoop import jdbc ETL MapReduce
Hadoop-23 Sqoop 数据MySQL到HDFS(部分) SQL生成数据 HDFS集群 Sqoop import jdbc ETL MapReduce
192 0
|
SQL 分布式计算 关系型数据库
Hadoop-22 Sqoop 数据MySQL到HDFS(全量) SQL生成数据 HDFS集群 Sqoop import jdbc ETL MapReduce
Hadoop-22 Sqoop 数据MySQL到HDFS(全量) SQL生成数据 HDFS集群 Sqoop import jdbc ETL MapReduce
237 0
|
前端开发 关系型数据库 MySQL
com.mysql.jdbc.Driver 和 com.mysql.cj.jdbc.Driver 的区别
这篇文章讨论了`com.mysql.jdbc.Driver`和`com.mysql.cj.jdbc.Driver`两个MySQL驱动类的区别,指出`com.mysql.jdbc.Driver`适用于MySQL 5的`mysql-connector-java`版本,而`com.mysql.cj.jdbc.Driver`适用于MySQL 6及以上版本的`mysql-connector-java`。文章还提到了在实际使用中如何根据MySQL版本选择合适的驱动类。
com.mysql.jdbc.Driver 和 com.mysql.cj.jdbc.Driver 的区别
|
关系型数据库 MySQL Java
【Azure 应用服务】App Service 无法连接到Azure MySQL服务,报错:com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
【Azure 应用服务】App Service 无法连接到Azure MySQL服务,报错:com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
447 0
|
2月前
|
缓存 关系型数据库 BI
使用MYSQL Report分析数据库性能(下)
使用MYSQL Report分析数据库性能
126 3
|
2月前
|
关系型数据库 MySQL 数据库
自建数据库如何迁移至RDS MySQL实例
数据库迁移是一项复杂且耗时的工程,需考虑数据安全、完整性及业务中断影响。使用阿里云数据传输服务DTS,可快速、平滑完成迁移任务,将应用停机时间降至分钟级。您还可通过全量备份自建数据库并恢复至RDS MySQL实例,实现间接迁移上云。
|
2月前
|
关系型数据库 MySQL 分布式数据库
阿里云PolarDB云原生数据库收费价格:MySQL和PostgreSQL详细介绍
阿里云PolarDB兼容MySQL、PostgreSQL及Oracle语法,支持集中式与分布式架构。标准版2核4G年费1116元起,企业版最高性能达4核16G,支持HTAP与多级高可用,广泛应用于金融、政务、互联网等领域,TCO成本降低50%。

推荐镜像

更多