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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 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方法时是不会抛出异常的。



相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
2月前
|
Java 关系型数据库 MySQL
mysql5.7 jdbc驱动
遵循上述步骤,即可在Java项目中高效地集成MySQL 5.7 JDBC驱动,实现数据库的访问与管理。
327 1
|
2月前
|
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
96 0
|
2月前
|
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
43 0
|
2月前
|
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
56 0
|
4月前
|
SQL druid Java
Java数据库部分(MySQL+JDBC)(二、JDBC超详细学习笔记)(下)
Java数据库部分(MySQL+JDBC)(二、JDBC超详细学习笔记)
60 3
Java数据库部分(MySQL+JDBC)(二、JDBC超详细学习笔记)(下)
|
4月前
|
前端开发 关系型数据库 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 的区别
|
4月前
|
关系型数据库 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
177 0
|
4天前
|
关系型数据库 MySQL 数据库
Python处理数据库:MySQL与SQLite详解 | python小知识
本文详细介绍了如何使用Python操作MySQL和SQLite数据库,包括安装必要的库、连接数据库、执行增删改查等基本操作,适合初学者快速上手。
51 15
|
5天前
|
关系型数据库 MySQL 数据库
数据库数据恢复—MYSQL数据库文件损坏的数据恢复案例
mysql数据库文件ibdata1、MYI、MYD损坏。 故障表现:1、数据库无法进行查询等操作;2、使用mysqlcheck和myisamchk无法修复数据库。
|
9天前
|
SQL 关系型数据库 MySQL
MySQL导入.sql文件后数据库乱码问题
本文分析了导入.sql文件后数据库备注出现乱码的原因,包括字符集不匹配、备注内容编码问题及MySQL版本或配置问题,并提供了详细的解决步骤,如检查和统一字符集设置、修改客户端连接方式、检查MySQL配置等,确保导入过程顺利。