java-springboot 1.x - jdbc tomcat 连接池配置

环境

  • jdk 1.8
  • springboot 1.56
  • mybatis 3.5.3

背景

看到经常有人错误的配置连接池参数,或则把springboot1.0、2.0的配置写混淆,在此特别针对 springboot 1.0如何配置和使用默认的tomcat连接池进行一下说明。

如何在bootstrap.yml中,正确配置和使用tomcat连接池?

直接在string.datasource下面配置连接池的参数,是不正确的,配置也不会生效。
注:以下内容粘贴到yml配置文件中,其中 — 为新配置文件的开始标记;spring.profiles 可以指定自己环境参数;

---
spring.profiles: default
spring:
  datasource:
    tomcat :
      initialSize: 1  #池启动时打开的连接数
      maxActive: 20    #打开的最大连接数
      maxIdle: 5      #最大空闲连接数
      minIdle: 1      #打开连接的最小数量
      maxWait: 30000  #获取连接时抛出SQLException之前等待的时间(以毫秒为单位)
      testOnBorrow: false   #如果在请求连接时进行验证,则为True
      testOnReturn: false   #如果在返回连接时进行验证,则为True
      testWhileIdle: true   #如果验证在连接未使用(空闲)时发生,则为True
      timeBetweenEvictionRunsMillis: 60000  #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      minEvictableIdleTimeMillis: 300000  #配置一个连接在池中最小生存的时间,单位是毫秒(与 timeBetweenEvictionRunsMillis 配合使用)
      validationQueryTimeout: 1000    #连接验证查询失败前的超时(以秒为单位)
      validationQuery: select 1 AS count    #检测连接是否有效的SQL

tomcat连接池的默认配置

连接池代码用到的关键对象

  • 默认配置:org.apache.tomcat.jdbc.pool.PoolProperties;
  • 所在包:tomcat-jdbc-8.5.16.jar
  • 配置项说明详见:tomcat-jdbc-8.5.16.jar!\org\apache\tomcat\jdbc\pool\mbeans-descriptors.xml
  • 连接池对象:org.apache.tomcat.jdbc.pool.ConnectionPool
  • 获取链接方法:org.apache.tomcat.jdbc.pool.ConnectionPool#getConnection()
public static final int DEFAULT_MAX_ACTIVE = 100;
    private volatile int defaultTransactionIsolation = DataSourceFactory.UNKNOWN_TRANSACTIONISOLATION;
    private volatile int initialSize = 10;
    private volatile int maxActive = DEFAULT_MAX_ACTIVE;
    private volatile int maxIdle = maxActive;
    private volatile int minIdle = initialSize;
    private volatile int maxWait = 30000;
    private volatile String validationQuery;
    private volatile int validationQueryTimeout = -1;
    private volatile String validatorClassName;
    private volatile Validator validator;
    private volatile boolean testOnBorrow = false;
    private volatile boolean testOnReturn = false;
    private volatile boolean testWhileIdle = false;
    private volatile int timeBetweenEvictionRunsMillis = 5000;
    private volatile int numTestsPerEvictionRun;
    private volatile int minEvictableIdleTimeMillis = 60000;
    private volatile boolean accessToUnderlyingConnectionAllowed = true;
    private volatile boolean removeAbandoned = false;
    private volatile int removeAbandonedTimeout = 60;
    private volatile boolean logAbandoned = false;
    private volatile String password;
    private volatile String username;
    private volatile long validationInterval = 3000;
    private volatile boolean jmxEnabled = true;
    private volatile String initSQL;
    private volatile boolean testOnConnect =false;
    private volatile boolean fairQueue = true;
    private volatile boolean useEquals = true;
    private volatile int abandonWhenPercentageFull = 0;
    private volatile long maxAge = 0;
    private volatile boolean useLock = false;
    private volatile int suspectTimeout = 0;
    private volatile boolean alternateUsernameAllowed = false;
    private volatile boolean commitOnReturn = false;
    private volatile boolean rollbackOnReturn = false;
    private volatile boolean useDisposableConnectionFacade = true;
    private volatile boolean logValidationErrors = false;
    private volatile boolean propagateInterruptState = false;
    private volatile boolean ignoreExceptionOnPreLoad = false;
    private volatile boolean useStatementFacade = true;

配置说明

  • maxActive=“100”

    表示并发情况下最大可从连接池中获取的连接数。
  • maxIdle=“30”

    如果在并发时达到了maxActive=100,那么连接池就必须从数据库中获取100个连接来供应用程序使用,当应用程序关闭连接后,由于maxIdle=30,因此并不是所有的连接都会归还给数据库,将会有30个连接保持在连接池种中,状态为空闲。
    maxIdle对应的连接,实际上是连接池保持的长连接,这也是连接池发挥优势的部分,理论上讲保持较多的长连接,在应用请求时可以更快的响应,但是过多的连接保持,反而会消耗数据库大量的资源,因此maxIdle也并不是越大越好
  • minIdle=”2”

    最小默认情况下并不生效,它的含义是当连接池中的连接少有minIdle,系统监控线程将启动补充功能,一般情况下我们并不启动补充线程。
  • removeAbandoned=“true”

    超过时间限制是否回收
  • removeAbandonedTimeout=“60”

    超时时间;单位为秒
  • logAbandoned=“true”

    关闭abanded连接时输出错误日志
    有时粗心的程序编写者在从连接池中获取连接使用后忘记了连接的关闭,这样连池的连接就会逐渐达到 maxActive 直至连接池无法提供服务。现代连接池一般提供一种“智能”的检查,但设置了 removeAbandoned=“true” 时,当连接池连接数到达(getNumIdle() < 2) and (getNumActive() > getMaxActive() - 3)时便会启动连接回收,那种活动时间超过 removeAbandonedTimeout=“60” 的连接将会被回收,同时如果 logAbandoned=“true” 设置为true,程序在回收连接的同时会打印日志。 removeAbandoned 是连接池的高级功能,理论上这中配置不应该出现在实际的生产环境,因为有时应用程序执行长事务,可能这种情况下,会被连接池误回收,该种配置一般在程序测试阶段,为了定位连接泄漏的具体代码位置,被开启,生产环境中连接的关闭应该靠程序自己保证。
    一般会是几种情况出现需要removeAbandoned: 代码未在finally释放connection , 不过我们都用sqlmapClientTemplate,底层都有链接释放的过程
    遇到数据库死锁。以前遇到过后端存储过程做了锁表操作,导致前台集群中连接池全都被block住,后续的业务处理因为拿不到链接所有都处理失败了。
  • initialSize

    连接池启动时创建的初始化连接数量(默认值为0)
  • maxWait

    最大等待时间,当没有可用连接时,连接池等待连接释放的最大时间,超过该时间限制会抛出异常,如果设置-1表示无限等待(默认为无限,调整为60000ms,避免因线程池不够用,而导致请求被无限制挂起)
  • poolPreparedStatements

    开启池的prepared(默认是false,未调整,经过测试,开启后的性能没有关闭的好。)
  • maxOpenPreparedStatements

    开启池的prepared 后的同时最大连接数(默认无限制,同上,未配置)
  • minEvictableIdleTimeMillis

    连接池中连接,在时间段内一直空闲,被逐出连接池的时间(默认为30分钟,可以适当做调整,需要和后端服务端的策略配置相关)
  • minEvictableIdleTimeMillis

    连接池中连接可空闲的时间,毫秒
  • timeBetweenEvictionRunsMillis

    设置的Evict线程的时间,单位ms,大于0才会开启evict检查线程
    timeBetweenEvictionRunsMillis和minEvictableIdleTimeMillis一起使用
    每timeBetweenEvictionRunsMillis毫秒秒检查一次连接池中空闲的连接,把空闲时间超过minEvictableIdleTimeMillis毫秒的连接断开,直到连接池中的连接数到minIdle为止
  • testOnBorrow

    顾明思义,就是在进行borrowObject进行处理时,对拿到的connection进行validateObject校验
  • testOnReturn

    顾明思义,就是在进行returnObject对返回的connection进行validateObject校验,个人觉得对数据库连接池的管理意义不大
  • testWhileIdle

    关注的重点,GenericObjectPool中针对pool管理,起了一个Evict的TimerTask定时线程进行控制(可通过设置参数timeBetweenEvictionRunsMillis>0),定时对线程池中的链接进行validateObject校验,对无效的链接进行关闭后,会调用ensureMinIdle,适当建立链接保证最小的minIdle连接数
  • validateQuery

    代表检查的sql,用来检查连接是否有效的sql,要求是一个查询语句,如果validateQuery为null,则testOnBorrow、testOnReturn、testWhileIdle 都不会起作用
  • validateQueryTimeout

    代表在执行检查时,通过statement设置,statement.setQueryTimeout(validationQueryTimeout)
  • numTestsPerEvictionRun

    代表每次检查链接的数量,建议设置和maxActive一样大,这样每次可以有效检查所有的链接.

如何验证连接池参数配置是否生效?

注入一个 datasource 对象,打印下对象内容,即可验证参数是否生效。

@Autowired
private DataSource ds;

public void test() throws IOException {
    System.out.println(ds.getClass());
    System.out.println(ds.toString());
}

看看几个关键属性是否和配置的一致:
关键属性为:maxActive=3; maxIdle=2; minIdle=1; initialSize=1; maxWait=3000;

class org.apache.tomcat.jdbc.pool.DataSource
org.apache.tomcat.jdbc.pool.DataSource@2123a61c{ConnectionPool[defaultAutoCommit=null; defaultReadOnly=null; defaultTransactionIsolation=-1; defaultCatalog=null; driverClassName=org.postgresql.Driver; maxActive=3; maxIdle=2; minIdle=1; initialSize=1; maxWait=3000; testOnBorrow=false; testOnReturn=false; timeBetweenEvictionRunsMillis=60000; numTestsPerEvictionRun=0; minEvictableIdleTimeMillis=300000; testWhileIdle=true; testOnConnect=false; password=********; url=********; validationQuery=select 1 AS count; validationQueryTimeout=1000; validatorClassName=null; validationInterval=3000; accessToUnderlyingConnectionAllowed=true; removeAbandoned=false; removeAbandonedTimeout=60; logAbandoned=false; connectionProperties=null; initSQL=null; jdbcInterceptors=null; jmxEnabled=true; fairQueue=true; useEquals=true; abandonWhenPercentageFull=0; maxAge=0; useLock=false; dataSource=null; dataSourceJNDI=null; suspectTimeout=0; alternateUsernameAllowed=false; commitOnReturn=false; rollbackOnReturn=false; useDisposableConnectionFacade=true; logValidationErrors=false; propagateInterruptState=false; ignoreExceptionOnPreLoad=false; useStatementFacade=true; }

pgsql连接池无法分配链接原因排查

异常信息

org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is org.apache.tomcat.jdbc.pool.PoolExhaustedException: [Thread-15] Timeout: Pool empty. Unable to fetch a connection in 3 seconds, none available[size:1; busy:1; idle:0; lastwait:2999].

通过系统表查看已有的链接和正在执行的SQL

在系统发生上述异常是,打开控制台执行脚本:

select * from pg_stat_activity;

其中的query列为正在执行的SQL;分析正在执行的SQL,一般可以定位到触发此SQL的业务代码;

总结

连接池配置需要考虑几个因素,如:数据库的最大连接数是多少?有几个微服务需要访问这个DB?每个微服务部署了几个实例?等等。

  • 比如我们只使用一个DB实例,最大连接数为1600;
  • 集群部署了60个实例,都需要访问数据库;
  • 那么每个服务的maxActive配置应该小于26(1600/20);
    在实际配置时,还需要考虑为扩容预留的链接等情况,所以maxActive=20是一个比较合理的配置;