前言
作為一個java程序員载慈,數據庫的JDBC幾乎每天都在做,數據庫連接池Druid每天也在使用珍手,但可能用起來太簡單了(spring中引入依賴即可)办铡,往往忽略了連接池的意義和優(yōu)化
本文從源碼的角度分析Druid的常用配置及原理
連接
當我們程序需要訪問數據庫時辞做,需要創(chuàng)建一個本地到數據庫服務的網絡連接,此時本地代碼就相當于一個數據庫的客戶端寡具,可以通過這個連接去訪問數據秤茅、執(zhí)行sql,如下
Driver driver = new com.mysql.cj.jdbc.Driver();
// 創(chuàng)建連接
Connection con = driver.connect(JDBC_URL, props);
Statement statement = con.createStatement();
ResultSet resultSet = statement.executeQuery("show tables");
while (resultSet.next()) {
System.out.println(resultSet.getString(1));
}
con.close();
池化技術
由于我們的代碼需要不斷與數據庫交互讀取數據晒杈,如果每次請求數據都創(chuàng)建一個連接的話嫂伞,網絡開銷是很大的,也會導致我們的程序比較慢拯钻,同時連接如果太多也會給數據庫造成壓力
為了解決這個問題帖努,就有了池化技術,把創(chuàng)建好的連接放在池里粪般,用時去池里獲取拼余,節(jié)省了創(chuàng)建連接的時間,也可以通過配置來限定池的最大連接數等
連接池最常用的工具基本就是阿里的Druid了亩歹,簡單使用如下
// druid 數據源
DruidDataSource druidDataSource = new DruidDataSource();
// 數據源配置
druidDataSource.setUrl(JDBC_URL);
druidDataSource.setUsername(USERNAME);
druidDataSource.setPassword(PASSWORD);
// 初始化
druidDataSource.init();
// 獲取表名
Connection con = druidDataSource.getConnection();
Statement statement = con.createStatement();
ResultSet resultSet = statement.executeQuery("show tables");
while (resultSet.next()) {
System.out.println(resultSet.getString(1));
}
con.close();
可以看到使用了Druid匙监,獲取連接不再是直接使用驅動創(chuàng)建連接,而是通過DruidDataSource
對象獲取連接
DruidDataSource
接下來就分析DruidDataSource的源碼小作,從三個方面入手:配置亭姥、存儲、線程
配置
首先作為一個連接池工具顾稀,首先要支持重要參數的可配置达罗,以下只列舉一部分常用的配置和其簡單含義,后面的源碼分析會實際的分析每個配置的作用
- maxActive 最大連接數
- initialSize 初始化連接數
- minIdle 最小空閑數
- keepAlive 是否保持連接
- asyncInit 是否異步初始化
- timeBetweenEvictionRunsMillis 回收連接任務運行的頻率
- minEvictableIdleTimeMillis 最小閑置時間静秆,連接閑置時間小于這個時間不會被回收粮揉,大于有可能被回收
- maxEvictableIdleTimeMillis 最大閑置時間,連接閑置時間超過這個數是一定被回收的
- validationQuery 測試是否有效的sql
- phyTimeoutMillis 連接物理超時時間
有很多配置都是和其它配置配合使用的抚笔,所以很多配置單獨拿出來說它的作用沒有意義扶认,還是要結合代碼看一下
存儲
DruidDataSource
作為一個連接池,內部一定會有一個容器來存儲連接殊橙,這應該是最重要的屬性
private volatile DruidConnectionHolder[] connections; // 當前的所有連接
connections
存儲的就是所有的數據庫連接對象辐宾,并封裝了一個連接的持有對象DruidConnectionHolder
,在持有物理連接的同時蛀柴,也記錄了一些連接的其它屬性螃概,比如:
- connectTimeMillis 連接建立的時間
- lastActiveTimeMillis 連接上一次被使用的時間
還有非常重要的一點,這個存儲連接的容器是有排序的鸽疾,每次使用連接都從最后拿,這就導致容器尾部的連接是最活躍的训貌,也就導致前面的連接閑置時間肯定是要高于后面的
計數
同時制肮,池內部有很多計數器來存儲當前各種維度的數量值
private int poolingCount = 0; // 可用連接數
private int activeCount = 0; // 正在使用連接數
private volatile long discardCount = 0; // 丟棄連接數
private int notEmptyWaitThreadCount = 0; // 等待連接的線程數
線程
DruidDataSource中有幾個線程冒窍,在初始化方法init被創(chuàng)建并運行,它們分別承擔不同的工作
public void init() throws SQLException {
// ...
createAndLogThread(); // 開啟負責日志統(tǒng)計的線程
createAndStartCreatorThread(); // 開啟負責創(chuàng)建連接的線程
createAndStartDestroyThread(); // 開啟負責負責銷毀連接的線程
// ...
}
實際上豺鼻,DruidDataSource
就是依靠這些線程來維護整個線程池中連接的創(chuàng)建和銷毀任務综液,它們可以看做是線程池的維護人員
小結
所以Druid池簡單來說就是一個連接的容器(connections),可配的參數儒飒,狀態(tài)/計數的存儲組成的一個類谬莹,在初始化方法中會創(chuàng)建多個線程,這些線程在連接池的生命周期一直運行并監(jiān)控這當前線程池的狀態(tài)桩了,并根據配置和計數數據在需要的時候在容器中創(chuàng)建/銷毀線程
連接池中這幾個線程是可以被替代的附帽,如果我們設置了調度器,則可以按我們自己的方式去調度創(chuàng)建銷毀連接的任務井誉,這屬于比較高級的用法了蕉扮,本文不做探討
線程源碼分析
協(xié)調
線程池內部運行的兩個主要線程:創(chuàng)建連接的線程和銷毀連接的線程,池外部還有我們用戶代碼中想要獲取連接的線程(在此統(tǒng)一稱之為用戶線程)
各個線程可能都要訪問和修改各種計數和連接容器颗圣,為了達到線程安全喳钟,DruidDataSource
內部提供了一個統(tǒng)一的ReentrantLock鎖
protected ReentrantLock lock;
各線程也少不了溝通,比如某用戶線程想獲取連接在岂,如何通知創(chuàng)建線程去創(chuàng)建連接奔则,創(chuàng)建線程創(chuàng)建完連接有如何告知用戶線程,為解決這個問題蔽午,DruidDataSource
內部提了兩個主要的Condition
protected Condition notEmpty;
protected Condition empty;
其中empty代表空條件易茬,創(chuàng)建線程通過empty.await()
即可等待空信號,而用戶線程通過empty.signal()
即可發(fā)送空信號給創(chuàng)建線程祠丝,此時用戶線程notEmpty.await()
開始等待非空條件疾呻,而創(chuàng)建線程一般會創(chuàng)建連接,創(chuàng)建完成后通過notEmpty.signal()
通知線程創(chuàng)建完畢
創(chuàng)建連接的線程
CreateConnectionThread是專門負責創(chuàng)建連接的写半,可以說DruidDataSource中的連接基本都是由它負責實際創(chuàng)建的(也會有特例岸蜗,比如默認情況下initialSize設置的連接數是在init方法中直接創(chuàng)建的)
大部分情況下CreateConnectionThread是在empty條件上等待空信號,即empty.wait()
叠蝇,當得到信號時再創(chuàng)建連接
接下來就看一下CreateConnectionThread的源碼
public class CreateConnectionThread extends Thread {
public CreateConnectionThread(String name){
super(name);
// 設置守護線程
this.setDaemon(true);
}
public void run() {
initedLatch.countDown();
long lastDiscardCount = 0;
int errorCount = 0;
// 線程一直運行著
for (;;) {
// 一.判斷是否需要創(chuàng)建連接
// 獲取鎖
lock.lockInterruptibly();
// 當前被丟棄的連接數
long discardCount = DruidDataSource.this.discardCount;
// 對比上一次記錄被丟棄的連接數璃岳,看看是否有變化
boolean discardChanged = discardCount - lastDiscardCount > 0;
lastDiscardCount = discardCount;
try {
// 標志是否需要等待空信號
boolean emptyWait = true;
// 存在異常,當前池連接數為0悔捶,且沒有新丟棄的連接
if (createError != null
&& poolingCount == 0
&& !discardChanged) {
emptyWait = false;
}
// 如果設置了異步初始化铃慷,且當前創(chuàng)建的連接數少于設置初始連接數,則跳過等待直接創(chuàng)建連接
if (emptyWait
&& asyncInit && createCount < initialSize) {
emptyWait = false;
}
// 如果沒有跳過等待蜕该,并不是實際的去等待犁柜,而是還有一層判斷
if (emptyWait) {
// 有三種情況可以跳過這一步的等待
// 1.等待使用連接的線程數大于當前可用連接數
// 2.設置了keeplive=true且當前池的總連接數小于設置最小連接數
// 3.連續(xù)失敗isFailContinuous(這一項先忽略)
// 跳過這一步等待并不代表可以直接創(chuàng)建,還要進行下一步的是否到達最大設置數量的判斷
if (poolingCount >= notEmptyWaitThreadCount //
&& (!(keepAlive && activeCount + poolingCount < minIdle))
&& !isFailContinuous()
) {
// 等待空信號
empty.await();
}
// 如果當前連接數量已超過設置最大數量堂淡,則等待空信號馋缅,否則就可以去創(chuàng)建連接了
if (activeCount + poolingCount >= maxActive) {
empty.await();
// 等待到了空信號扒腕,并不是直接創(chuàng)建連接,而是重新判斷一次是否需要等待萤悴,因為連接數是絕對不能超越maxActive的瘾腰,所以為了安全,必須重新判斷一次
continue;
}
}
} catch (InterruptedException e) {
//...
} finally {
// 釋放鎖
lock.unlock();
}
// 二.開始創(chuàng)建連接
PhysicalConnectionInfo connection = null;
try {
// 創(chuàng)建物理連接
connection = createPhysicalConnection();
} catch (SQLException e) {
//...
}
// 加入連接池的連接列表覆履,即connections
boolean result = put(connection);
// 如果連接池關閉蹋盆,創(chuàng)建連接線程也停止
if (closing || closed) {
break;
}
}
}
}
代碼看起來還是比較復雜,簡單總結一下:
<特殊情況>
創(chuàng)建連接的線程有兩種特殊情況硝全,這兩種情況主要是異步初始化化和處理異常栖雾,這種情況下直接跳過等待,也不需考慮maxActive
柳沙,直接創(chuàng)建連接岩灭,這種情況相對特殊暫不做考慮
<常規(guī)情況>
大部分情況下,創(chuàng)建連接的線程要根據minIdle
,maxActive
等配置以及線程池的狀態(tài)來判斷是否需要等待赂鲤,如果不需要等待也會創(chuàng)建連接
常規(guī)情況下有三種條件噪径,滿意任意一種就可以不需等待直接創(chuàng)建連接,但還有個大前提就是池中的連接總數不能超過maxActive
設置的數量
三種條件分別是
- 當等待使用連接的線程數(
notEmptyWaitThreadCount
)大于池中可用連接數(poolingCount
)数初,即供不應求時 - 當線程池設置保持連接(
keepAlive=true
)找爱,且當前池中的總連接數(activeCount + poolingCount
)小于設置最小連接數(minIdle
),即池中沒有保持足夠的最小連接數時 - isFailContinuous 連續(xù)失敗時
三種條件如果都不滿足泡孩,則在empty
條件上等待索要連接的信號车摄,得到信號則創(chuàng)建連接(還需要判斷最大連接數)
如果三個條件滿足任意一個,但連接數已到達maxActive
仑鸥,依然在empty
條件上等待信號吮播,得到信號重新再判斷一次,是為了確保連接數不超過最大配置
畫個圖梳理一下
用一句話總結一下:
CreateConnectionThread負責給線程池創(chuàng)建連接眼俊,當線程池中供不應求意狠、最小保持連接數不足、連續(xù)錯誤時線程會主動創(chuàng)建連接疮胖,否則就會休息節(jié)省體力环戈,得到需求信號再創(chuàng)建連接,創(chuàng)建完成后重新開始審視創(chuàng)建的工作, ps:整個過程確保連接數不能超出設定范圍
銷毀連接的線程
與CreateConnectionThread對應澎灸,DestroyConnectionThread承擔銷毀連接的任務院塞,主要根據配置的參數和當前的技術器,銷毀掉需要銷毀的連接
public class DestroyConnectionThread extends Thread {
public DestroyConnectionThread(String name) {
super(name);
// 設置守護線程
this.setDaemon(true);
}
public void run() {
initedLatch.countDown();
// 不斷執(zhí)行
for (;;) {
try {
//...
// 根據配置timeBetweenEvictionRunsMillis決定銷毀任務執(zhí)行的間隔
if (timeBetweenEvictionRunsMillis > 0) {
Thread.sleep(timeBetweenEvictionRunsMillis);
} else {
Thread.sleep(1000);
}
//...
// 執(zhí)行銷毀任務
destroyTask.run();
} catch (InterruptedException e) {
break;
}
}
}
}
銷毀連接的任務實時性要求并不是太高性昭,所以可能會隔一段時間才去計算并銷毀一次拦止,這個間隔的時間就是配置timeBetweenEvictionRunsMillis
其中DestroyTask的run方法定義如下
public void run() {
shrink(true, keepAlive);
if (isRemoveAbandoned()) {
removeAbandoned();
}
}
主要調用的方法即shrink,意指收縮線程池糜颠,重點看一下這個方法:
public void shrink(boolean checkTime, boolean keepAlive) {
// 獲取鎖
lock.lockInterruptibly();
// 是否需要補充
boolean needFill = false;
// 驅逐的數量
int evictCount = 0;
// 需要贝葱梗活的數量
int keepAliveCount = 0;
int fatalErrorIncrement = fatalErrorCount - fatalErrorCountLastShrink;
fatalErrorCountLastShrink = fatalErrorCount;
try {
// 未初始化完成不執(zhí)行
if (!inited) {
return;
}
// 池中可用連接數超出最小連接數的數量
final int checkCount = poolingCount - minIdle;
final long currentTimeMillis = System.currentTimeMillis();
// 循環(huán)池中可用連接
for (int i = 0; i < poolingCount; ++i) {
DruidConnectionHolder connection = connections[i];
// 異常的處理艺玲,暫不做考慮
if ((onFatalError || fatalErrorIncrement > 0) && (lastFatalErrorTimeMillis > connection.connectTimeMillis)) {
keepAliveConnections[keepAliveCount++] = connection;
continue;
}
// 如果檢查時間括蝠,銷毀線程傳入的是true
if (checkTime) {
// 如果設置了物聯連接超時時間
if (phyTimeoutMillis > 0) {
// 當前連接連接時間過過了超時時間鞠抑,加入要待回收集合中
long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
if (phyConnectTimeMillis > phyTimeoutMillis) {
evictConnections[evictCount++] = connection;
continue;
}
}
// 計算當前連接已閑置的時間
long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
// 如果連接閑置時間比較短,則可不被回收忌警,可以直接跳出循環(huán)搁拙,因為連接池是尾部更活躍,后面的肯定更短不需要判斷了
if (idleMillis < minEvictableIdleTimeMillis
&& idleMillis < keepAliveBetweenTimeMillis
) {
break;
}
// 如果連接閑置時間超出了設置的 最小閑置時間
if (idleMillis >= minEvictableIdleTimeMillis) {
// 如果當前連接的位置在checkCount以內法绵,則加入待回收集合
if (checkTime && i < checkCount) {
evictConnections[evictCount++] = connection;
continue;
// 否則如果已超出最大閑置時間箕速,也要加入待回收集合
} else if (idleMillis > maxEvictableIdleTimeMillis) {
evictConnections[evictCount++] = connection;
continue;
}
}
// 如果閑置時間超出保活檢測時間朋譬,且設置了keepAlive盐茎,則加入待驗證保活的集合中
if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) {
keepAliveConnections[keepAliveCount++] = connection;
}
} else {
//...
}
}
// 要刪除的連接總數徙赢,實際上keepAliveCount只是有可能被刪除字柠,還沒有最終定論,這里做法是先刪除掉狡赐,如果驗證連接可用后續(xù)再加回來即可
int removeCount = evictCount + keepAliveCount;
if (removeCount > 0) {
// 刪除連接池中的廢棄連接窑业,由于廢棄的連接一定是前removeCount個連接,所以直接使用復制即可刪除
System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
// 當前可用連接數變小
poolingCount -= removeCount;
}
keepAliveCheckCount += keepAliveCount;
// 如果設置了闭硖耄活常柄,且總連接數小于最小連接數,則需要補充
if (keepAlive && poolingCount + activeCount < minIdle) {
needFill = true;
}
} finally {
lock.unlock();
}
// 如果有要回收的連接
if (evictCount > 0) {
// 循環(huán)
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) {
// 循環(huán)要蔽髋耍活的連接
for (int i = keepAliveCount - 1; i >= 0; --i) {
DruidConnectionHolder holer = keepAliveConnections[i];
Connection connection = holer.getConnection();
holer.incrementKeepAliveCheckCount();
boolean validate = false;
try {
// 驗證鏈接是否有效,此時要用到配置的validationQuery來驗證連接的有效性哨颂,如果沒設置喷市,就默認有效
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) {
// skip
}
lock.lock();
try {
// 記錄被丟棄的連接數+1
discardCount++;
// 如果且總連接數小于最小連接數,發(fā)出空信號
if (activeCount + poolingCount <= minIdle) {
emptySignal();
}
} finally {
lock.unlock();
}
}
}
this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount);
// 清空需要迸剌铮活的連接集合
Arrays.fill(keepAliveConnections, null);
}
// 如果需要補充
if (needFill) {
lock.lock();
try {
// 計算需要補充的數量东抹,createTaskCount是使用自定義調度時的邏輯,暫時忽略
int fillCount = minIdle - (activeCount + poolingCount + createTaskCount);
// 發(fā)出空信號
for (int i = 0; i < fillCount; ++i) {
emptySignal();
}
} finally {
lock.unlock();
}
} else if (onFatalError || fatalErrorIncrement > 0) {
// 異常處理 忽略..
}
}
核心代碼依然相當復雜沃测,還是嘗試總結一下
(一) 銷毀任務實時性不高缭黔,銷毀線程執(zhí)行是一個定時任務,時間間隔可配
(二) 銷毀線程只考慮數目為poolingCount
的池中可用連接蒂破,正在使用的連接不可能被銷毀(其實也已不在池中)
(三) 銷毀線程會從前往后循環(huán)查看所有的池中連接馏谨,主要判斷是否需要銷毀或者保活附迷,主要包含如下邏輯:
- 循環(huán)前會提前計算當前可用連接超出最小限制連接的數量惧互,為
checkCount
哎媚,這個數量其實就是線程池中多余連接的數量,而且按照容器的排序喊儡,越前面的連接越不活躍拨与,所以前checkCount
就是多余連接,但多余連接不一定會被移除艾猜,有可能因為閑置時間(說明剛用完不久)較短而被暫時保留 - 如果當前連接閑置時間比較短买喧,不需要進行銷毀或保活測試匆赃,直接跳出循環(huán)淤毛,因為后面的連接活躍度更高
- 如果連接閑置時間比較長,比如超過了設置的最大閑置時間算柳,或超過最小閑置時間且當前連接本身就是多余連接低淡,就會從池中移出至待銷毀的集合中
- 如果連接閑置時間比較長,超過了彼蚕睿活測試的設定時間(且keepAlive)蔗蹋,就會從池中移出至待測試有效性的集合中
- 待銷毀集合的連接后續(xù)會被直接關閉,待測試有效性集合的連接需要測試連接是否可用滥壕,如果不可用直接銷毀纸颜,通過校驗加回至連接池中
- 由于銷毀了很多連接,可能導致keepAlive情況下最小連接數不夠了绎橘,所以需要通過empty.signal通知創(chuàng)建線程補充連接
再畫個示意圖
用戶線程
用戶線程主要是去池中獲取連接胁孙,上文也提到過,是從最后拿連接称鳞,重點方法takeLast
DruidConnectionHolder takeLast() throws InterruptedException, SQLException {
try {
while (poolingCount == 0) {
// 發(fā)送空信號涮较,讓創(chuàng)建線程創(chuàng)建連接
emptySignal(); // send signal to CreateThread create connection
// 增加等待線程數
notEmptyWaitThreadCount++;
// 等待非空信號
try {
notEmpty.await(); // signal by recycle or creator
} finally {
notEmptyWaitThreadCount--;
}
//...
}
} catch (InterruptedException ie) {
//...
}
// 有了可用連接
// 可用連接減一,因為要拿出用了
decrementPoolingCount();
// 取出最后一個連接
DruidConnectionHolder last = connections[poolingCount];
connections[poolingCount] = null;
// 返回
return last;
}
邏輯就是取池中最后一個連接冈止,如果沒有通知創(chuàng)建線程創(chuàng)建連接
最后
費了好大勁狂票,基本捋明白了Druid連接池的重要代碼,感覺真的很復雜
總結一下Druid的優(yōu)點
- 連接的創(chuàng)建銷毀異步執(zhí)行熙暴,保證效率
- 連接池的固定最大連接數避免了連接的過度創(chuàng)建
- 連接池中連接的存活時間可配置闺属,保證高并發(fā)下連接不會被回收,可重復利用
- 連接池的敝苊梗活機制掂器,可以固定維持一定數量的連接長期保留在池中,還可以定時檢測連接的有效性俱箱,固定維持的連接可以在并發(fā)驟增的情況下提前預熱国瓮,避免一次性建立過多連接
其實還是有很多地方并沒有想太明白,而且很多結論也很難測試,如果有誤乃摹,歡迎指正