彻底解决jdbc数据库连接超时重试-communication link failure的正确姿势

Posted TGITCIC

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了彻底解决jdbc数据库连接超时重试-communication link failure的正确姿势相关的知识,希望对你有一定的参考价值。

必须确保你的druid jdbc版本为1.2.6

这个问题只有在druid 1.2.6里解决,因为只要是低于druid 1.2.6版本,本身就存在bug,无论你怎么设都会打断连接。

背景

我们经常会在日志中看到“jdbc connection timeout, last connection was 11,080 ms这样的错误。

这个代表mysql主动把你的jdbc连接给踢掉了。

为什么MYSQL要踢掉connection?

这个很正常,那是因为:MYSQL不可能无限接在接受一个数据库连接请求后就一直给你KEEP在那边的。生产环境都是这么一个“梗”,试想一个connection里的一个sql的迪卡尔积有10mb,2000个边接keep在那8小时不关闭,这个mysql会被随时打爆。

在2001年古老的oracle 7g、8i时代,就已经有一个参数叫timeout,根据oracle创始人Larry Ellison和“杀死比尔”-哦,错了,是比尔.盖茨对这个参数解释是:1分钟。即数据库keep住你的一个连接1分钟,这1分钟内如果没有任何的重复连接那么该连接会被数据库踢掉。

这就是生产数据库的“自我保护”机制,这是必须的。这也是为什么我强调大家你的“线上SQL单条执行必须<1s“的道理,因为你试想一下在国内的场景,动不动一秒内来1000个并发?是不是这样的?

MYSQL的默认超时是8小时,这个值肯定是不能接受的。在奥乐齐这边我们考虑到实际业务场景和开发成熟度,目前这个超时设的是10分钟,即10分钟内这条connection连接过来后没有进一步动作,该connection会被踢除,这个规则已经很“宽容”了。

那么这就要求我们的JAVA应用具有自动重连功能

网上的教程统统都是错的

我可以说100%都是错的,我们在一个企业级的应用中,JDBC连接的参数至少有19个,这19个每一个都是非常重要的,而真正涉及到“自动重连”有三层境界。

自动重连的第一层镜界

确保以下9个参数设置正确,缺一个都无法自动重连

  1. jdbc connection url中需要有:autoReconnect=true&useUnicode=true&characterEncoding=utf-8
  2. minIdle值与initialSize相同
  3. maxIdle最大空闲值与initalSize和minIdle相同
  4. testOnBorrow设为false
  5. testOnReturn设为true
  6. testWhileIdle设为true
  7. timeBetweenEvictionRunsMillis设为480000 #代表8分钟,我们生产上的的mysql的timeout为10分钟,因此这个值必须<生产上的mysql的timeout的值
  8. numTestsPerEvictionRun设为10 #默认为3,这个值在common dbcb或者是非druid connection pool的情况下必须按照数据库内设置的timeout和每隔多少个timeBetweenEvictionRunsMillis以及initalSize来计算的,否则会发生在若干时间(1小时后)全部连接超时后,而initSize由于>此值,因此导致(initSize-numTestsPerEvictionRun)无效,进而导致系统上在获取到无效连接时抛错。举例来说数据库的timeout值为10分钟,initialSize为50个,timeBetweenEvictionRunsMillis为5分钟,那么此时这个值你要设为(timeBetweenEvictionRunsMillis/数据库timeout)*initialSize+1个,在此例中=(8/10)*50+1=41个,为了保险期间,你可以把这个值在此例中设为:45个
  9. minEvictableIdleTimeMillis设为480000 #代表8分钟,我们生产上的的mysql的timeout为10分钟,因此这个值必须<生产上的mysql的timeout的值

自动重连的第二层镜界

假设我们以上9个参数全部设置了,可是还有一个问题。

假设我们的连接中有一个“长事务”,这个事务一直在运行,你不能硬生生用mysql里设置的timeout 10分钟去硬生生打断正在运行中的事务吧?此时我们要赋于我们的jdbc connection pool一个自动续约的的功能,怎么设这个自动续约呢?

  • maxEvictableIdleTimeMillis: 600000 #等于数据失效时间
  • keepAlive: true
  • keepAliveBetweenTimeMillis: 540000 #保活时间即<数据库失效时间

此处需要高度注意的是:APACHE的DBCP数据库连接池是不支持keepalive这个参数的。没有这个参数其它两个参数都设了无效,这也是为什么我们后期准备把apache dbcp改成阿里的druid。

自动重连的第三层镜界

你以为把上面这些东西我们一股脑的都设置进去后就可以了?如下面这样

mysql:
  datasource:
    master: #master db
      type: com.alibaba.druid.pool.DruidDataSource
      driverClassName: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/aldi_api_gateway?useUnicode=true&characterEncoding=utf-8&useSSL=false&useAffectedRows=true&autoReconnect=true
      username: aldiwrite
      password: 111111
      minIdle: 50
      initialSize: 50
      maxActive: 100
      maxWait: 5000
      testOnBorrow: true
      testOnReturn: true
      testWhileIdle: true
      validationQuery: select 1
      validationQueryTimeout: 1
      timeBetweenEvictionRunsMillis: 480000
      ConnectionErrorRetryAttempts: 3
      NotFullTimeoutRetryCount: 3
      #removeAbandonedTimeoutMillis: 480000
      #removeAbandoned: true
      numTestsPerEvictionRun: 3
      minEvictableIdleTimeMillis: 480000
      maxEvictableIdleTimeMillis: 600000
      keepAliveBetweenTimeMillis: 540000
      keepalive: true
    slaver1: #slaver db
      type: com.alibaba.druid.pool.DruidDataSource
      driverClassName: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3307/aldi_api_gateway?useUnicode=true&characterEncoding=utf-8&useSSL=false&useAffectedRows=true&autoReconnect=true
      username: aldiread
      password: 111111
      minIdle: 50
      initialSize: 50
      maxActive: 100
      maxWait: 5000
      testOnBorrow: true
      testOnReturn: true
      testWhileIdle: true
      validationQuery: select 1
      validationQueryTimeout: 1
      timeBetweenEvictionRunsMillis: 480000
      ConnectionErrorRetryAttempts: 3
      NotFullTimeoutRetryCount: 3
      #removeAbandonedTimeoutMillis: 480000
      ​#removeAbandoned: true
      minEvictableIdleTimeMillis: 480000
      maxEvictableIdleTimeMillis: 600000
      keepAliveBetweenTimeMillis: 540000
      keepalive: true
      numTestsPerEvictionRun: 3

错!还不够!

这只是配置,还没有让代码生效呢。怎么让代码生效?你得写spring boot 的自动装配类

package org.mk.demo.db.config;

import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import com.alibaba.druid.pool.DruidDataSource;

@Configuration
public class MultiDSConfig {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Value("${mysql.datasource.master.url}")
    private String masterUrl;
    @Value("${mysql.datasource.master.username}")
    private String username;
    @Value("${mysql.datasource.master.password}")
    private String password;
    @Value("${mysql.datasource.master.driverClassName}")
    private String driverClassName;
    @Value("${mysql.datasource.master.initialSize}")
    private int initialSize;
    @Value("${mysql.datasource.master.minIdle}")
    private int minIdle;
    @Value("${mysql.datasource.master.maxActive}")
    private int maxActive;
    @Value("${mysql.datasource.master.maxWait}")
    private int maxWait;
    // 故意注释
    @Value("${mysql.datasource.master.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;
    @Value("${mysql.datasource.master.validationQuery}")
    private String validationQuery;
    @Value("${mysql.datasource.master.testWhileIdle}")
    private boolean testWhileIdle;
    @Value("${mysql.datasource.master.testOnBorrow}")
    private boolean testOnBorrow;
    @Value("${mysql.datasource.master.testOnReturn}")
    private boolean testOnReturn;
    @Value("${mysql.datasource.master.ConnectionErrorRetryAttempts}")
    private int connectionErrorRetryAttempts;
    @Value("${mysql.datasource.master.NotFullTimeoutRetryCount}")
    private int notFullTimeoutRetryCount;
    // @Value("${mysql.datasource.master.removeAbandonedTimeoutMillis}")
    // private long removeAbandonedTimeoutMillis;
    // @Value("${mysql.datasource.master.removeAbandoned}")
    // private boolean removeAbandoned;
    @Value("${mysql.datasource.master.keepalive}")
    private boolean keepAlive = true;
    @Value("${mysql.datasource.master.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;
    @Value("${mysql.datasource.master.maxEvictableIdleTimeMillis}")
    private int maxEvictableIdleTimeMillis;
    @Value("${mysql.datasource.master.keepAliveBetweenTimeMillis}")
    private int keepAliveBetweenTimeMillis;

    @Bean
    @ConfigurationProperties(prefix = "mysql.datasource.master")
    public DataSource masterDataSource() {
        DruidDataSource datasource = new DruidDataSource();
        datasource.setUrl(masterUrl);
        datasource.setUsername(username);
        datasource.setPassword(password);
        datasource.setDriverClassName(driverClassName);
        // configuration
        datasource.setInitialSize(initialSize);
        datasource.setMinIdle(minIdle);
        datasource.setMaxActive(maxActive);
        datasource.setMaxWait(maxWait);
        // 故意注释下面这一句
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        logger.info(">>>>>>timeBetweenEvictionRunsMillis->" + timeBetweenEvictionRunsMillis);
        // datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setValidationQuery(validationQuery);
        datasource.setTestWhileIdle(testWhileIdle);
        datasource.setTestOnBorrow(testOnBorrow);
        datasource.setTestOnReturn(testOnReturn);
        // datasource.setPoolPreparedStatements(poolPreparedStatements);
        // datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
        datasource.setConnectionErrorRetryAttempts(connectionErrorRetryAttempts);
        datasource.setNotFullTimeoutRetryCount(notFullTimeoutRetryCount);
        //datasource.setRemoveAbandonedTimeoutMillis(removeAbandonedTimeoutMillis);
        //datasource.setRemoveAbandoned(removeAbandoned);
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
        datasource.setKeepAliveBetweenTimeMillis(keepAliveBetweenTimeMillis);
        datasource.setKeepAlive(keepAlive);
        return datasource;
    }

    @Bean
    @ConfigurationProperties(prefix = "mysql.datasource.slaver1")
    public DataSource slave1DataSource() {
        return new DruidDataSource();
    }

    @Bean
    public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
        @Qualifier("slave1DataSource") DataSource slave1DataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
        targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource);

        MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
        myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
        myRoutingDataSource.setTargetDataSources(targetDataSources);
        return myRoutingDataSource;
    }

    @Bean
    public JdbcTemplate dataSource(DataSource myRoutingDataSource) {
        return new JdbcTemplate(myRoutingDataSource);
    }

    @Bean
    public DataSourceTransactionManager txManager(DataSource myRoutingDataSource) {
        return new DataSourceTransactionManager(myRoutingDataSource);
    }
}

到此,三重境界经历过后整个JAVA应用的jdbc才具有了真正的“自动重连”功能。

骚年,程序员不好做哈!

附:核心19个参数设置全解释

  1. jdbc connection url中需要有:autoReconnect=true&useUnicode=true&characterEncoding=utf-8
  2. minIdle值与initialSize相同
  3. maxIdle最大空闲值与initalSize和minIdle相同
  4. maxWait连接超时统一可以设成=5000(5秒),正式生产环境调优的好一般是设成1秒的,这就是为什么生产上高于1秒的sql必须要优化的道理
  5. minEvictableIdleTimeMillis: 480000 #小于mysql maxinteractive的值(奥乐齐生产这边是10分钟)

  6. maxEvictableIdleTimeMillis: 600000 #必须于mysql maxinteractive的值(奥乐齐生产这边是10分钟)
  7. keepAliveBetweenTimeMillis: 540000 #必须小于mysql maxinteractive的值(奥乐齐生产这边是10分钟)
  8. testOnBorrow设为false
  9. testOnReturn设为true
  10. keepAlive:设为true
  11. testWhileIdle设为true
  12. validationQuery设为select 1
  13. validationQueryTimeout设为1
  14. validationInterval设为3000
  15. conRetryTime设为3
  16. dynamicPropertiesSupport设为false #这个值是什么意思, 欧电对若干mybatis用tk.mabatis.mapper中的dynamic进行了封装,如果你的pool内的dao层有用到extend BaseMapperProvider(这个是欧电底层包装过的),这个值就必须设成false,一定注意
  17. timeBetweenEvictionRunsMillis设为480000 #代表8分钟,我们的DBwait timeout都设的是8分钟
  18. numTestsPerEvictionRun设为10 #默认为3,这个值在common dbcb或者是非druid connection pool的情况下必须按照数据库内设置的timeout和每隔多少个timeBetweenEvictionRunsMillis以及initalSize来计算的,否则会发生在若干时间(1小时后)全部连接超时后,而initSize由于>此值,因此导致(initSize-numTestsPerEvictionRun)无效,进而导致系统上在获取到无效连接时抛错。举例来说数据库的timeout值为10分钟,initialSize为50个,timeBetweenEvictionRunsMillis为5分钟,那么此时这个值你要设为(timeBetweenEvictionRunsMillis/数据库timeout)*initialSize+1个,在此例中=(8/10)*50+1=41个,为了保险期间,你可以把这个值在此例中设为:45个
  19. minEvictableIdleTimeMillis=480000 #代表8分钟,我们的DBwait timeout都设的是8分钟,这个值必须等于timeBetweenEvictionRunsMillis值,如果不设这个值在使用common dbcp的pool里照样不能实现错误重连

以上是关于彻底解决jdbc数据库连接超时重试-communication link failure的正确姿势的主要内容,如果未能解决你的问题,请参考以下文章

MySQL数据库连接重试功能和连接超时功能的DB连接Python实现

用jdbc连接mysql数据库,执行程序一段时间后控制台报连接超时。why,如何解决!

使用 Serverless Aurora 时 Sequelize 连接超时,寻找增加超时持续时间或重试连接的方法

Nacos Config连接超时重试

连接数据库超时设置autoReconnect=true,默认重试次数调整

连接数据库超时设置autoReconnect=true,默认重试次数调整