彻底解决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个参数设置正确,缺一个都无法自动重连
- jdbc connection url中需要有:autoReconnect=true&useUnicode=true&characterEncoding=utf-8
- minIdle值与initialSize相同
- maxIdle最大空闲值与initalSize和minIdle相同
- testOnBorrow设为false
- testOnReturn设为true
- testWhileIdle设为true
- timeBetweenEvictionRunsMillis设为480000 #代表8分钟,我们生产上的的mysql的timeout为10分钟,因此这个值必须<生产上的mysql的timeout的值
- 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个;
- 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个参数设置全解释
- jdbc connection url中需要有:autoReconnect=true&useUnicode=true&characterEncoding=utf-8
- minIdle值与initialSize相同
- maxIdle最大空闲值与initalSize和minIdle相同
- maxWait连接超时统一可以设成=5000(5秒),正式生产环境调优的好一般是设成1秒的,这就是为什么生产上高于1秒的sql必须要优化的道理
-
minEvictableIdleTimeMillis: 480000 #小于mysql maxinteractive的值(奥乐齐生产这边是10分钟)
- maxEvictableIdleTimeMillis: 600000 #必须于mysql maxinteractive的值(奥乐齐生产这边是10分钟)
- keepAliveBetweenTimeMillis: 540000 #必须小于mysql maxinteractive的值(奥乐齐生产这边是10分钟)
- testOnBorrow设为false
- testOnReturn设为true
- keepAlive:设为true
- testWhileIdle设为true
- validationQuery设为select 1
- validationQueryTimeout设为1
- validationInterval设为3000
- conRetryTime设为3
- dynamicPropertiesSupport设为false #这个值是什么意思, 欧电对若干mybatis用tk.mabatis.mapper中的dynamic进行了封装,如果你的pool内的dao层有用到extend BaseMapperProvider(这个是欧电底层包装过的),这个值就必须设成false,一定注意
- timeBetweenEvictionRunsMillis设为480000 #代表8分钟,我们的DBwait 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个;
- minEvictableIdleTimeMillis=480000 #代表8分钟,我们的DBwait timeout都设的是8分钟,这个值必须等于timeBetweenEvictionRunsMillis值,如果不设这个值在使用common dbcp的pool里照样不能实现错误重连
以上是关于彻底解决jdbc数据库连接超时重试-communication link failure的正确姿势的主要内容,如果未能解决你的问题,请参考以下文章
MySQL数据库连接重试功能和连接超时功能的DB连接Python实现
用jdbc连接mysql数据库,执行程序一段时间后控制台报连接超时。why,如何解决!
使用 Serverless Aurora 时 Sequelize 连接超时,寻找增加超时持续时间或重试连接的方法