JavaEE解决jdbc连接超时问题

前言

前阵子又重回老本行,开始了 JavaEE 的征程,为公司的技术部成立了 Java 组,实现了去年年底总结写的今年的期望。开始踩坑,发现每天早上第一次调用服务端的 API 时,都会有一个报错,查看日志发现是 jdbc 连接超时抛出异常。

1
2
3
4
5
6
7
org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction;
nested exception is com.mysql.jdbc.exceptions.jdbc4.CommunicationsException:
The last packet successfully received from the server was 145,178,488 milliseconds ago.
The last packet sent successfully to the server was 145,178,488 milliseconds ago.
is longer than the server configured value of 'wait_timeout'.
You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection property 'autoReconnect=true' to avoid this problem.
...

原因

第一次出现这个问题时,我直接重启 Tomcat 就一切恢复正常了,但是又过了一天,问题再次出现。

仔细分析错误信息,问题肯定出在数据库连接部分,在网上查阅大量信息后,明确了问题的原因:
MySQL 对所有连接的有效时间默认为28800秒,正好8小时,也就是说,如果一个连接8小时没有请求和操作,就会自动断开。但是对于 Hibernate 来说,它的连接池并不知道它所管理的连接中是否有被 MySQL 断开的。如果一个程序要使用数据库连接,而 Hibernate 的连接池分配一个已经被 MySQL 断开了的给程序使用,那么便会出现错误。

为了证实确实是这个错误,我在本机上做了如下测试:首先部署项目并启动 Tomcat,修改系统时间,往后调1天。然后再调用 API 测试,果然出现这个问题。

MySQL + Hibernate 架构相当普遍,所以这个问题也相当普遍。有空建议做一下同样的测试,看看是否存在此问题,不要因为恰好用户多,访问量大而侥幸绕开了这个潜藏的问题。

解决

先说一种简单粗暴的解决办法,就是修改 JDBC 连接的 URL,添加 autoReconnect=true 这个参数,就能解决问题。

1
jdbc:mysql://hostaddress:3306/schemaname?autoReconnect=true

但是注意了,MYSQL5以上设置 autoReconnect=true 是无效的,只有4.x版本有效。当前使用的版本是5.7,所以还是得想想别的思路。

思路

思路1:增大 MySQL 的连接有效时间。
思路2:从程序自身出发,修改连接池的相关参数。

显然,思路1是不可行的,原因如下几点:

  • 不能保证这个问题绝对的解决,如果无访问时间够长,大于 MySQL 的连接有效时间,问题同样会发生。
  • 遭到了 DBA 的强烈抵制,会影响其他的 MySQL 的性能。
  • 一般情况下,我们都是将程序部署在别人的服务器上,不太方便去修改 MySQL 配置。

所以网上的答案诸如 「 需要修改 my.conf,在 mysqld 后添加 wait_timeout=172800,interactive_timeout=172800 」是不可取的,不能从根本上解决问题。

那么只能从思路2入手了,修改连接池的相关参数。

Hibernate 支持如下的连接池:

  • DriverManagerConnectionProvider:代表由 Hibernate 提供的默认的数据库连接池
  • C3P0ConnectionProvider:代表 C3P0 连接池
  • ProxoolConnectionProvider:代表 Proxool 连接池
  • DBCPConnectionProvider:代表 DBCP 连接池
  • DatasourceConnectionProvider:代表在受管理环境中由容器提供的数据源

其中,默认连接池并不支持在分配一个连接时,测试其有效与否的功能。而 C3P0、Proxool、DBCP 都提供了这样的功能,正好可以解决上述问题。综合考虑各个连接池的效率、稳定性、易用性,决定换用 Proxool,它确实在各方面表现优良,方便配置。

配置

Proxool 连接池是 sourceforge 下的一个开源项目,这个项目提供一个健壮、易用的连接池,最为关键的是这个连接池提供监控的功能,方便易用,便于发现连接泄漏的情况。

Hibernate配置文件

1
2
3
4
5
6
7
8
9
<session-factory>
<property name="hibernate.connection.provider_class">org.hibernate.connection.ProxoolConnectionProvider</property>
<property name="hibernate.proxool.xml">proxool.xml</property>
<property name="hibernate.proxool.pool_alias">mysql</property>
<property name="show_sql">false</property>
<property name="dialect">org.hibernate.dialect.MySQLDialect</property>
<mapping resource="ren/bingo/pojo/hbm.xml" />
...
</session-factory>

其中各属性含义如下:

  • hibernate.connection.provider_class : 指明使用 Proxool 连接池。
  • hibernate.proxool.xml : 指明 Proxool 配置文件所在位置,这里与 Hibernate 的配置文件在同一目录下。
  • hibernate.proxool.pool_alias : 指明要使用的 proxool.xml 中定义的 proxool 别名。

Proxool配置文件 (proxool.xml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="UTF-8"?>
<!-- the proxool configuration can be embedded within your own application's. Anything outside the "proxool" tag is ignored. -->
<something-else-entirely>
<proxool>
<!-- proxool别名 -->
<alias>mysql</alias>
<!-- 数据库连接Url -->
<driver-url>
jdbc:mysql://localhost:3306/yourDatebase?useUnicode=true&characterEncoding=UTF-8
</driver-url>
<!-- JDBC驱动名称 -->
<driver-class>com.mysql.jdbc.Driver</driver-class>
<!-- 数据库连接帐号 -->
<driver-properties>
<property name="user" value="root" />
<property name="password" value="password" />
</driver-properties>
<!-- proxool自动侦察各个连接状态的时间间隔(毫秒),侦察到空闲的连接就马上回收,超时的销毁 -->
<house-keeping-sleep-time>90000</house-keeping-sleep-time>
<!-- 指因未有空闲连接可以分配而在队列中等候的最大请求数,超过这个请求数的用户连接就不会被接受 -->
<maximum-new-connections>20</maximum-new-connections>
<!-- 最少保持的空闲连接数 -->
<prototype-count>3</prototype-count>
<!-- 允许最大连接数,超过了这个连接,再有请求时,就排在队列中等候,最大的等待请求数由maximum-new-connections决定 -->
<maximum-connection-count>20</maximum-connection-count>
<!-- 最小连接数 -->
<minimum-connection-count>3</minimum-connection-count>
<!-- 在分配连接前后是否进行有效性测试,这个是解决本问题的关键 -->
<test-before-use>true</test-before-use>
<test-after-use>true</test-after-use>
<!-- 用于测试的SQL语句 -->
<house-keeping-test-sql>SELECT CURRENT_USER</house-keeping-test-sql>
</proxool>
</something-else-entirely>

其中,解决问题最关键是这两行,在分配连接前后是否进行有效性测试。

1
2
<test-before-use>true</test-before-use>
<test-after-use>true</test-after-use>

而下面的 test-sql 则是进行测试的 SQL 语句。

1
<house-keeping-test-sql>SELECT CURRENT_USER</house-keeping-test-sql>

这种思路就是分配连接前后,进行有效性测试,如果无效,则新建一个连接再返回。

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<servlet>
<servlet-name>ServletConfigurator</servlet-name>
<servlet-class>org.logicalcobwebs.proxool.configuration.ServletConfigurator</servlet-class>
<init-param>
<param-name>xmlFile</param-name>
<param-value>WEB-INF/proxool.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>proxool</servlet-name>
<servlet-class>org.logicalcobwebs.proxool.admin.servlet.AdminServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>proxool</servlet-name>
<url-pattern>/proxool</url-pattern>
</servlet-mapping>

自此,Proxool 配置成功。重新启动 Tomcat,再次做上述测试,问题已解决。

如果想要用 Spring 以 dataSource 方式使用 proxool 连接池,则稍微改一下写法。在 Spring 的 applicationContext.xml 中定义 dataSource bean 即可,property 参数还是一样的。

后话

Question :
现在 SSM 框架这么火,谁还用 SSH ?不用 Hibernate 而使用 Mybatis 怎么办?

Answer :
本文只是讲清问题的原因,以及解决的思路。不同的集成框架,解决问题的办法总是大同小异的,网上搜索肯定可以找到满意的资料。当然,作为一个很热衷于归档的人,我肯定也会做一篇专题总结的。

既然说到火,那就再火一点,干脆改用 SSM 搭配堪称最好的数据库连接池 Druid。下一篇博文我会详细地介绍阿里巴巴的这个开源项目,以及 Druid 的使用。

  • 本文作者: Bingo
  • 本文链接: https://blog.bingo.ren/49.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!