MySQL数据库连接超时自动断开的解决方案
最近在将本地的JavaWeb项目部署到服务器上之后遇到的一个奇怪问题
Tomcat 的日志中关键信息如下:
05-May-2017 05:15:27.682 SEVERE [http-nio-8080-exec-5] org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [Login] in context with path [] threw exception
org.hibernate.exception.JDBCConnectionException: could not execute query
Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: The last packet successfully received from the server was 83,827,560 milliseconds ago. The last packet sent successfully to the server was 83,827,560 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.
Caused by: java.net.SocketException: Broken pipe (Write failed)
at java.net.SocketOutputStream.socketWrite0(Native Method)
at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)
at java.net.SocketOutputStream.write(SocketOutputStream.java:155)
at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
at com.mysql.jdbc.MysqlIO.send(MysqlIO.java:3721)
... 41 more
从日志中可以看出是MySQL数据库出现了异常
The last packet successfully received from the server was 83,827,560 milliseconds ago. The last packet sent successfully to the server was 83,827,560 milliseconds ago. is longer than the server configured value of 'wait_timeout'
最后一个数据包在 83827560 ms 之前被成功接收,最后一个数据包在83827560 ms 之前被成功发送。比服务的配置参数
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.
你应考虑在程序中进行数据库操作之前检验数据库连接的有效性或者将数据库的
autoConnect
属性设置为
true
来避免这个问题。
在google上搜索之后,发现问题出在如果超过这个
wait_timeout
时间(默认是8小时)对数据库没有任何操作,那么MySQL会自动关闭数据库连接以节省资源。而我的网站对数据库进行操作的时候却并未判断数据库连接是否断开,因此在管道(pipe)的这一端请求数据而另一端却被关闭的情况下,就会抛出这个
Broken pipe
异常。
这里就有一个疑问: 日志上说最后一个数据包是在
83827560ms
之前被成功接收和发送的,这个时间换算过来大概就是139分钟,也就是2小时多一点,明显没有超过
wait_timeout
的值。但是数据库连接自动断开的问题确实是在第二天发生了,也就是在一个晚上没有对数据库进行操作(显然超过了8小时)的情况下发生的这个问题。
这个疑问我暂时还没有找到答案,希望有知道的大神赐教。
不过既然找到了这个问题,那么我们就很容易考虑到如下的几个解决方案:
-
在主线程中定时发送一个
SELECT
操作来确保数据库一直保持连接状态。
-
延长数据库的
wait_timeout
时间。
-
按照日志中给出的解决方案在程序中进行数据库操作之前检验数据库连接的有效性。
-
按照日志中给出的解决方案将MySQL数据库的
autoConnect
属性设置为
true
。
接下来一一分析这几个解决方案是否可行
方法一
首先看一下我的Hibernate配置文件
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="connection.url">jdbc:mysql://localhost:3306/DatabaseName</property>
<property name="connection.characterEncoding">utf-8</property>
<property name="connection.username">root</property>
<property name="connection.password">password</property>
<!-- SQL dialect -->
<property name="dialect">org.hibernate.dialect.MySQLDialect</property>
<property name="show_sql">true</property>
<!--mapping files-->
......
</session-factory></hibernate-configuration>
接下来看一下Hibernate进行数据库连接的流程到底是什么样的
-
Hibernate(我导入的jar包是
hibernate3.jar
)在上面的那种配置之下,会有一个默认的连接池,名字叫:
DriverManagerConnectionProvider
;这是一个极其简单的连接池,默认会在池中保留20个连接,这些连接不是一开始Hibernate初始化时就创建好的,而是在你需要使用连接时创建出来,使用完之后才加入到池中的。这里有一个叫
closeConnection(Connection conn)
的方法,这个方法将传入的连接不做任何处理,放到池中。而这个类内部的连接池实际是一个
ArrayList
,每次取得时候
remove
掉
ArrayList
的第一个连接,用完后直接用
add
方法加入到
ArrayList
的最后。
-
程序对数据库进行操作时,Hibernate会通过
DriverManagerConnectionProvider
得到一个连接
Connection
,在使用完之后,调用
session.close()
时,Hibernate会调用
DriverManagerConnectionProvider
的
closeConnection
方法(就是上面说的那个
closeConnection(Connection conn)
方法),这个时候,该连接会直接放到
DriverManagerConnectionProvider
的
ArrayList
中,从始至终也没有地方去调用Connection的
close
方法。
我们的那个
SELECT
操作是定时的,而我们对数据库的其它操作显然并不是定时的,而这两个操作都会向
DriverManagerConnectionProvider
获取一个连接,并且工作完之后都会将自己获得的连接放回连接池的末尾。
说到这里,问题就很明显了,我们可以假设出现下面的场景
在
SELECT
操作进行的过程中,它会持有一个连接(记为
connection1
),此时数据库收到了其它的操作请求,由于此时
SELECT
操作还未结束,
connection1
并没有放回连接池,因此这个请求会再创建一个新的连接或者拿到连接池内部的一个连接(记为
connection2
)这两个操作都结束之后,
connection1
和
connection2
都被放回了连接池中,并且是放到了连接池的末尾。
**那么,下一次的
SELECT
操作拿到的连接到底是
connection1
还是
connection2
呢? **
答案是都有可能,这取决于上面的场景中连接被放回连接池的顺序以及两次
SELECT
操作之间是否有过其它的数据库操作,这些我们显然无法判断。
因此可以得出结论: 最终
SELECT
操作是否保持了连接的有效性我们
无法判断
。
参考链接:
http://blog.csdn.net/nethibernate/article/details/6658855
方法二
这个方法我并没有使用,因为
wait_timeout
决定的是非交互连接的时间长短。
因此可以得出结论: 方法二虽然可以解决问题,但是
wait_timeout
的值到底设置为多少比较合适没有一个固定的说法,不好把握。
参考链接:
http://www.cnblogs.com/jiunadianshi/articles/2475475.html
http://stackoverflow.com/questions/4440336/mysql-wait-timeout
方法三
这个方法虽然可行,但是过于繁琐,因为它要确保在进行对数据库的
任何
操作之前都要检验数据库连接是否断开。
我并没有使用这个方法,因为代码中的数据库操作已经封装好,再去进行修改一是麻烦,二是可能会带来其他bug。
方法四
这个方法也是Google上看到的最多的方案,不过在
MySQL开发者文档
中可以看到关于这个属性的描述
The use of this feature is not recommended, because it has side effects related to session state and data consistency
并不提倡使用这个特性,因为它可能会对会话状态和数据一致性产生副作用
到底副作用是什么,文档上并没有详细说明,在stackoverflow上搜索时倒是看到了
session state and transactions cannot be maintained over a new connection.
(原有的)会话状态和事务在新的连接中并不能被保持下来。
看到这里我就放弃了使用它的想法。
看来方法四并不能解决问题?
不过不要忘了我们是使用Hibernate框架而不是JDBC来进行数据库操作的,既然在MySQL配置中无法解决这个问题,那么我们可以考虑一下Hibernate配置。
最终解决方案
Hibernate中恰好有类似于
autoReconnect
这样的属性来自动保持数据库的连接,那就是使用c3p0连接池的
testConnectionOnCheckout
属性。
因此,我导入了c3p0的相关jar包,并且修改了Hibernate的配置文件
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="connection.url">jdbc:mysql://localhost:3306/DatabaseName</property>
<property name="connection.characterEncoding">utf-8</property>
<property name="connection.username">root</property>
<property name="connection.password">password</property>
<!-- SQL dialect -->
<property name="dialect">org.hibernate.dialect.MySQLDialect</property>
<property name="show_sql">true</property>
<!-- c3p0连接池配置 -->
<property name="hibernate.connection.provider_class">
org.hibernate.connection.C3P0ConnectionProvider </property>
<property name="hibernate.c3p0.max_size">20</property>
<property name="hibernate.c3p0.min_size">5</property>
<property name="hibernate.c3p0.timeout">50000</property>
<property name="hibernate.c3p0.max_statements">100</property>
<property name="hibernate.c3p0.idle_test_period">3000</property>
<!-- 当连接池耗尽并接到获得连接的请求,则新增加连接的数量 -->
<property name="hibernate.c3p0.acquire_increment">2</property>
<property name="hibernate.c3p0.testConnectionOnCheckout">true</property>
<!--mapping files-->
......
</session-factory></hibernate-configuration>
上面配置中最重要的就是hibernate.c3p0.testConnectionOnCheckout这个属性,它保证了每次取出连接时都会检查该连接是否被关闭,如果被关闭,则重新建立连接。
至此,困扰我的这个问题就解决了,分享出解决这个问题的过程,希望对大家也能有所帮助。
参考链接:
http://stackoverflow.com/questions/270800/why-does-hibernate-jdbc-mysql-drop-connections-after-a-day-or-so
http://blog.csdn.net/nethibernate/article/details/6658855
http://hibernatedb.blogspot.com/2009/05/automatic-reconnect-from-hibernate-to.html
c3p0相关jar包下载地址:
https://sourceforge.net/projects/c3p0/