背景:
通过一个接口触发后台数据库的批量更新操作,原本只是一个触发动作,不需要返回值,因此没有关心出现的http超时问题。后面发现批量更新任务中断了,查日志发现了Connection has already been closed报错。
具体的报错信息如下:
首先有一个警告:
24-Aug-2023 11:15:58.527 警告 [Tomcat JDBC Pool Cleaner[571592672:1692844532994]] org.apache.tomcat.jdbc.pool.ConnectionPool.abandon Connection has been abandoned PooledConnection[com.mysql.jdbc.JDBC4Connection@4a3901b4]:java.lang.Exception
at org.apache.tomcat.jdbc.pool.ConnectionPool.getThreadDump(ConnectionPool.java:1102)
at org.apache.tomcat.jdbc.pool.ConnectionPool.borrowConnection(ConnectionPool.java:807)
at org.apache.tomcat.jdbc.pool.ConnectionPool.borrowConnection(ConnectionPool.java:651)
at org.apache.tomcat.jdbc.pool.ConnectionPool.getConnection(ConnectionPool.java:198)
at org.apache.tomcat.jdbc.pool.DataSourceProxy.getConnection(DataSourceProxy.java:132)
at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:111)
at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:77)
at org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:82)
at org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction.java:68)
at org.apache.ibatis.executor.BaseExecutor.getConnection(BaseExecutor.java:336)
at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:84)
at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:62)
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324)
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:83)
at sun.reflect.GeneratedMethodAccessor256.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.ibatis.plugin.Invocation.proceed(Invocation.java:49)
at ...
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
at sun.reflect.GeneratedMethodAccessor261.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
at com.sun.proxy.$Proxy362.selectList(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)
at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:137)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:75)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
at com.sun.proxy.$Proxy463.page(Unknown Source)
at ...
然后就是导致更新操作中断的一个报错:
2023-08-24 11:15:58.797 |[FA18413C-D23E-F8A6-2550-9384CE3F308B]| WARN | Error while extracting database name - falling back to empty error codes | org.springframework.jdbc.support.SQLErrorCodesFactory.getErrorCodes | SQLErrorCodesFactory.java:218
org.springframework.jdbc.support.MetaDataAccessException: Error while extracting DatabaseMetaData; nested exception is java.sql.SQLException: Connection has already been closed.
分析:
根据报错的信息,可以看出是因为执行SQL时获取不到Connection连接,然后去看一下数据库配置中,有三个配置可以关注一下:
配置 默认值 说明
removeAbandoned false 是否强制关闭连接时长大于removeAbandonedTimeoutMillis的连接
removeAbandonedTimeoutMillis 300 * 1000 一个连接从被连接到被关闭之间的最大生命周期
logAbandoned false 强制关闭连接时是否记录日志
看了下我们项目的配置:
1.removeAbandoned是true,代表的意思是,关闭连接时长大于一定时长的连接
2.removeAbandonedTimeout是180,代表从被连接到被关闭之间的最大生命周期是3分钟
3.logAbandoned是true,代表强制关闭连接时记录日志
问题的原因可能就是出现在这里了,这里会循环遍历连接池中的连接,如果存活,就判断是否超过了配置的removeAbandonedTimeout,如果超过了时间,这个连接就要被干掉。因为spring中配置事务时配置的service开启一个事务,在service中拿到连接开启一个事务,而遍历中一直使用注入的dao去调用方法,其本质就是一直使用一个连接,不会遍历一次执行完重新获取连接,导致该连接超时被tomcat关闭回收。
可以尝试的解决方案:
1.适当增大 removeAbandonedTimeout时间,让单次获取的连接能够执行时间更长一点,让其支持更长一点的事务。
2.将所有dao层方法抽出来另放一个业务service层,注入dao层,方法里使用dao的调用方法。在原来的service层中注入业务service,原有dao调用的方法,全部替换成业务service调用的方法。这样每次业务service调用到(update、insert、delete)方法,就开启一个事务,执行完就回收。再执行就有获取一个连接,执行到事务方法后,又会主动关闭,就不会因连接超时被tomcat强行回收了!