搞懂Druid之连接池初始化

1,625 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 15 天,点击查看活动详情

大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈


前言

Druid是阿里开源的数据库连接池,是阿里监控系统Dragoon的副产品,提供了强大的可监控性和基于Filter-Chain的可扩展性。

本篇文章将对Druid数据库连接池的初始化进行分析。

正文

DruidDataSource初始化有两种方式。

  1. DruidDataSource实例创建出来后,主动调用其init() 方法完成初始化;
  2. 首次调用DruidDataSourcegetConnection() 方法时,会调用到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(用于存放需要保活的连接对象)。连接池的预热有两种,如果配置了asyncInittrue,且异步线程池不为空,则执行异步连接池预热,反之执行同步连接池预热。

// 用于存放能获取的连接对象,真正意义上的连接池
// 已经被获取的连接不在其中
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并启动,即会调用到LogStatsThreadrun() 方法。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为空时,才会被创建出来,并且在CreateConnectionThreadrun() 方法一开始,就会调用initedLatchcountDown() 方法,其中initedLatch是一个初始值为2的CountDownLatch对象,另外一次countDown() 调用在DestroyConnectionThreadrun() 方法中,目的就是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并启动,此时initedLatchcountDown() 调用是在DestroyConnectionThreadrun() 方法中。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方法只要被调用到,那么就会调用initedLatchcountDown() 方法,此时阻塞在init() 方法中的initedLatch.await() 方法上的线程就会被唤醒并继续往下执行。

总结

Druid数据库连接池初始化时,会通过双重检查锁的方式来防止初始化两次。然后会完成如下事情。

  1. 判断出数据库类型;
  2. 参数校验。检验不过会抛出异常;
  3. 通过SPI机制加载过滤器;
  4. 加载数据库驱动;
  5. 初始化连接有效性校验器。例如testOnBorrowtestWhileIdle等场景就会用这个校验器去校验连接;
  6. 初始化全局状态统计器;
  7. 初始化连接池数组并预热。
  8. 创建日志记录线程并启动。如果已经开启了全局状态统计器,则这里的日志记录线程不会创建;
  9. 创建创建连接的线程并启动;
  10. 创建销毁连接的线程并启动。

大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 15 天,点击查看活动详情