最近在将本地的JavaWeb项目部署到服务器上之后遇到的一个奇怪问题
- 在部署完成后,网站当时可以正常工作,但是第二天访问网站的时候却会遇到一个500 Server Error。
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
。
接下来一一分析这几个解决方案是否可行
方法一
- 通过定时的
SELECT
操作来保持数据库连接的有效性
首先看一下我的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
方法二
- 延长数据库的<b>wait_timeout</b>时间
这个方法我并没有使用,因为wait_timeout决定的是非交互连接的时间长短。
将这个值设置得大了,可能会导致空闲连接过多。
如果你的MySQL Server有大量的闲置连接,他们不仅会白白消耗内存,而且如果连接一直在累加而不断开,最终肯定会达到MySQL Server的连接上限数,这会报’too many connections’的错误。将这个值设置得小了,那就失去了使用方法二的意义。
因此可以得出结论: 方法二虽然可以解决问题,但是wait_timeout的值到底设置为多少比较合适没有一个固定的说法,不好把握。
参考链接:
http://www.cnblogs.com/jiunadianshi/articles/2475475.html
http://stackoverflow.com/questions/4440336/mysql-wait-timeout
方法三
- 在程序中进行数据库操作之前检验数据库连接的有效性
这个方法虽然可行,但是过于繁琐,因为它要确保在进行对数据库的任何操作之前都要检验数据库连接是否断开。
我并没有使用这个方法,因为代码中的数据库操作已经封装好,再去进行修改一是麻烦,二是可能会带来其他bug。
方法四
- 将数据库的
autoConnect
属性设置为true
这个方法也是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
属性。
- 数据库自动连接的方法就是在获取连接时检查一下,看看该连接是否还有效,即该Connection是否已经被MySQL数据库关闭了,如果关了就重新建立一个连接。
因此,我导入了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/