问题描述
异常堆栈如下:
org.springframework.transaction.TransactionSystemException: nested exception is com.mysql.jdbc.excedbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed. at org.springframework.jdbc.datasource.DataSourceTransactionManager.doCommit(DataSourceTransactionManager.java:316) at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManage1) at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java: at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupp518) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:673) at com.weshow.interlive.business.service.AccountInfoService$$EnhancerBySpringCGLIB$$d0052aac.getAccountInfo(<generated>) at com.weshow.interlive.business.handler.AccoutMessageHandler.dealAccountBalance(AccoutMessageHandler.java:65) at com.weshow.interlive.business.handler.AccoutMessageHandler.processCommand(AccoutMessageHandler.java:41) at com.weshow.interlive.business.handler.AccoutMessageHandler$$FastClassBySpringCGLIB$$1db0c1c6.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:738) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) at org.springframework.aop.interceptor.AsyncExecutionInterceptor$1.call(AsyncExecutionInterceptor.java:115) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.lang.Thread.run(Thread.java:748) Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed. at sun.reflect.GeneratedConstructorAccessor69.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.Util.getInstance(Util.java:408) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:918) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:897) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:886) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:860) at com.mysql.jdbc.ConnectionImpl.throwConnectionClosedException(ConnectionImpl.java:1187) at com.mysql.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:1182) at com.mysql.jdbc.ConnectionImpl.commit(ConnectionImpl.java:1517) at sun.reflect.GeneratedMethodAccessor58.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.tomcat.jdbc.pool.ProxyConnection.invoke(ProxyConnection.java:126) at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:108) at org.apache.tomcat.jdbc.pool.interceptor.AbstractCreateStatementInterceptor.invoke(AbstractCreateStatementInterceptor.java: at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:108) at org.apache.tomcat.jdbc.pool.DisposableConnectionFacade.invoke(DisposableConnectionFacade.java:81) at com.sun.proxy.$Proxy88.commit(Unknown Source) at org.springframework.jdbc.datasource.DataSourceTransactionManager.doCommit(DataSourceTransactionManager.java:313) ... 17 common frames omitted
我这边采用的配置如下:
max-idle: 5 max-wait: 10000 min-idle: 2
可能原因
原因1:
MySQLNonTransientConnectionException: No operations allowed after statement closed
之所以会出现这个异常,是因为Mysql在5以后针对超长时间DB连接做了一个处理,那就是如果一个DB连接在无任何操作情况下过了8个小时后,Mysql会自动把这个连接关闭。所以使用连接池的时候虽然连接对象还在但是链接数据库的时候会一直报这个异常。解决方法很简单在Mysql的官方网站上就可以找到。
解决方案:
第一种是在DB连接字符串后面加一个参数。
这样的话,如果当前链接因为超时断掉了,那么驱动程序会自动重新连接数据库。
jdbc:mysql://localhost:3306/makhtutat?autoReconnect=true
不过Mysql并不建议使用这个方法。因为第一个DB操作失败的后,第二DB成功前如果出现了重新连接的效果。这个失败操作将不会处于一个事务以内,第二DB操作如果成功的话,这个事务将被提交。
conn.createStatement().execute(
"UPDATE checking_account SET balance = balance - 1000.00 WHERE customer='Smith'");
conn.createStatement().execute(
"UPDATE savings_account SET balance = balance + 1000.00 WHERE customer='Smith'");
conn.commit();
当然如果出现了重新连接,一些用户变量和临时表的信息也会丢失。
另一种方法是Mysql推荐的,需要程序员手动处理异常。
<span style="font-family:'Microsoft YaHei';font-size:12px;">public void doBusinessOp() throws SQLException { Connection conn = null; Statement stmt = null; ResultSet rs = null; int retryCount = 5; boolean transactionCompleted = false; do { try { conn = getConnection(); // assume getting this from a // javax.sql.DataSource, or the // java.sql.DriverManager conn.setAutoCommit(false); retryCount = 0; stmt = conn.createStatement(); String query = "SELECT foo FROM bar ORDER BY baz"; rs = stmt.executeQuery(query); while (rs.next()) { } all.close() transactionCompleted = true; } catch (SQLException sqlEx) { String sqlState = sqlEx.getSQLState(); // 这个08S01就是这个异常的sql状态。单独处理手动重新链接就可以了。 if ("08S01".equals(sqlState) || "40001".equals(sqlState)) { retryCount--; } else { retryCount = 0; } } finally { all close: } } while (!transactionCompleted && (retryCount > 0));} }</span>原因2:开启事务与提交事务的时长超出了,连接等待的时长。
于是看了下mysql的设置:SHOW VARIABLES LIKE '%timeout%';重点观察
interactive_timeout
wait_timeout
两个字段如下,可以发现试采用MYSQL默认设置8小时,可以排除此类原因。
mysql> SHOW VARIABLES LIKE '%timeout%'; +-----------------------------+----------+ | Variable_name | Value | +-----------------------------+----------+ | connect_timeout | 10 | | delayed_insert_timeout | 300 | | innodb_flush_log_at_timeout | 1 | | innodb_lock_wait_timeout | 50 | | innodb_rollback_on_timeout | OFF | | interactive_timeout | 28800 | | lock_wait_timeout | 31536000 | | net_read_timeout | 30 | | net_write_timeout | 60 | | rpl_stop_slave_timeout | 31536000 | | slave_net_timeout | 3600 | | wait_timeout | 28800 | +-----------------------------+----------+ 12 rows in set (0.01 sec)
若以上2个字段值很小,可以通过以下方式进行配置。
而mysql的默认设置应给是28800秒,即8小时。
解决办法有两种:一、修改数据库设置
1. 修改配置文件,my.cnf(windows下my.ini),在[mysqld]下加两行interactive_timeout=设置值 wait_timeout=设置的值 然后重启mysql服务
2. 直接在sql命令行里设置 set global interactive_timeout=设置值 set global wait_timeout=设置值 这样的设置好像是只要一重启mysql服务 就会还原的
二、修改应用配置
将事务的操作时间控制在配置范围内。连接池闲置连接关掉设置为0 maxIdle=0。(保证每次获得的连接是最新的,已便有足够的时间).
优化自己的单个事务的处理时长,以便在合理范围内!
结果分析
通过针对原因进行分析对比发现,我这边基本确定试原因1,但是autoReconnect=true既不是一个好的选择,也不是每次都生效的选择,一定还有其他原因和解决方法?进一步分析发现,如果一个DB连接在无任何操作情况下过了8个小时后(Mysql 服务器默认的“wait_timeout”是8小时),Mysql会自动把这个连接关闭。这就是问题的所在,在连接池中的connections如果空闲超过8小时,mysql将其断开,而连接池自己并不知道该connection已经失效,如果这时有 Client请求connection,连接池将该失效的Connection提供给Client,将会造成上面的异常。
所以配置datasource时需要配置相应的连接池参数,定是去检查连接的有效性,定时清理无效的连接。
所以解决方法试添加保活检测:
initial-size: 5 validation-query: SELECT 1 test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800