Spring Boot在1.x.x版本默认使用tomcat连接池,该连接池配置不当时,容易出现如下问题
Connection reset by peer: socket write error
或者
The last packet successfully received from the server was xxx milliseconds ago.
The last packet sent successfully to the server was xxxx milliseconds ago.
自2.0.0版后Spring Boot默认连接池改为HikariCP
出现该异常原因:tomcat连接池testWhileIdle属性默认为false,连接闲置时,并未进行验证,当数据库连接不稳定(网络波动,短暂断网)或者长时间不用连接超时(mysql默认连接8小时未用则超时),池中保有的连接其实已经断开,但由于没有检查,连接池还是认为当前连接可用,当程序调用数据库时,就会抛出异常
解决方案:
解决方法的主题思路是:设置testOnBorrow=true,即当从连接池取连接时先检验连接是否有效
1. 使用spring boot默认配置情况下,一般不会出现该问题(spring自动帮我们设置了tomcat连接池的相关属性),如需定制,使用
spring.datasource.tomcat.*=
tomcat后面配置tomcat连接池中的各种属性,如
spring.datasource.tomcat.testOnBorrow=true
spring.datasource.tomcat.validationQuery=SELECT 1
2. 当自己配置DataSource时,该bug在1.x.x版本为必现bug
- application.properties
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driverClassName=com.mysql.jdbc.Driver
- DataSourceConfig.java
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
}
按照上述配置完成后,最终得到的DataSource里tomcat连接池的配置全是默认值(和spring自动配置时不同),默认情况下tomcat连接池的所有test属性都是false,连接在产生后就不会再进行检测,当我们切断网络,再重连后,连接池中所有连接失效,并且不会自动重连,程序使用连接时抛出异常,该bug极容易被忽略,因为一般情况下不会复现(生产环境网络稳定),唯一可能会暴露该bug的,是数据库的wait_timeout时间(mysql为例),当一个连接8小时未使用时,数据库会自动断开,这时连接池中该连接已经失效,可由于没有验证机制,导致抛出异常,具体表现,应用运行8小时后,会有数据库连接异常报错,甚至如果8小时都没有使用过,会出现根本用不了的情况
修复方法:
- application.properties
#这里以配置两个数据库为例,该配置仅适用于1.x.x,以tomcat为默认连接池的版本
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driverClassName=com.mysql.jdbc.Driver
#重点,注意这里不加tomcat
spring.datasource.testOnBorrow=true
spring.datasource.validationQuery=SELECT 1
datasource.datacenter.url=jdbc:sqlserver://127.0.0.1:3306;DatabaseName=test
datasource.datacenter.username=root
datasource.datacenter.password=root
datasource.datacenter.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
datasource.datacenter.testOnBorrow=true
datasource.datacenter.validationQuery=SELECT 1
- DataSourceConfig
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("datasource.datacenter")
public DataSource dataCenterDataSource() {
return DataSourceBuilder.create().build();
}
}
补充:
DataSourceBuilder在create()的时候,仅仅创建了一个空的DataSourceBuilder
public static DataSourceBuilder create() {
return new DataSourceBuilder(null);
}
该空DataSourceBuilder在build方法
public DataSource build() {
Class<? extends DataSource> type = getType();
DataSource result = BeanUtils.instantiate(type);
maybeGetDriverClassName();
bind(result);
return result;
}
getType()方法返回具体使用哪个连接池,如果我们配置了,就使用配置的,如果没有配置,spring会按顺序判断可用的连接池
private static final String[] DATA_SOURCE_TYPE_NAMES = new String[] {
"org.apache.tomcat.jdbc.pool.DataSource",
"com.zaxxer.hikari.HikariDataSource",
"org.apache.commons.dbcp.BasicDataSource", // deprecated
"org.apache.commons.dbcp2.BasicDataSource" };
当然这是1.x.x版本,拿到具体连接池DataSource的Class后,用BeanUtils.instantiate(type)把该类实例化,后两行为绑定配置信息,当然这里由于我们构建的是个空的DataSourceBuilder,bind的各种属性自然都是空,包括数据库的连接信息,总之到这一步,return的result,除了明确了使用那个连接池,其他配置信息都是空,之后在放入spring容器时,spring会根据@ConfigurationProperties("spring.datasource"),对该类属性进行设置
另外附Tomcat JDBC Connection Pool 配置信息,中文版本