数据库连接池配置问题处理

这段时间测试经常反馈测试环境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中没有对应的属性,具体属性如下图:

image-20240701181340233

可以发现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继承了DruidAbstractDataSourceDruidAbstractDataSource中的属性就是一些数据库连接池的基本配置。

image-20240701200227504

可以看到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自动配置很方便,但是配置的含义需要花时间去了解,对默认配置的合理性要进行评估。