1.问题描述
程序在测试环境和生产环境运行都没有问题,但生产数据库的数据出了问题,排查服务器日志发现抛出以下异常信息(截选):
[2021-07-07 17:32:25][ERROR][PlatformMappingExceptionResolver.java Line:27 (doResolveException)] org.springframework.dao.RecoverableDataAccessException:
### Error querying database. Cause: java.sql.SQLRecoverableException: No more data to read from socket
### The error may exist in file [/u01/FQM/apps/FQM/WEB-INF/classes/com/qdairlines/mapperfoc/AirportMapper.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: SELECT AIRPORT_4CODE code4, AIRPORT_3CODE code3, CITY_CH_NAME cityName, AIRPORT_CH_NAME airportName, IS_SPECIAL special FROM T7001 where ? like '%' || AIRPORT_4CODE ||'%'
### Cause: java.sql.SQLRecoverableException: No more data to read from socket
; SQL []; No more data to read from socket; nested exception is java.sql.SQLRecoverableException: No more data to read from socket
at org.springframework.jdbc.support.SQLExceptionSubclassTranslator.doTranslate(SQLExceptionSubclassTranslator.java:98)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:73)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:371)
at com.sun.proxy.$Proxy14.selectList(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:198)
at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:119)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:63)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:52)
at com.sun.proxy.$Proxy185.ListAirportView(Unknown Source)
at com.qdairlines.service.airport.impl.AirportServiceImpl.ListAirportView(AirportServiceImpl.java:48)
at com.qdairlines.controller.dict.Select2Controller.airportView(Select2Controller.java:189)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
2.问题原因
因为使用了连接池,当从连接池取得的connection失效或者超时的时候,使用这个连接来进行数据库操作就会抛出以上异常。
3.解决方案
让数据库连接池在给你返回connection之前,检查该connnection是否超时或者失效,如果是,则evict这个connection,并返回一个可用的connection。
具体配置如下:
<bean id="dataSourceSC" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<!-- 驱动名 -->
<property name="driverClassName" value="${jdbc.driverClassName}" />
<!-- 数据库url -->
<property name="url" value="${jdbc.url}" />
<!-- 用户名 -->
<property name="username" value="${jdbc.username}" />
<!-- 密码 -->
<property name="password" value="${jdbc.password}" />
<!-- 初试连接数 -->
<property name="initialSize" value="10" />
<!-- 最大活跃数 -->
<property name="maxActive" value="30" />
<!-- 最大空闲数 -->
<property name="maxIdle" value="15" />
<!-- 最小空闲数 -->
<property name="minIdle" value="5" />
<!-- 程序中的连接不使用后是否被连接池回收 -->
<property name="removeAbandoned" value="true" />
<!-- 连接在所指定的秒数内未使用才会被删除(秒) -->
<property name="removeAbandonedTimeout" value="60" />
<!-- 最长等待时间 -->
<property name="maxWait" value="10000" />
<!-- 表示在回收连接的时候,在日志中输出回收的连接的信息,包括是在哪用的这个连接,可以用来追踪连接溢出 -->
<property name="logAbandoned" value="true" />
<!-- 检查该connnection是否超时或者失效 -->
<!-- 如果为true(默认为false),当应用向连接池申请连接时,连接池会判断这条连接是否是可用的 -->
<property name="testOnBorrow">
<value>true</value>
</property>
<!-- 如果为true(默认为false),当应用向连接池申请连接时,连接池会判断这条连接是否是可用的 -->
<property name="testOnReturn">
<value>true</value>
</property>
<!-- 如果为true(默认true),当应用向连接池申请连接,并且testOnBorrow为false时,连接池将会判断连接是否处于空闲状态,如果是,则验证这条连接是否可用 -->
<property name="testWhileIdle">
<value>true</value>
</property>
<!-- 连接在池中保持空闲而不被空闲连接回收器线程,(如果有)回收的最小时间值,单位毫秒 -->
<property name="minEvictableIdleTimeMillis">
<value>180000</value>
</property>
<!-- 在空闲连接回收器线程运行期间休眠的时间值,以毫秒为单位.如果设置为非正数,则不运行空闲连接回收器线程 -->
<property name="timeBetweenEvictionRunsMillis">
<value>360000</value>
</property>
<!-- SQL查询,用来验证从连接池取出的连接,在将连接返回给调用者之前.如果指定,则查询必须是一个SQL SELECT并且必须返回至少一行记录 -->
<property name="validationQuery">
<value>SELECT 1 FROM SYS.DUAL</value>
</property>
</bean>
4.相关知识
4.1 数据库链接,常见的问题
<1> 数据库意外重启后,原先的数据库连接池能自动废弃老的无用的链接,建立新的数据库链接
<2> 网络异常中断后,原先的建立的 tcp 链接,应该能进行自动切换。比如网站演习中的交换机重启会导致网络瞬断
<3> 分布式数据库中间件,比如 cobar 会定时的将空闲链接异常关闭,客户端会出现半开的空闲链接。
4.2 常见问题解决思路
<1> sql 心跳检查 ( 主动式 )
<2> 拿链接尝试一下,发现处理失败丢弃链接,探雷的请求会失败几个 ( 牺牲小我,完成大我的精神 )
<3> 设置合理的空闲链接的超时时间,避免半开链接 ( 懒模式,解决半开链接 )
4.3 Sql 心跳检查几点思考
4.3.1 性能问题
目前网站的应用大部分的瓶颈还是在I/O这一块,大部分的I/O还是在数据库的这一层面上,每一个请求可能会调用10来次SQL查询,如果不走事务,一个请求会重复获取链接,如果每次获取链接,比如在testOnBorrow都进行validateObject,性能开销不是很能接受,可以假定一次SQL操作消毫0.5~1ms(一般走了网络请求基本就这数)
4.3.2 成本和收益
网站异常数据库重启,网络异常断开的频率是非常低的,一般也就在数据库升级,演习维护时才会进行,而且一般也是选在晚上,访问量相对比较低的请求,而且一般会有人员值班关注,所以异步的validateObject是可以接受,但一个前提需要确保能保证在一个合理的时间段内,数据库能完成自动重联。
5.参考资料:
c3p0官方文档:http://www.mchange.com/projects/c3p0/index.html#configuring_recovery