开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 15 天,点击查看活动详情
大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈
前言
Druid是阿里开源的数据库连接池,是阿里监控系统Dragoon的副产品,提供了强大的可监控性和基于Filter-Chain的可扩展性。
本篇文章将对Druid数据库连接池的初始化进行分析。
正文
DruidDataSource初始化有两种方式。
- 将DruidDataSource实例创建出来后,主动调用其init() 方法完成初始化;
- 首次调用DruidDataSource的getConnection() 方法时,会调用到init() 方法完成初始化。
由于init() 方法过长,下面将分点介绍init() 方法完成的关键事情。
一. 双重检查inited状态
对inited状态进行Double Check,防止DruidDataSource初始化两次。源码示意如下。
public void init() throws SQLException {
if (inited) {
return;
}
// 省略
final ReentrantLock lock = this.lock;
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
throw new SQLException("interrupt", e);
}
boolean init = false;
try {
if (inited) {
return;
}
// 省略
} catch (SQLException e) {
// 省略
} catch (InterruptedException e) {
// 省略
} catch (RuntimeException e) {
// 省略
} catch (Error e) {
// 省略
} finally {
inited = true;
lock.unlock();
// 省略
}
}
二. 判断数据库类型
根据jdbcUrl得到数据库类型dbTypeName。源码如下所示。
if (this.dbTypeName == null || this.dbTypeName.length() == 0) {
this.dbTypeName = JdbcUtils.getDbType(jdbcUrl, null);
}
三. 参数校验
对一些关键参数进行校验。源码如下所示。
// 连接池最大连接数量不能小于等于0
if (maxActive <= 0) {
throw new IllegalArgumentException("illegal maxActive " + maxActive);
}
// 连接池最大连接数量不能小于最小连接数量
if (maxActive < minIdle) {
throw new IllegalArgumentException("illegal maxActive " + maxActive);
}
// 连接池初始连接数量不能大于最大连接数量
if (getInitialSize() > maxActive) {
throw new IllegalArgumentException("illegal initialSize " + this.initialSize + ", maxActive " + maxActive);
}
// 不允许同时开启基于日志手段记录连接池状态和全局状态监控
if (timeBetweenLogStatsMillis > 0 && useGlobalDataSourceStat) {
throw new IllegalArgumentException("timeBetweenLogStatsMillis not support useGlobalDataSourceStat=true");
}
// 连接最大空闲时间不能小于连接最小空闲时间
if (maxEvictableIdleTimeMillis < minEvictableIdleTimeMillis) {
throw new SQLException("maxEvictableIdleTimeMillis must be grater than minEvictableIdleTimeMillis");
}
// 不允许开启了保活机制但保活间隔时间小于等于回收检查时间间隔
if (keepAlive && keepAliveBetweenTimeMillis <= timeBetweenEvictionRunsMillis) {
throw new SQLException("keepAliveBetweenTimeMillis must be grater than timeBetweenEvictionRunsMillis");
}
四. SPI机制加载过滤器
调用到DruidDataSource#initFromSPIServiceLoader方法,基于SPI机制加载过滤器Filter。源码如下所示。
private void initFromSPIServiceLoader() {
if (loadSpifilterSkip) {
return;
}
if (autoFilters == null) {
List<Filter> filters = new ArrayList<Filter>();
// 基于ServiceLoader加载Filter
ServiceLoader<Filter> autoFilterLoader = ServiceLoader.load(Filter.class);
// 遍历加载的每一个Filter,根据@AutoLoad注解的属性判断是否加载该Filter
for (Filter filter : autoFilterLoader) {
AutoLoad autoLoad = filter.getClass().getAnnotation(AutoLoad.class);
if (autoLoad != null && autoLoad.value()) {
filters.add(filter);
}
}
autoFilters = filters;
}
// 将每个需要加载的Filter添加到filters字段中,并去重
for (Filter filter : autoFilters) {
if (LOG.isInfoEnabled()) {
LOG.info("load filter from spi :" + filter.getClass().getName());
}
addFilter(filter);
}
}
五. 加载驱动
调用DruidDataSource#resolveDriver方法,根据配置的驱动名称加载数据库驱动。源码如下所示。
protected void resolveDriver() throws SQLException {
if (this.driver == null) {
// 若没有配置驱动名则尝试从jdbcUrl中获取
if (this.driverClass == null || this.driverClass.isEmpty()) {
this.driverClass = JdbcUtils.getDriverClassName(this.jdbcUrl);
}
// Mock驱动相关
if (MockDriver.class.getName().equals(driverClass)) {
driver = MockDriver.instance;
} else if ("com.alibaba.druid.support.clickhouse.BalancedClickhouseDriver"
.equals(driverClass)) {
// ClickHouse相关
Properties info = new Properties();
info.put("user", username);
info.put("password", password);
info.putAll(connectProperties);
driver = new BalancedClickhouseDriver(jdbcUrl, info);
} else {
if (jdbcUrl == null && (driverClass == null || driverClass.length() == 0)) {
throw new SQLException("url not set");
}
// 加载驱动
driver = JdbcUtils.createDriver(driverClassLoader, driverClass);
}
} else {
if (this.driverClass == null) {
this.driverClass = driver.getClass().getName();
}
}
}
六. 初始化连接有效性校验器
调用DruidDataSource#initValidConnectionChecker方法,初始化ValidConnectionChecker,用于校验某个连接是否可用。源码如下所示。
private void initValidConnectionChecker() {
if (this.validConnectionChecker != null) {
return;
}
String realDriverClassName = driver.getClass().getName();
// 不同的数据库初始化不同的ValidConnectionChecker
if (JdbcUtils.isMySqlDriver(realDriverClassName)) {
// MySQL数据库还支持使用ping的方式来校验连接活性,这比执行一条简单查询语句来判活更高效
// 由usePingMethod参数决定是否开启
this.validConnectionChecker = new MySqlValidConnectionChecker(usePingMethod);
} else if (realDriverClassName.equals(JdbcConstants.ORACLE_DRIVER)
|| realDriverClassName.equals(JdbcConstants.ORACLE_DRIVER2)) {
this.validConnectionChecker = new OracleValidConnectionChecker();
} else if (realDriverClassName.equals(JdbcConstants.SQL_SERVER_DRIVER)
|| realDriverClassName.equals(JdbcConstants.SQL_SERVER_DRIVER_SQLJDBC4)
|| realDriverClassName.equals(JdbcConstants.SQL_SERVER_DRIVER_JTDS)) {
this.validConnectionChecker = new MSSQLValidConnectionChecker();
} else if (realDriverClassName.equals(JdbcConstants.POSTGRESQL_DRIVER)
|| realDriverClassName.equals(JdbcConstants.ENTERPRISEDB_DRIVER)
|| realDriverClassName.equals(JdbcConstants.POLARDB_DRIVER)) {
this.validConnectionChecker = new PGValidConnectionChecker();
} else if (realDriverClassName.equals(JdbcConstants.OCEANBASE_DRIVER)
|| (realDriverClassName.equals(JdbcConstants.OCEANBASE_DRIVER2))) {
DbType dbType = DbType.of(this.dbTypeName);
this.validConnectionChecker = new OceanBaseValidConnectionChecker(dbType);
}
}
七. 初始化全局状态统计器
如果useGlobalDataSourceStat设置为true,则初始化全局状态统计器,用于统计和分析数据库连接池的性能数据。源码片段如下所示。
if (isUseGlobalDataSourceStat()) {
dataSourceStat = JdbcDataSourceStat.getGlobal();
if (dataSourceStat == null) {
dataSourceStat = new JdbcDataSourceStat("Global", "Global", this.dbTypeName);
JdbcDataSourceStat.setGlobal(dataSourceStat);
}
if (dataSourceStat.getDbType() == null) {
dataSourceStat.setDbType(this.dbTypeName);
}
} else {
dataSourceStat = new JdbcDataSourceStat(this.name, this.jdbcUrl, this.dbTypeName, this.connectProperties);
}
八. 初始化连接池数组并预热
创建三个连接池数组,分别是connections(用于存放能获取的连接对象),evictConnections(用于存放需要丢弃的连接对象)和keepAliveConnections(用于存放需要保活的连接对象)。连接池的预热有两种,如果配置了asyncInit为true,且异步线程池不为空,则执行异步连接池预热,反之执行同步连接池预热。
// 用于存放能获取的连接对象,真正意义上的连接池
// 已经被获取的连接不在其中
connections = new DruidConnectionHolder[maxActive];
// 用于存放需要被关闭丢弃的连接
evictConnections = new DruidConnectionHolder[maxActive];
// 用于存放需要保活的连接
keepAliveConnections = new DruidConnectionHolder[maxActive];
SQLException connectError = null;
// 有线程池且异步初始化配置为true,则异步预热
if (createScheduler != null && asyncInit) {
for (int i = 0; i < initialSize; ++i) {
submitCreateTask(true);
}
} else if (!asyncInit) {
// 同步预热,预热连接数由initialSize配置
while (poolingCount < initialSize) {
try {
PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
// 对DruidDataSource和Connection做了一层封装
DruidConnectionHolder holder
= new DruidConnectionHolder(this, pyConnectInfo);
connections[poolingCount++] = holder;
} catch (SQLException ex) {
LOG.error("init datasource error, url: " + this.getUrl(), ex);
if (initExceptionThrow) {
connectError = ex;
break;
} else {
Thread.sleep(3000);
}
}
}
if (poolingCount > 0) {
poolingPeak = poolingCount;
poolingPeakTime = System.currentTimeMillis();
}
}
九. 创建日志记录线程并启动
调用DruidDataSource#createAndLogThread方法创建通过打印日志来记录连接池状态的线程。createAndLogThread() 方法如下所示。
private void createAndLogThread() {
// timeBetweenLogStatsMillis小于等于0表示不开启打印日志记录连接池状态的功能
if (this.timeBetweenLogStatsMillis <= 0) {
return;
}
String threadName = "Druid-ConnectionPool-Log-" + System.identityHashCode(this);
// 创建线程
logStatsThread = new LogStatsThread(threadName);
// 启动线程
logStatsThread.start();
this.resetStatEnable = false;
}
createAndLogThread() 方法会创建LogStatsThread并启动,即会调用到LogStatsThread的run() 方法。LogStatsThread线程的run() 方法如下所示。
public void run() {
try {
for (; ; ) {
try {
// 每间隔timeBetweenLogStatsMillis就打印一次连接池状态
logStats();
} catch (Exception e) {
LOG.error("logStats error", e);
}
Thread.sleep(timeBetweenLogStatsMillis);
}
} catch (InterruptedException e) {
}
}
上述run() 方法中会每间隔timeBetweenLogStatsMillis的时间就调用一次logStats() 方法来打印连接池状态。logStats() 方法如下所示。
public void logStats() {
final DruidDataSourceStatLogger statLogger = this.statLogger;
if (statLogger == null) {
return;
}
// 拿到各种连接池的状态
DruidDataSourceStatValue statValue = getStatValueAndReset();
// 打印
statLogger.log(statValue);
}
在logStats() 方法中会先调用getStatValueAndReset() 方法来拿到各种连接池的状态,然后调用DruidDataSourceStatLogger完成打印。最后看一眼getStatValueAndReset() 方法里面拿哪些连接池状态,getStatValueAndReset() 方法代码片段如下所示。
public DruidDataSourceStatValue getStatValueAndReset() {
DruidDataSourceStatValue value = new DruidDataSourceStatValue();
lock.lock();
try {
value.setPoolingCount(this.poolingCount);
value.setPoolingPeak(this.poolingPeak);
value.setPoolingPeakTime(this.poolingPeakTime);
value.setActiveCount(this.activeCount);
value.setActivePeak(this.activePeak);
value.setActivePeakTime(this.activePeakTime);
value.setConnectCount(this.connectCount);
value.setCloseCount(this.closeCount);
value.setWaitThreadCount(lock.getWaitQueueLength(notEmpty));
value.setNotEmptyWaitCount(this.notEmptyWaitCount);
value.setNotEmptyWaitNanos(this.notEmptyWaitNanos);
value.setKeepAliveCheckCount(this.keepAliveCheckCount);
// 重置参数
this.poolingPeak = 0;
this.poolingPeakTime = 0;
this.activePeak = 0;
this.activePeakTime = 0;
this.connectCount = 0;
this.closeCount = 0;
this.keepAliveCheckCount = 0;
this.notEmptyWaitCount = 0;
this.notEmptyWaitNanos = 0;
} finally {
lock.unlock();
}
value.setName(this.getName());
value.setDbType(this.dbTypeName);
value.setDriverClassName(this.getDriverClassName());
// 省略
value.setSqlSkipCount(this.getDataSourceStat().getSkipSqlCountAndReset());
value.setSqlList(this.getDataSourceStat().getSqlStatMapAndReset());
return value;
}
十. 创建创建连接的线程并启动
调用DruidDataSource#createAndStartCreatorThread方法来创建创建连接的线程CreateConnectionThread并启动。createAndStartCreatorThread() 方法如下所示。
protected void createAndStartCreatorThread() {
// 只有异步创建连接的线程池为空时,才创建CreateConnectionThread
if (createScheduler == null) {
String threadName = "Druid-ConnectionPool-Create-"
+ System.identityHashCode(this);
createConnectionThread = new CreateConnectionThread(threadName);
// 启动线程
createConnectionThread.start();
return;
}
initedLatch.countDown();
}
CreateConnectionThread只有在异步创建连接的线程池createScheduler为空时,才会被创建出来,并且在CreateConnectionThread的run() 方法一开始,就会调用initedLatch的countDown() 方法,其中initedLatch是一个初始值为2的CountDownLatch对象,另外一次countDown() 调用在DestroyConnectionThread的run() 方法中,目的就是init() 方法执行完以前,创建连接的线程和销毁连接的线程一定要创建出来并启动完毕。
createAndLogThread();
// 在内部会调用到initedLatch.countDown()
createAndStartCreatorThread();
// 在内部最终会调用initedLatch.countDown()
createAndStartDestroyThread();
initedLatch.await();
十一. 创建销毁连接的线程并启动
调用DruidDataSource#createAndStartDestroyThread方法来创建销毁连接的线程DestroyConnectionThread并启动。createAndStartDestroyThread() 方法如下所示。
protected void createAndStartDestroyThread() {
// 销毁连接的任务
destroyTask = new DestroyTask();
// 如果销毁连接的线程池不会为空,则让其周期执行销毁连接的任务
if (destroyScheduler != null) {
long period = timeBetweenEvictionRunsMillis;
if (period <= 0) {
period = 1000;
}
destroySchedulerFuture = destroyScheduler
.scheduleAtFixedRate(destroyTask, period, period,
TimeUnit.MILLISECONDS);
initedLatch.countDown();
return;
}
// 如果销毁连接的线程池为空,则创建销毁连接的线程
String threadName = "Druid-ConnectionPool-Destroy-" + System.identityHashCode(this);
destroyConnectionThread = new DestroyConnectionThread(threadName);
// 启动线程
destroyConnectionThread.start();
}
createAndStartDestroyThread() 方法中会先判断销毁连接的线程池是否存在,如果存在,则不再创建DestroyConnectionThread,而是会让销毁连接的线程池来执行销毁任务,如果不存在,则创建DestroyConnectionThread并启动,此时initedLatch的countDown() 调用是在DestroyConnectionThread的run() 方法中。DestroyConnectionThread#run方法源码如下所示。
public void run() {
// run()方法只要执行了,就调用initedLatch#countDown
initedLatch.countDown();
for (; ; ) {
// 每间隔timeBetweenEvictionRunsMillis执行一次DestroyTask的run()方法
try {
if (closed || closing) {
break;
}
if (timeBetweenEvictionRunsMillis > 0) {
Thread.sleep(timeBetweenEvictionRunsMillis);
} else {
Thread.sleep(1000);
}
if (Thread.interrupted()) {
break;
}
// 执行DestroyTask的run()方法来销毁需要销毁的线程
destroyTask.run();
} catch (InterruptedException e) {
break;
}
}
}
DestroyConnectionThread#run方法只要被调用到,那么就会调用initedLatch的countDown() 方法,此时阻塞在init() 方法中的initedLatch.await() 方法上的线程就会被唤醒并继续往下执行。
总结
Druid数据库连接池初始化时,会通过双重检查锁的方式来防止初始化两次。然后会完成如下事情。
- 判断出数据库类型;
- 参数校验。检验不过会抛出异常;
- 通过SPI机制加载过滤器;
- 加载数据库驱动;
- 初始化连接有效性校验器。例如testOnBorrow,testWhileIdle等场景就会用这个校验器去校验连接;
- 初始化全局状态统计器;
- 初始化连接池数组并预热。
- 创建日志记录线程并启动。如果已经开启了全局状态统计器,则这里的日志记录线程不会创建;
- 创建创建连接的线程并启动;
- 创建销毁连接的线程并启动。
大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 15 天,点击查看活动详情