数据库连接池配置问题处理
这段时间测试经常反馈测试环境APP登录很慢大概有2-3s,用postman调用接口响应时间正常300毫秒左右。
有点奇怪,查看日志,根据日志分析排除了网络波动的原因。
接下来看代码,代码逻辑并不复杂,单表的查询,然后调用下游的授权服务获取用户凭证。
日志有打印调用下游服务的时间,响应很快在10ms内,怀疑目标缩小到单表的SQL,但是表中数据不多不应该出现慢SQL,把代码中的SQL拿出来去数据库查询响应也是很快。
经过一段时间的寻找,最终发现是数据库连接池的问题。
下面是数据库连接池的yaml配置:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
url: @JDBC.url@
username: @JDBC.username@
password: @JDBC.password@
driver-class-name: @JDBC.driverClassName@
#配置初始化连接池大小、最小、最大
initialSize: 3
minIdle: 3
maxActive: 10
#配置获取连接等待超时的时间
maxWait: 10000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 10000
#配置一个连接在池中最小生存的时间,单位是毫秒
# minEvictableIdleTimeMillis : 300000
#用来检测连接是否有效的sql,要求是一个查询语句
validationQuery: SELECT 'x'
#建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testWhileIdle: true
#申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnBorrow: true
#归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn: false
#打开 PSCache ,并且指定每个连接上 PSCache 的大小
poolPreparedStatements: true
#有两个含义:1) Destroy线程会检测连接的间隔时间2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
maxPoolPreparedStatementPerConnectionSize: 20
#配置监控统计拦截的 filters=stat,wall,log4j,config(数据库密码加密使用)
filters: stat,log4j,config
#当程序存在缺陷时,申请的连接忘记关闭,这时候,就存在连接泄漏了,打开removeAbandoned功能
removeAbandoned: true
#1800秒,也就是30分钟
removeAbandonedTimeout: 1800
#关闭abanded连接时输出错误日志
logAbandoned: true
#通过日志输出执行慢的SQL slowSqlMillis的缺省值为3000,也就是3秒,# config.decrypt启用加密,config.decrypt.key配置公钥
# connectionProperties : druid.stat.slowSqlMillis=5000;druid.stat.mergeSql=true;config.decrypt=true;config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKBMc1V3VtFM0cz9D7jZ4CUfNV3qHVYEC3/NP5mGPqQM3NRkhKbT38Xw9FsHGR2Ga2Bf5gohh4uhfvLt8gUQ96ECAwEAAQ==
connectionProperties: druid.stat.slowSqlMillis=5000;druid.stat.mergeSql=true
# 数据库过滤器
filter:
config:
enabled: true
问题很明显idea有提示波浪线,数据库连接池的配置没用生效,原因是DataSourceProperties
中没有对应的属性,具体属性如下图:
可以发现Spring Boot提供的属性类中并不能对连接池进行配置,所以程序运行的后使用的是DruidDataSource
默认的连接池参数,默认值如下:
protected volatile int initialSize = 0; //初始化线程数量
protected volatile int maxActive = 8; //最大活跃线程数量
protected volatile int minIdle = 0; //最小线程数量
protected volatile int maxIdle = 8; //最大空闲线程数量
protected volatile long maxWait = -1L; //最大等待时间
initialSize
是初始化时的线程数目,minIdle
是最小空闲线程数目(线程池会保证始终有指定数目的空闲线程)。
当使用默认的参数时,Spring初始化并不会创建数据库线程,只有当第一个请求发起需要连接数据库时,才会创建线程,并且当程序空闲一段时间后线程会被回收。
当线程池中没有线程,程序处理请求时,需要创建新线程连接数据库,从而导致了个别请求出现了延迟。
问题定位到了,下面来解决,需要配置好数据库线程池,怎么配置了?
看DruidDataSource
的源码,DruidDataSource
继承了DruidAbstractDataSource
,DruidAbstractDataSource
中的属性就是一些数据库连接池的基本配置。
可以看到DruidAbstractDataSource
没有@ConfigurationProperties
注解意味着无法直接使用配置文件来进行配置,可以自己来创建属性类来映射配置文件,然后读取属性类的参数手动注入DruidDataSource
,这样维护起来比较方便,代码如下:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "spring.datasource")
public class DruidProperties {
private String url;
private String username;
private String password;
private String driverClassName;
private int initialSize;
private int minIdle;
private int maxActive;
private long maxWait;
private long timeBetweenEvictionRunsMillis;
private String validationQuery;
private boolean testWhileIdle;
private boolean testOnBorrow;
private boolean testOnReturn;
private boolean poolPreparedStatements;
private int maxPoolPreparedStatementPerConnectionSize;
private String filters;
// Getters and Setters
}
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
@Configuration
@MapperScan(basePackages = {
"com.granwin.device.dao.device",
"com.granwin.device.dao.base",
"com.granwin.device.dao.merchant",
"com.granwin.device.dao.ota"
}, sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfig {
@Autowired
private DruidProperties druidProperties;
@Bean
@Primary
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(druidProperties.getUrl());
dataSource.setUsername(druidProperties.getUsername());
dataSource.setPassword(druidProperties.getPassword());
dataSource.setDriverClassName(druidProperties.getDriverClassName());
dataSource.setInitialSize(druidProperties.getInitialSize());
dataSource.setMinIdle(druidProperties.getMinIdle());
dataSource.setMaxActive(druidProperties.getMaxActive());
dataSource.setMaxWait(druidProperties.getMaxWait());
dataSource.setTimeBetweenEvictionRunsMillis(druidProperties.getTimeBetweenEvictionRunsMillis());
dataSource.setValidationQuery(druidProperties.getValidationQuery());
dataSource.setTestWhileIdle(druidProperties.isTestWhileIdle());
dataSource.setTestOnBorrow(druidProperties.isTestOnBorrow());
dataSource.setTestOnReturn(druidProperties.isTestOnReturn());
dataSource.setPoolPreparedStatements(druidProperties.isPoolPreparedStatements());
dataSource.setMaxPoolPreparedStatementPerConnectionSize(druidProperties.getMaxPoolPreparedStatementPerConnectionSize());
try {
dataSource.setFilters(druidProperties.getFilters());
} catch (Exception e) {
e.printStackTrace();
}
return dataSource;
}
@Bean(name = "sqlSessionFactory")
@Primary
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource());
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:sqlmap/**/*Mapper.xml"));
return bean.getObject();
}
@Bean(name = "sqlSessionTemplate")
@Primary
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}
@Bean(name = "transactionManager")
@Primary
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
}
打包部署后,进行首次请求,响应时间为323ms,说明线程池初始化了线程,正常运行。
总结
Spring Boot
的自动配置帮我们省下了很多事情,出现这次情况就是错误的配置数据库线程池,使用了默认配置导致。
Spring Boot
的约定大于配置,简化开发过程中的决策,减少需要做的配置工作量,甚至不需要配置也能成功运行,导致问题被隐藏。
所以在开发时,需要做到以下几点:
- 仔细检查确保配置生效,通过idea跳转去是否映射到了属性类。
- 使用
Spring Boot
自动配置很方便,但是配置的含义需要花时间去了解,对默认配置的合理性要进行评估。
评论