資料庫連線池-Druid資料庫連線池原始碼解析

半夏之沫發表於2023-01-02

前言

本文將對Druid資料庫連線池的原始碼進行分析和學習,以瞭解Druid資料庫連線池的工作原理。Druid資料庫連線池的基本邏輯幾乎全部在DruidDataSource類中,所以本文主要是圍繞DruidDataSource的各項功能展開論述。

Druid版本:1.2.11

正文

一. DruidDataSource初始化

DruidDataSource初始化有兩種方式,如下所示。

  • DruidDataSource例項建立出來後,主動呼叫其init()方法完成初始化;
  • 首次呼叫DruidDataSourcegetConnection()方法時,會呼叫到init()方法完成初始化。

由於init()方法過長,下面將分點介紹init()方法完成的關鍵事情。

1. 雙重檢查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();
        
        ......
        
    }
}

2. 判斷資料庫型別

根據jdbcUrl得到資料庫型別dbTypeName。原始碼如下所示。

if (this.dbTypeName == null || this.dbTypeName.length() == 0) {
    this.dbTypeName = JdbcUtils.getDbType(jdbcUrl, null);
}

3. 引數校驗

對一些關鍵引數進行校驗。原始碼如下所示。

// 連線池最大連線數量不能小於等於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");
}

4. 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);
    }
}

5. 載入驅動

呼叫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();
        }
    }
}

6. 初始化連線有效性校驗器

呼叫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);
    }

}

7. 初始化全域性狀態統計器

如果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);
}

8. 初始化連線池陣列並預熱

建立三個連線池陣列,分別是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();
    }
}

9. 建立日誌記錄執行緒並啟動

呼叫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;
}

10. 建立建立連線的執行緒並啟動

呼叫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();

11. 建立銷燬連線的執行緒並啟動

呼叫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()方法上的執行緒就會被喚醒並繼續往下執行。

二. DruidDataSource連線建立

DruidDataSource連線的建立由CreateConnectionThread執行緒完成,其run()方法如下所示。

public void run() {
    initedLatch.countDown();

    long lastDiscardCount = 0;
    int errorCount = 0;
    for (; ; ) {
        try {
            lock.lockInterruptibly();
        } catch (InterruptedException e2) {
            break;
        }

        long discardCount = DruidDataSource.this.discardCount;
        boolean discardChanged = discardCount - lastDiscardCount > 0;
        lastDiscardCount = discardCount;

        try {
            // emptyWait為true表示生產連線執行緒需要等待,無需生產連線
            boolean emptyWait = true;

            // 發生了建立錯誤,且池中已無連線,且丟棄連線的統計沒有改變
            // 此時生產連線執行緒需要生產連線
            if (createError != null
                    && poolingCount == 0
                    && !discardChanged) {
                emptyWait = false;
            }

            if (emptyWait
                    && asyncInit && createCount < initialSize) {
                emptyWait = false;
            }

            if (emptyWait) {
                // 池中已有連線數大於等於正在等待連線的應用執行緒數
                // 且當前是非keepAlive場景
                // 且當前是非連續失敗
                // 此時生產連線的執行緒在empty上等待
                // keepAlive && activeCount + poolingCount < minIdle時會在shrink()方法中觸發emptySingal()來新增連線
                // isFailContinuous()返回true表示連續失敗,即多次(預設2次)建立物理連線失敗
                if (poolingCount >= notEmptyWaitThreadCount
                        && (!(keepAlive && activeCount + poolingCount < minIdle))    
                        && !isFailContinuous()    
                ) {
                    empty.await();
                }

                // 防止建立超過maxActive數量的連線
                if (activeCount + poolingCount >= maxActive) {
                    empty.await();
                    continue;
                }
            }

        } catch (InterruptedException e) {
            ......
        } finally {
            lock.unlock();
        }

        PhysicalConnectionInfo connection = null;

        try {
            connection = createPhysicalConnection();
        } catch (SQLException e) {
            LOG.error("create connection SQLException, url: " + jdbcUrl + ", errorCode " + e.getErrorCode()
                    + ", state " + e.getSQLState(), e);

            errorCount++;
            if (errorCount > connectionErrorRetryAttempts && timeBetweenConnectErrorMillis > 0) {
                // 多次建立失敗
                setFailContinuous(true);
                // 如果配置了快速失敗,就喚醒所有在notEmpty上等待的應用執行緒
                if (failFast) {
                    lock.lock();
                    try {
                        notEmpty.signalAll();
                    } finally {
                        lock.unlock();
                    }
                }

                if (breakAfterAcquireFailure) {
                    break;
                }

                try {
                    Thread.sleep(timeBetweenConnectErrorMillis);
                } catch (InterruptedException interruptEx) {
                    break;
                }
            }
        } catch (RuntimeException e) {
            LOG.error("create connection RuntimeException", e);
            setFailContinuous(true);
            continue;
        } catch (Error e) {
            LOG.error("create connection Error", e);
            setFailContinuous(true);
            break;
        }

        if (connection == null) {
            continue;
        }

        // 把連線新增到連線池
        boolean result = put(connection);
        if (!result) {
            JdbcUtils.close(connection.getPhysicalConnection());
            LOG.info("put physical connection to pool failed.");
        }

        errorCount = 0;

        if (closing || closed) {
            break;
        }
    }
}

CreateConnectionThreadrun()方法整體就是在一個死迴圈中不斷的等待,被喚醒,然後建立執行緒。當一個物理連線被建立出來後,會呼叫DruidDataSource#put方法將其放到連線池connections中,put()方法原始碼如下所示。

protected boolean put(PhysicalConnectionInfo physicalConnectionInfo) {
    DruidConnectionHolder holder = null;
    try {
        holder = new DruidConnectionHolder(DruidDataSource.this, physicalConnectionInfo);
    } catch (SQLException ex) {
        ......
        return false;
    }

    return put(holder, physicalConnectionInfo.createTaskId, false);
}

private boolean put(DruidConnectionHolder holder, long createTaskId, boolean checkExists) {
    // 涉及到連線池中連線數量改變的操作,都需要加鎖
    lock.lock();
    try {
        if (this.closing || this.closed) {
            return false;
        }

        // 池中已有連線數已經大於等於最大連線數,則不再把連線加到連線池並直接返回false
        if (poolingCount >= maxActive) {
            if (createScheduler != null) {
                clearCreateTask(createTaskId);
            }
            return false;
        }

        // 檢查重複新增
        if (checkExists) {
            for (int i = 0; i < poolingCount; i++) {
                if (connections[i] == holder) {
                    return false;
                }
            }
        }

        // 連線放入連線池
        connections[poolingCount] = holder;
        // poolingCount++
        incrementPoolingCount();

        if (poolingCount > poolingPeak) {
            poolingPeak = poolingCount;
            poolingPeakTime = System.currentTimeMillis();
        }

        // 喚醒在notEmpty上等待連線的應用執行緒
        notEmpty.signal();
        notEmptySignalCount++;

        if (createScheduler != null) {
            clearCreateTask(createTaskId);

            if (poolingCount + createTaskCount < notEmptyWaitThreadCount
                    && activeCount + poolingCount + createTaskCount < maxActive) {
                emptySignal();
            }
        }
    } finally {
        lock.unlock();
    }
    return true;
}

put()方法會先將物理連線從PhysicalConnectionInfo中獲取出來並封裝成一個DruidConnectionHolderDruidConnectionHolder就是Druid連線池中的連線。新新增的連線會存放在連線池陣列connectionspoolingCount位置,然後poolingCount會加1,也就是poolingCount代表著連線池中可以獲取的連線的數量。

三. DruidDataSource連線銷燬

DruidDataSource連線的建立由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;
        }
    }
}

DestroyConnectionThreadrun()方法就是在一個死迴圈中每間隔timeBetweenEvictionRunsMillis的時間就執行一次DestroyTaskrun()方法。DestroyTask#run方法實現如下所示。

public void run() {
    // 根據一系列條件判斷並銷燬連線
    shrink(true, keepAlive);

    // RemoveAbandoned機制
    if (isRemoveAbandoned()) {
        removeAbandoned();
    }
}

DestroyTask#run方法中會呼叫DruidDataSource#shrink方法來根據設定的條件來判斷出需要銷燬和保活的連線。DruidDataSource#shrink方法如下所示。

// checkTime參數列示在將一個連線進行銷燬前,是否需要判斷一下空閒時間
public void shrink(boolean checkTime, boolean keepAlive) {
    // 加鎖
    try {
        lock.lockInterruptibly();
    } catch (InterruptedException e) {
        return;
    }

    // needFill = keepAlive && poolingCount + activeCount < minIdle
    // needFill為true時,會呼叫empty.signal()喚醒生產連線的執行緒來生產連線
    boolean needFill = false;
    // evictCount記錄需要銷燬的連線數
    // keepAliveCount記錄需要保活的連線數
    int evictCount = 0;
    int keepAliveCount = 0;
    int fatalErrorIncrement = fatalErrorCount - fatalErrorCountLastShrink;
    fatalErrorCountLastShrink = fatalErrorCount;

    try {
        if (!inited) {
            return;
        }

        // checkCount = 池中已有連線數 - 最小空閒連線數
        // 正常情況下,最多能夠將前checkCount個連線進行銷燬
        final int checkCount = poolingCount - minIdle;
        final long currentTimeMillis = System.currentTimeMillis();
        // 正常情況下,需要遍歷池中所有連線
        // 從前往後遍歷,i為陣列索引
        for (int i = 0; i < poolingCount; ++i) {
            DruidConnectionHolder connection = connections[i];

            // 如果發生了致命錯誤(onFatalError == true)且致命錯誤發生時間(lastFatalErrorTimeMillis)在連線建立時間之後
            // 把連線加入到保活連線陣列中
            if ((onFatalError || fatalErrorIncrement > 0) && (lastFatalErrorTimeMillis > connection.connectTimeMillis)) {
                keepAliveConnections[keepAliveCount++] = connection;
                continue;
            }

            if (checkTime) {
                // phyTimeoutMillis表示連線的物理存活超時時間,預設值是-1
                if (phyTimeoutMillis > 0) {
                    // phyConnectTimeMillis表示連線的物理存活時間
                    long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
                    // 連線的物理存活時間大於phyTimeoutMillis,則將這個連線放入evictConnections陣列
                    if (phyConnectTimeMillis > phyTimeoutMillis) {
                        evictConnections[evictCount++] = connection;
                        continue;
                    }
                }

                // idleMillis表示連線的空閒時間
                long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;

                // minEvictableIdleTimeMillis表示連線允許的最小空閒時間,預設是30分鐘
                // keepAliveBetweenTimeMillis表示保活間隔時間,預設是2分鐘
                // 如果連線的空閒時間小於minEvictableIdleTimeMillis且還小於keepAliveBetweenTimeMillis
                // 則connections陣列中當前連線之後的連線都會滿足空閒時間小於minEvictableIdleTimeMillis且還小於keepAliveBetweenTimeMillis
                // 此時跳出遍歷,不再檢查其餘的連線
                if (idleMillis < minEvictableIdleTimeMillis
                        && idleMillis < keepAliveBetweenTimeMillis
                ) {
                    break;
                }

                // 連線的空閒時間大於等於允許的最小空閒時間
                if (idleMillis >= minEvictableIdleTimeMillis) {
                    if (checkTime && i < checkCount) {
                        // i < checkCount這個條件的理解如下:
                        // 每次shrink()方法執行時,connections陣列中只有索引0到checkCount-1的連線才允許被銷燬
                        // 這樣才能保證銷燬完連線後,connections陣列中至少還有minIdle個連線
                        evictConnections[evictCount++] = connection;
                        continue;
                    } else if (idleMillis > maxEvictableIdleTimeMillis) {
                        // 如果空閒時間過久,已經大於了允許的最大空閒時間(預設7小時)
                        // 那麼無論如何都要銷燬這個連線
                        evictConnections[evictCount++] = connection;
                        continue;
                    }
                }

                // 如果開啟了保活機制,且連線空閒時間大於等於了保活間隔時間
                // 此時將連線加入到保活連線陣列中
                if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) {
                    keepAliveConnections[keepAliveCount++] = connection;
                }
            } else {
                // checkTime為false,那麼前checkCount個連線直接進行銷燬,不再判斷這些連線的空閒時間是否超過閾值
                if (i < checkCount) {
                    evictConnections[evictCount++] = connection;
                } else {
                    break;
                }
            }
        }

        // removeCount = 銷燬連線數 + 保活連線數
        // removeCount表示本次從connections陣列中拿掉的連線數
        // 注:一定是從前往後拿,正常情況下最後minIdle個連線是安全的
        int removeCount = evictCount + keepAliveCount;
        if (removeCount > 0) {
            // [0, 1, 2, 3, 4, null, null, null] -> [3, 4, 2, 3, 4, null, null, null]
            System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
            // [3, 4, 2, 3, 4, null, null, null] -> [3, 4, null, null, null, null, null, null, null]
            Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
            // 更新池中連線數
            poolingCount -= removeCount;
        }
        keepAliveCheckCount += keepAliveCount;

        // 如果池中連線數加上活躍連線數(借出去的連線)小於最小空閒連線數
        // 則將needFill設為true,後續需要喚醒生產連線的執行緒來生產連線
        if (keepAlive && poolingCount + activeCount < minIdle) {
            needFill = true;
        }
    } finally {
        lock.unlock();
    }

    if (evictCount > 0) {
        // 遍歷evictConnections陣列,銷燬其中的連線
        for (int i = 0; i < evictCount; ++i) {
            DruidConnectionHolder item = evictConnections[i];
            Connection connection = item.getConnection();
            JdbcUtils.close(connection);
            destroyCountUpdater.incrementAndGet(this);
        }
        Arrays.fill(evictConnections, null);
    }

    if (keepAliveCount > 0) {
        // 遍歷keepAliveConnections陣列,對其中的連線做可用性校驗
        // 校驗透過連線就放入connections陣列,沒透過連線就銷燬
        for (int i = keepAliveCount - 1; i >= 0; --i) {
            DruidConnectionHolder holer = keepAliveConnections[i];
            Connection connection = holer.getConnection();
            holer.incrementKeepAliveCheckCount();

            boolean validate = false;
            try {
                this.validateConnection(connection);
                validate = true;
            } catch (Throwable error) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("keepAliveErr", error);
                }
            }

            boolean discard = !validate;
            if (validate) {
                holer.lastKeepTimeMillis = System.currentTimeMillis();
                boolean putOk = put(holer, 0L, true);
                if (!putOk) {
                    discard = true;
                }
            }

            if (discard) {
                try {
                    connection.close();
                } catch (Exception e) {
                
                }

                lock.lock();
                try {
                    discardCount++;

                    if (activeCount + poolingCount <= minIdle) {
                        emptySignal();
                    }
                } finally {
                    lock.unlock();
                }
            }
        }
        this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount);
        Arrays.fill(keepAliveConnections, null);
    }

    // 如果needFill為true則喚醒生產連線的執行緒來生產連線
    if (needFill) {
        lock.lock();
        try {
            // 計算需要生產連線的個數
            int fillCount = minIdle - (activeCount + poolingCount + createTaskCount);
            for (int i = 0; i < fillCount; ++i) {
                emptySignal();
            }
        } finally {
            lock.unlock();
        }
    } else if (onFatalError || fatalErrorIncrement > 0) {
        lock.lock();
        try {
            emptySignal();
        } finally {
            lock.unlock();
        }
    }
}

DruidDataSource#shrink方法中,核心邏輯是遍歷connections陣列中的連線,並判斷這些連線是需要銷燬還是需要保活。通常情況下,connections陣列中的前checkCount(checkCount = poolingCount - minIdle)個連線是“危險”的,因為這些連線只要滿足了:空閒時間 >= minEvictableIdleTimeMillis(允許的最小空閒時間),那麼就需要被銷燬,而connections陣列中的最後minIdle個連線是“相對安全”的,因為這些連線只有在滿足:空閒時間 > maxEvictableIdleTimeMillis(允許的最大空閒時間)時,才會被銷燬。這麼判斷的原因,主要就是需要讓連線池裡能夠保證至少有minIdle個空閒連線可以讓應用執行緒獲取。

當確定好了需要銷燬和需要保活的連線後,此時會先將connections陣列清理,只保留安全的連線,這個過程示意圖如下。

最後,會遍歷evictConnections陣列,銷燬陣列中的連線,遍歷keepAliveConnections陣列,對其中的每個連線做可用性校驗,如果校驗可用,那麼就重新放回connections陣列,否則銷燬。

四. DruidDataSource連線獲取

DruidDataSource獲取連線的入口方法是DruidDataSource#getConnection方法,實現如下。

public DruidPooledConnection getConnection() throws SQLException {
    // maxWait表示獲取連線時最大等待時間,單位毫秒,預設值為-1
    return getConnection(maxWait);
}

public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
    // 首次獲取連線時觸發資料庫連線池初始化
    init();

    if (filters.size() > 0) {
        FilterChainImpl filterChain = new FilterChainImpl(this);
        return filterChain.dataSource_connect(this, maxWaitMillis);
    } else {
        // 直接獲取連線
        return getConnectionDirect(maxWaitMillis);
    }
}

DruidDataSource#getConnection方法會呼叫到DruidDataSource#getConnectionDirect方法來獲取連線,實現如下所示。

public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
    int notFullTimeoutRetryCnt = 0;
    for (; ; ) {
        DruidPooledConnection poolableConnection;
        try {
            // 從連線池拿到連線
            poolableConnection = getConnectionInternal(maxWaitMillis);
        } catch (GetConnectionTimeoutException ex) {
            // 拿連線時有異常,可以重試
            // 重試次數由notFullTimeoutRetryCount指定
            if (notFullTimeoutRetryCnt <= this.notFullTimeoutRetryCount && !isFull()) {
                notFullTimeoutRetryCnt++;
                if (LOG.isWarnEnabled()) {
                    LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);
                }
                continue;
            }
            throw ex;
        }

        // 如果配置了testOnBorrow = true,那麼每次拿到連線後,都需要校驗這個連線的有效性
        if (testOnBorrow) {
            boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
            // 如果連線不可用,則銷燬連線,然後重新從池中獲取
            if (!validate) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("skip not validate connection.");
                }

                discardConnection(poolableConnection.holder);
                continue;
            }
        } else {
            if (poolableConnection.conn.isClosed()) {
                discardConnection(poolableConnection.holder);
                continue;
            }

            // 如果配置testOnBorrow = fasle但testWhileIdle = true
            // 則判斷連線空閒時間是否大於等於timeBetweenEvictionRunsMillis
            // 如果是,則校驗連線的有效性
            if (testWhileIdle) {
                final DruidConnectionHolder holder = poolableConnection.holder;
                long currentTimeMillis = System.currentTimeMillis();
                // lastActiveTimeMillis是連線最近一次活躍時間
                // 新建連線,歸還連線到連線池,都會更新這個時間
                long lastActiveTimeMillis = holder.lastActiveTimeMillis;
                // lastExecTimeMillis是連線最近一次執行時間
                // 新建連線,設定連線的事務是否自動提交,記錄SQL到事務資訊中,都會更新這個時間
                long lastExecTimeMillis = holder.lastExecTimeMillis;
                // lastKeepTimeMillis是連線最近一次保活時間
                // 在連線被保活並放回連線池時,會更新這個時間
                long lastKeepTimeMillis = holder.lastKeepTimeMillis;

                // 如果配置checkExecuteTime為true,則最近活躍時間取值為最近執行時間
                if (checkExecuteTime
                        && lastExecTimeMillis != lastActiveTimeMillis) {
                    lastActiveTimeMillis = lastExecTimeMillis;
                }

                // 如果連線最近一次做的操作是保活,那麼最近活躍時間取值為最近保活時間
                if (lastKeepTimeMillis > lastActiveTimeMillis) {
                    lastActiveTimeMillis = lastKeepTimeMillis;
                }

                // 計算空閒時間
                long idleMillis = currentTimeMillis - lastActiveTimeMillis;

                // testWhileIdle為true時的判斷時間間隔
                long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;

                if (timeBetweenEvictionRunsMillis <= 0) {
                    // timeBetweenEvictionRunsMillis如果小於等於0,那麼重置為60秒
                    timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
                }

                // 如果空閒時間大於等於timeBetweenEvictionRunsMillis,則執行連線的有效性校驗
                if (idleMillis >= timeBetweenEvictionRunsMillis
                        || idleMillis < 0
                ) {
                    boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
                    if (!validate) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("skip not validate connection.");
                        }

                        discardConnection(poolableConnection.holder);
                        continue;
                    }
                }
            }
        }

        // 如果設定removeAbandoned為true
        // 則將連線放到activeConnections活躍連線map中
        if (removeAbandoned) {
            StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
            poolableConnection.connectStackTrace = stackTrace;
            poolableConnection.setConnectedTimeNano();
            poolableConnection.traceEnable = true;

            activeConnectionLock.lock();
            try {
                activeConnections.put(poolableConnection, PRESENT);
            } finally {
                activeConnectionLock.unlock();
            }
        }

        if (!this.defaultAutoCommit) {
            poolableConnection.setAutoCommit(false);
        }

        return poolableConnection;
    }
}

DruidDataSource#getConnectionDirect方法中會先呼叫getConnectionInternal()方法從連線池中拿連線,然後如果開啟了testOnBorrow,則校驗一下連線的有效性,如果無效則重新呼叫getConnectionInternal()方法拿連線,直到拿到的連線透過校驗。如果沒有開啟testOnBorrow但是開啟了testWhileIdle,則會判斷連線的空閒時間是否大於等於timeBetweenEvictionRunsMillis引數,如果滿足則校驗一下連線的有效性,若沒有透過校驗,那麼需要重新呼叫getConnectionInternal()方法拿連線,直到拿到的連線透過校驗或者連線的空閒時間小於timeBetweenEvictionRunsMillis

下面看一下實際從連線池拿連線的getConnectionInternal()方法的實現,如下所示。

private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException {
    
    ......

    final long nanos = TimeUnit.MILLISECONDS.toNanos(maxWait);
    final int maxWaitThreadCount = this.maxWaitThreadCount;

    DruidConnectionHolder holder;

    // 在死迴圈中從連線池拿連線
    // 一開始createDirect為false,表示先從池子中拿
    for (boolean createDirect = false; ; ) {
        if (createDirect) {
            // createDirect為true表示直接建立連線
            createStartNanosUpdater.set(this, System.nanoTime());
            // creatingCount為0表示當前沒有其它連線正在被建立
            if (creatingCountUpdater.compareAndSet(this, 0, 1)) {
                // 建立物理連線
                PhysicalConnectionInfo pyConnInfo = DruidDataSource.this.createPhysicalConnection();
                holder = new DruidConnectionHolder(this, pyConnInfo);
                holder.lastActiveTimeMillis = System.currentTimeMillis();

                creatingCountUpdater.decrementAndGet(this);
                directCreateCountUpdater.incrementAndGet(this);

                ......
                
                boolean discard;
                lock.lock();
                try {
                    // 如果當前正在使用的連線數未達到最大連線數
                    // 則當前正在使用的連線數加1
                    // 否則銷燬剛剛建立出來的連線
                    if (activeCount < maxActive) {
                        activeCount++;
                        holder.active = true;
                        if (activeCount > activePeak) {
                            activePeak = activeCount;
                            activePeakTime = System.currentTimeMillis();
                        }
                        break;
                    } else {
                        discard = true;
                    }
                } finally {
                    lock.unlock();
                }

                if (discard) {
                    JdbcUtils.close(pyConnInfo.getPhysicalConnection());
                }
            }
        }

        // 上鎖
        try {
            lock.lockInterruptibly();
        } catch (InterruptedException e) {
            connectErrorCountUpdater.incrementAndGet(this);
            throw new SQLException("interrupt", e);
        }

        try {
            // maxWaitThreadCount表示允許的最大等待連線的應用執行緒數
            // notEmptyWaitThreadCount表示正在等待連線的應用執行緒數
            // 等待連線的應用執行緒數達到最大值時,丟擲異常
            if (maxWaitThreadCount > 0
                    && notEmptyWaitThreadCount >= maxWaitThreadCount) {
                connectErrorCountUpdater.incrementAndGet(this);
                throw new SQLException("maxWaitThreadCount " + maxWaitThreadCount + ", current wait Thread count "
                        + lock.getQueueLength());
            }

            // 發生了致命錯誤,且設定了致命錯誤數最大值大於0,且正在使用的連線數大於等於致命錯誤數最大值
            if (onFatalError
                    && onFatalErrorMaxActive > 0
                    && activeCount >= onFatalErrorMaxActive) {
                
                // 拼接異常並丟擲
                
                ......

                throw new SQLException(
                        errorMsg.toString(), lastFatalError);
            }

            connectCount++;

            // 如果配置的建立連線的執行緒池是一個定時執行緒池
            // 且連線池已經沒有可用連線,
            // 且當前借出的連線數未達到允許的最大連線數
            // 且當前沒有其它執行緒(應用執行緒,建立連線的執行緒,建立連線的執行緒池裡的執行緒)在建立連線
            // 此時將createDirect置為true,讓當前應用執行緒直接建立連線
            if (createScheduler != null
                    && poolingCount == 0
                    && activeCount < maxActive
                    && creatingCountUpdater.get(this) == 0
                    && createScheduler instanceof ScheduledThreadPoolExecutor) {
                ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) createScheduler;
                if (executor.getQueue().size() > 0) {
                    createDirect = true;
                    continue;
                }
            }

            if (maxWait > 0) {
                // 如果設定了等待連線的最大等待時間,則呼叫pollLast()方法來拿連線
                // pollLast()方法執行時如果池中沒有連線,則應用執行緒會在notEmpty上最多等待maxWait的時間
                holder = pollLast(nanos);
            } else {
                // 呼叫takeLast()方法拿連線時,如果池中沒有連線,則會在notEmpty上一直等待,直到池中有連線
                holder = takeLast();
            }

            if (holder != null) {
                if (holder.discard) {
                    continue;
                }

                // 正在使用的連線數加1
                activeCount++;
                holder.active = true;
                if (activeCount > activePeak) {
                    activePeak = activeCount;
                    activePeakTime = System.currentTimeMillis();
                }
            }
        } catch (InterruptedException e) {
            connectErrorCountUpdater.incrementAndGet(this);
            throw new SQLException(e.getMessage(), e);
        } catch (SQLException e) {
            connectErrorCountUpdater.incrementAndGet(this);
            throw e;
        } finally {
            lock.unlock();
        }

        break;
    }

    // 如果拿到的連線為null,說明拿連線時等待超時了
    // 此時丟擲連線超時異常
    if (holder == null) {
    
        ......
        
        final Throwable createError;
        try {
            lock.lock();
            ......
            createError = this.createError;
        } finally {
            lock.unlock();
        }
        
        ......

        if (createError != null) {
            throw new GetConnectionTimeoutException(errorMessage, createError);
        } else {
            throw new GetConnectionTimeoutException(errorMessage);
        }
    }

    holder.incrementUseCount();

    DruidPooledConnection poolalbeConnection = new DruidPooledConnection(holder);
    return poolalbeConnection;
}

getConnectionInternal()方法中拿到連線的方式有三種,如下所示。

  1. 直接建立連線。需要滿足配置的建立連線的執行緒池是一個定時執行緒池,且連線池已經沒有可用連線,且當前借出的連線數未達到允許的最大連線數,且當前沒有其它執行緒在建立連線;
  2. 從池中拿連線,並最多等待maxWait的時間。需要設定了maxWait
  3. 從池中拿連線,並一直等待直到拿到連線。

下面最後看一下超時等待拿連線的DruidDataSource#pollLast方法的實現。

private DruidConnectionHolder pollLast(long nanos) throws InterruptedException, SQLException {
    long estimate = nanos;

    for (; ; ) {
        if (poolingCount == 0) {
            // 如果池中已經沒有連線,則喚醒在empty上等待的建立連線執行緒來建立連線
            emptySignal();

            if (failFast && isFailContinuous()) {
                throw new DataSourceNotAvailableException(createError);
            }

            // 等待時間耗盡,返回null
            if (estimate <= 0) {
                waitNanosLocal.set(nanos - estimate);
                return null;
            }

            // 應用執行緒即將在下面的notEmpty上等待
            // 這裡先把等待獲取連線的應用執行緒數加1
            notEmptyWaitThreadCount++;
            if (notEmptyWaitThreadCount > notEmptyWaitThreadPeak) {
                notEmptyWaitThreadPeak = notEmptyWaitThreadCount;
            }

            try {
                long startEstimate = estimate;
                // 應用執行緒在notEmpty上等待
                // 有連線被建立或者被歸還時,會喚醒在notEmpty上等待的應用執行緒
                estimate = notEmpty.awaitNanos(estimate);
                notEmptyWaitCount++;
                notEmptyWaitNanos += (startEstimate - estimate);

                if (!enable) {
                    connectErrorCountUpdater.incrementAndGet(this);

                    if (disableException != null) {
                        throw disableException;
                    }

                    throw new DataSourceDisableException();
                }
            } catch (InterruptedException ie) {
                notEmpty.signal();
                notEmptySignalCount++;
                throw ie;
            } finally {
                notEmptyWaitThreadCount--;
            }

            if (poolingCount == 0) {
                if (estimate > 0) {
                    // 若喚醒後池中還是沒有連線,且此時等待時間還有剩餘
                    // 則重新在notEmpty上等待
                    continue;
                }

                waitNanosLocal.set(nanos - estimate);
                return null;
            }
        }

        // poolingCount--
        decrementPoolingCount();
        // 從池中拿到連線
        DruidConnectionHolder last = connections[poolingCount];
        connections[poolingCount] = null;

        long waitNanos = nanos - estimate;
        last.setLastNotEmptyWaitNanos(waitNanos);

        return last;
    }
}

五. DruidDataSource連線歸還

Druid資料庫連線池中,每一個物理連線都會被包裝成DruidConnectionHolder,在提供給應用執行緒前,還會將DruidConnectionHolder包裝成DruidPooledConnection,類圖如下所示。

應用執行緒中使用連線完畢後,會呼叫DruidPooledConnectionclose()方法來歸還連線,也就是將連線放回連線池。DruidPooledConnection#close方法如下所示。

public void close() throws SQLException {
    if (this.disable) {
        return;
    }

    DruidConnectionHolder holder = this.holder;
    if (holder == null) {
        if (dupCloseLogEnable) {
            LOG.error("dup close");
        }
        return;
    }

    DruidAbstractDataSource dataSource = holder.getDataSource();
    // 判斷歸還連線的執行緒和獲取連線的執行緒是否是同一個執行緒
    boolean isSameThread = this.getOwnerThread() == Thread.currentThread();

    // 如果不是同一個執行緒,則設定asyncCloseConnectionEnable為true
    if (!isSameThread) {
        dataSource.setAsyncCloseConnectionEnable(true);
    }

    // 如果開啟了removeAbandoned機制
    // 或者asyncCloseConnectionEnable為true
    // 則呼叫syncClose()方法來歸還連線
    // syncClose()方法中會先加鎖,然後呼叫recycle()方法來回收連線
    if (dataSource.isAsyncCloseConnectionEnable()) {
        syncClose();
        return;
    }

    if (!CLOSING_UPDATER.compareAndSet(this, 0, 1)) {
        return;
    }

    try {
        for (ConnectionEventListener listener : holder.getConnectionEventListeners()) {
            listener.connectionClosed(new ConnectionEvent(this));
        }

        List<Filter> filters = dataSource.getProxyFilters();
        if (filters.size() > 0) {
            FilterChainImpl filterChain = new FilterChainImpl(dataSource);
            filterChain.dataSource_recycle(this);
        } else {
            // 回收連線
            recycle();
        }
    } finally {
        CLOSING_UPDATER.set(this, 0);
    }

    this.disable = true;
}

DruidPooledConnection#close方法中,會先判斷本次歸還連線的執行緒和獲取連線的執行緒是否是同一個執行緒,如果不是,則先加鎖然後再呼叫recycle()方法來回收連線,如果是則直接呼叫recycle()方法來回收連線。當開啟了removeAbandoned機制時,就可能會出現歸還連線的執行緒和獲取連線的執行緒不是同一個執行緒的情況,這是因為一旦開啟了removeAbandoned機制,那麼每一個被借出的連線都會被放到activeConnections活躍連線map中,並且在銷燬連線的執行緒DestroyConnectionThread中會每間隔timeBetweenEvictionRunsMillis的時間就遍歷一次activeConnections活躍連線map,一旦有活躍連線被借出的時間大於了removeAbandonedTimeoutMillis,那麼銷燬連線的執行緒DestroyConnectionThread就會主動去回收這個連線,以防止連線洩漏

下面看一下DruidPooledConnection#recycle方法的實現。

public void recycle() throws SQLException {
    if (this.disable) {
        return;
    }

    DruidConnectionHolder holder = this.holder;
    if (holder == null) {
        if (dupCloseLogEnable) {
            LOG.error("dup close");
        }
        return;
    }

    if (!this.abandoned) {
        DruidAbstractDataSource dataSource = holder.getDataSource();
        // 呼叫DruidAbstractDataSource#recycle回收當前連線
        dataSource.recycle(this);
    }

    this.holder = null;
    conn = null;
    transactionInfo = null;
    closed = true;
}

DruidPooledConnection#recycle方法中會呼叫到DruidDataSource#recycle方法來回收連線。DruidDataSource#recycle方法實現如下所示。

protected void recycle(DruidPooledConnection pooledConnection) throws SQLException {
    final DruidConnectionHolder holder = pooledConnection.holder;

    ......

    final boolean isAutoCommit = holder.underlyingAutoCommit;
    final boolean isReadOnly = holder.underlyingReadOnly;
    final boolean testOnReturn = this.testOnReturn;

    try {
        // 如果是非自動提交且存在事務
        // 則回滾事務
        if ((!isAutoCommit) && (!isReadOnly)) {
            pooledConnection.rollback();
        }

        // 重置連線資訊(配置還原為預設值,關閉Statement,清除連線的Warnings等)
        boolean isSameThread = pooledConnection.ownerThread == Thread.currentThread();
        if (!isSameThread) {
            final ReentrantLock lock = pooledConnection.lock;
            lock.lock();
            try {
                holder.reset();
            } finally {
                lock.unlock();
            }
        } else {
            holder.reset();
        }

        ......

        // 開啟了testOnReturn機制,則校驗連線有效性
        if (testOnReturn) {
            boolean validate = testConnectionInternal(holder, physicalConnection);
            // 校驗不透過則關閉物理連線
            if (!validate) {
                JdbcUtils.close(physicalConnection);

                destroyCountUpdater.incrementAndGet(this);

                lock.lock();
                try {
                    if (holder.active) {
                        activeCount--;
                        holder.active = false;
                    }
                    closeCount++;
                } finally {
                    lock.unlock();
                }
                return;
            }
        }
        
        ......

        lock.lock();
        try {
            // 連線即將放回連線池,需要將active設定為false
            if (holder.active) {
                activeCount--;
                holder.active = false;
            }
            closeCount++;

            // 將連線放到connections陣列的poolingCount位置
            // 然後poolingCount加1
            // 然後喚醒在notEmpty上等待連線的一個應用執行緒
            result = putLast(holder, currentTimeMillis);
            recycleCount++;
        } finally {
            lock.unlock();
        }

        if (!result) {
            JdbcUtils.close(holder.conn);
            LOG.info("connection recyle failed.");
        }
    } catch (Throwable e) {
        ......
    }
}

DruidDataSource#recycle方法中會先重置連線資訊,即將連線的一些配置重置為預設值,然後關閉連線的StatementWarnings,如果開啟了testOnReturn機制,則還需要校驗一下連線的有效性,校驗不透過則直接關閉物理連線,最後,將連線放回到connections陣列的poolingCount位置,然後喚醒一個在notEmpty上等待連線的應用執行緒。

六. removeAbandoned機制

Druid資料庫連線池提供了removeAbandoned機制來防止連線洩漏。要開啟removeAbandoned機制,需要設定如下引數。

引數說明
removeAbandoned發生連線洩漏時,是否需要回收洩漏的連線。預設為false,表示不回收。
removeAbandonedTimeoutMillis判斷髮生連線洩漏的超時時間。預設為300秒。

下面將對開啟removeAbandoned機制後,如何回收發生了洩漏的連線進行說明。當應用執行緒從連線池獲取到一個連線後,如果開啟了removeAbandoned機制,那麼會將這個連線放到activeConnections活躍連線map中,對應的方法為DruidDataSource#getConnectionDirect,原始碼片段如下所示。

public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
    int notFullTimeoutRetryCnt = 0;
    for (; ; ) {
        DruidPooledConnection poolableConnection;
        
        ......

        if (removeAbandoned) {
            StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
            poolableConnection.connectStackTrace = stackTrace;
            // 設定connectedTimeNano,用於後續判斷連線借出時間是否大於removeAbandonedTimeoutMillis
            poolableConnection.setConnectedTimeNano();
            poolableConnection.traceEnable = true;

            activeConnectionLock.lock();
            try {
                // 將從連線池獲取到的連線放到activeConnections中
                activeConnections.put(poolableConnection, PRESENT);
            } finally {
                activeConnectionLock.unlock();
            }
        }

        if (!this.defaultAutoCommit) {
            poolableConnection.setAutoCommit(false);
        }

        return poolableConnection;
    }
}

又已知Druid資料庫連線池有一個銷燬連線的執行緒會每間隔timeBetweenEvictionRunsMillis執行一次DestroyTask#run方法來銷燬連線,DestroyTask#run方法如下所示。

public void run() {
    shrink(true, keepAlive);

    // 如果開啟了removeAbandoned機制
    // 則執行removeAbandoned()方法來檢測發生了洩漏的連線並回收
    if (isRemoveAbandoned()) {
        removeAbandoned();
    }
}

DestroyTask#run方法的最後會判斷是否開啟了removeAbandoned機制,如果開啟了則會執行DruidDataSource#removeAbandoned方法來檢測哪些連線發生了洩漏,並主動回收這些連線。DruidDataSource#removeAbandoned方法如下所示。

public int removeAbandoned() {
    int removeCount = 0;

    long currrentNanos = System.nanoTime();

    List<DruidPooledConnection> abandonedList = new ArrayList<DruidPooledConnection>();

    activeConnectionLock.lock();
    try {
        Iterator<DruidPooledConnection> iter = activeConnections.keySet().iterator();

        for (; iter.hasNext(); ) {
            DruidPooledConnection pooledConnection = iter.next();

            // 執行中的連線不會被判定為發生了洩漏
            if (pooledConnection.isRunning()) {
                continue;
            }

            long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000);

            // 判斷連線借出時間是否達到連線洩漏的超時時間
            if (timeMillis >= removeAbandonedTimeoutMillis) {
                // 將發生了洩漏的連線從activeConnections中移除
                iter.remove();
                pooledConnection.setTraceEnable(false);
                // 將發生了洩露的連線新增到abandonedList集合中
                abandonedList.add(pooledConnection);
            }
        }
    } finally {
        activeConnectionLock.unlock();
    }

    if (abandonedList.size() > 0) {
        // 遍歷abandonedList集合
        // 主動呼叫每個發生了洩漏的DruidPooledConnection的close()方法來回收連線
        for (DruidPooledConnection pooledConnection : abandonedList) {
            final ReentrantLock lock = pooledConnection.lock;
            lock.lock();
            try {
                if (pooledConnection.isDisable()) {
                    continue;
                }
            } finally {
                lock.unlock();
            }

            JdbcUtils.close(pooledConnection);
            pooledConnection.abandond();
            removeAbandonedCount++;
            removeCount++;

            ......
            
        }
    }

    return removeCount;
}

DruidDataSource#removeAbandoned方法中主要完成的事情就是將每個發生了洩漏的連線從activeConnections中移動到abandonedList中,然後遍歷abandonedList中的每個連線並呼叫DruidPooledConnection#close方法,最終完成洩漏連線的回收。

總結

Druid資料庫連線池中,應用執行緒向連線池獲取連線時,如果池中沒有連線,則應用執行緒會在notEmpty上等待,同時Druid資料庫連線池中有一個建立連線的執行緒,會持續的向連線池建立連線,如果連線池已滿,則建立連線的執行緒會在empty上等待。

當有連線被生產,或者有連線被歸還,會喚醒在notEmpty上等待的應用執行緒,同理有連線被銷燬時,會喚醒在empty上等待的生產連線的執行緒。

Druid資料庫連線池中還有一個銷燬連線的執行緒,會每間隔timeBetweenEvictionRunsMillis的時間執行一次DestroyTask任務來銷燬連線,這些被銷燬的連線可以是存活時間達到最大值的連線,也可以是空閒時間達到指定值的連線。如果還開啟了保活機制,那麼空閒時間大於keepAliveBetweenTimeMillis的連線都會被校驗一次有效性,校驗不透過的連線會被銷燬。

最後,Druid資料庫連線池提供了removeAbandoned機制來防止連線洩漏,當開啟了removeAbandoned機制時,每一個被應用執行緒獲取的連線都會被新增到activeConnections活躍連線map中,如果這個連線在應用執行緒中使用完畢後沒有被關閉,那麼Druid資料庫連線池會從activeConnections中將其識別出來並主動回收。

相關文章