天天看點

池化技術(一)Druid是如何管理資料庫連接配接的?參考文章

基于依賴程式的版本資訊:druid:1.1.16               驅動程式mysql-connector-java:8.0.17

下一篇:HikariCP是如何管理資料庫連接配接的

零、類圖&流程預覽

下方流程中涉及到的類、屬性、方法名均列在這裡:Druid-類圖-屬性表  ←該表格用來輔助了解下面的流程圖和代碼,不用細看,混亂時可用來理清關系。

本文會通過getConnection作為入口,探索在druid裡,一個連接配接的生命周期。大體流程被劃分成了以下幾個主流程:

池化技術(一)Druid是如何管理資料庫連接配接的?參考文章

(圖檔可以右擊新頁面打開看大圖)

一、主流程1:擷取連接配接流程

首先從入口來看看它在擷取連接配接時做了哪些操作:

池化技術(一)Druid是如何管理資料庫連接配接的?參考文章

主流程1(對應源代碼:代碼段1-1)

上述為擷取連接配接時的流程圖,首先會調用init進行連接配接池的初始化,然後運作責任鍊上的每一個filter,最終執行getConnectionDirect擷取真正的連接配接對象,如果開啟了testOnBorrow,則每次都會去測試連接配接是否可用(這也是官方不建議設定testOnBorrow為true的原因,影響性能,這裡的測試是指測試mysql服務端的長連接配接是否斷開,一般mysql服務端長連保活時間是8h,被使用一次則重新整理一次使用時間,若一個連接配接距離上次被使用超過了保活時間,那麼再次使用時将無法與mysql服務端通信),如果testOnBorrow沒有被置為true,則會進行testWhileIdle的檢查(這一項官方建議設定為true,預設值也是true),檢查時會判斷目前連接配接對象距離上次被使用的時間是否超過規定檢查的時間,若超過,則進行檢查一次,這個檢查時間通過timeBetweenEvictionRunsMillis來控制,預設60s,每個連接配接對象會記錄下上次被使用的時間,用目前時間減去上一次的使用時間得出閑置時間,閑置時間再跟timeBetweenEvictionRunsMillis比較,超過這個時間就做一次連接配接可用性檢查,這個相比testOnBorrow每次都檢查來說,性能會提升很多,用的時候無需關注該值,因為預設值是true,經測試如果将該值設定為false,testOnBorrow也設定為false,資料庫服務端長連保活時間改為60s,60s内不使用連接配接,超過60s後使用将會報連接配接錯誤。若使用testConnectionInternal方法測試長連接配接結果為false,則證明該連接配接已被服務端斷開或者有其他的網絡原因導緻該連接配接不可用,則會觸發discardConnection進行連接配接回收(對應流程1.4,因為丢棄了一個連接配接,是以該方法會喚醒主流程3進行檢查是否需要建立連接配接)。整個流程運作在一個死循環内,直到取到可用連接配接或者超過重試上限報錯退出(在連接配接沒有超過連接配接池上限的話,最多重試一次(重試次數預設重試1次,可以通過notFullTimeoutRetryCount屬性來控制),是以取連接配接這裡一旦發生等待,在連接配接池沒有滿的情況下,最大等待 2 × maxWait 的時間 ←這個有待驗證)。

特别說明①

為了保證性能,不建議将testOnBorrow設定為true,或者說牽扯到長連接配接可用檢測的那幾項配置使用druid預設的配置就可以保證性能是最好的,如上所說,預設長連接配接檢查是60s一次,是以不啟用testOnBorrow的情況下要想保證萬無一失,自己要确認下所連的那個mysql服務端的長連接配接保活時間(雖然預設是8h,但是dba可能給測試環境設定的時間遠小于這個時間,是以如果這個時間小于60s,就需要手動設定timeBetweenEvictionRunsMillis了,如果mysql服務端長連接配接時間是8h或者更長,則用預設值即可。

特别說明②

為了防止不必要的擴容,在mysql服務端長連接配接夠用的情況下,對于一些qps較高的服務、網關業務,建議把池子的最小閑置連接配接數minIdle和最大連接配接數maxActive設定成一樣的,且按照需要調大,且開啟keepAlive進行連接配接活性檢查(參考流程4.1),這樣就不會後期發生動态建立連接配接的情況(建連還是個比較重的操作,是以不如一開始就申請好所有需要的連接配接,個人意見,僅供參考),但是像管理背景這種,長期qps非常低,但是有的時候需要用管理背景做一些巨大的操作(比如導資料什麼的)導緻需要的連接配接暴增,且管理背景不會特别要求性能,就适合将minIdle的值設定的比maxActive小,這樣不會造成不必要的連接配接浪費,也不會在需要暴增連接配接的時候無法動态擴增連接配接。

二、主流程2:初始化連接配接池

通過上面的流程圖可以看到,在擷取一個連接配接的時候首先會檢查連接配接池是否已經初始化完畢(通過inited來控制,bool類型,未初始化為flase,初始化完畢為true,這個判斷過程在init方法内完成),若沒有初始化,則調用init進行初始化(圖主流程1中的紫色部分),下面來看看init方法裡又做了哪些操作:

池化技術(一)Druid是如何管理資料庫連接配接的?參考文章

主流程2(對應源代碼:代碼段2-1)

可以看到,執行個體化的時候會初始化全局的重入鎖lock,在初始化過程中包括後續的連接配接池操作都會利用該鎖保證線程安全,初始化連接配接池的時候首先會進行雙重檢查是否已經初始化過,若沒有,則進行連接配接池的初始化,這時候還會通過SPI機制額外加載責任鍊上的filter,但是這類filter需要在類上加上@AutoLoad注解。然後初始化了三個數組,容積都為maxActive,首先connections就是用來存放池子裡連接配接對象的,evictConnections用來存放每次檢查需要抛棄的連接配接(結合流程4.1了解),keepAliveConnections用于存放需要連接配接檢查的存活連接配接(同樣結合流程4.1了解),然後生成初始化數(initialSize)個連接配接,放進connections,然後生成兩個必須的守護線程,用來添加連接配接進池以及從池子裡摘除不需要的連接配接,這倆過程較複雜,是以拆出來單說(主流程3和主流程4)。

特别說明①

從流程上看如果一開始執行個體化的時候不對連接配接池進行初始化(這個初始化是指對池子本身的初始化,并非單純的指druid對象屬性的初始化),那麼在第一次調用getConnection時就會走上圖那麼多邏輯,尤其是耗時較久的建立連接配接操作,被重複執行了很多次,導緻第一次getConnection時耗時過久,如果你的程式并發量很大,那麼第一次擷取連接配接時就會因為初始化流程而發生排隊,是以建議在執行個體化連接配接池後對其進行預熱,通過調用init方法或者getConnection方法都可以。

特别說明②

在建構全局重入鎖的時候,利用lock對象生成了倆Condition,對這倆Condition解釋如下:

當連接配接池連接配接夠用時,利用empty阻塞添加連接配接的守護線程(主流程3),當連接配接池連接配接不夠用時,擷取連接配接的那個線程(這裡記為業務線程A)就會阻塞在notEmpty上,且喚起阻塞在empty上的添加連接配接的守護線程,走完添加連接配接的流程,走完後會重新喚起阻塞在notEmpty上的業務線程A,業務線程A就會繼續嘗試擷取連接配接。

三、流程1.1:責任鍊

WARN:這塊東西結合源碼看更容易了解

池化技術(一)Druid是如何管理資料庫連接配接的?參考文章

流程1.1(對應源代碼:代碼段1-2)

這裡對應流程1裡擷取連接配接時需要執行的責任鍊,每個DruidAbstractDataSource裡都包含filters屬性,filters是對Druid裡Filters接口的實作,裡面有很多對應着連接配接池裡的映射方法,比如例子中dataSource的getConnection方法在觸發的時候就會利用FilterChain把每個filter裡的dataSource_getConnection給執行一遍,這裡也要說明下FilterChain,通過流程1.1可以看出來,datasource是利用FilterChain來觸發各個filter的執行的,FilterChain裡也有一堆datasource裡的映射方法,比如上圖裡的dataSource_connect,這個方法會把datasource裡的filters全部執行一遍直到nextFilter取不到值,才會觸發dataSource.getConnectionDirect,這個結合代碼會比較容易了解。

四、流程1.2:從池中擷取連接配接的流程

池化技術(一)Druid是如何管理資料庫連接配接的?參考文章

流程1.2(對應源代碼:代碼段1-3)

 通過getConnectionInternal方法從池子裡擷取真正的連接配接對象,druid支援兩種方式新增連接配接,一種是通過開啟不同的守護線程通過await、signal通信實作(本文啟用的方式,也是預設的方式),另一種是直接通過線程池異步新增,這個方式通過在初始化druid時傳入asyncInit=true,再把一個線程池對象指派給createScheduler,就成功啟用了這種模式,沒仔細研究這種方式,是以本文的流程圖和代碼塊都會規避這個模式。

上面的流程很簡單,連接配接足夠時就直接poolingCount-1,數組取值,傳回,activeCount+1,整體複雜度為O(1),關鍵還是看取不到連接配接時的做法,取不到連接配接時,druid會先喚起新增連接配接的守護線程新增連接配接,然後陷入等待狀态,然後喚醒該等待的點有兩處,一個是用完了連接配接recycle(主流程5)進池子後觸發,另外一個就是新增連接配接的守護線程成功新增了一個連接配接後觸發,await被喚起後繼續加入鎖競争,然後往下走如果發現池子裡的連接配接數仍然是0(說明在喚醒後參與鎖競争裡剛被放進來的連接配接又被别的線程拿去了),則繼續下一次的await,這裡采用的是awaitNanos方法,初始值是maxWait,然後下次被重新整理後就是maxWait減去上次阻塞花費的實際時間,每次await的時間會逐漸減少,直到歸零,整體時間是約等于maxWait的,但實際比maxActive要大,因為程式本身存在耗時以及被喚醒後又要參與鎖競争導緻也存在一定的耗時。

如果最終都沒辦法拿到連接配接則傳回null出去,緊接着觸發主流程1中的重試邏輯。

druid如何防止在擷取不到連接配接時阻塞過多的業務線程?

通過上面的流程圖和流程描述,如果非常極端的情況,池子裡的連接配接完全不夠用時,會阻塞過多的業務線程,甚至會阻塞超過maxWait這麼久,有沒有一種措施是可以在連接配接不夠用的時候控制阻塞線程的個數,超過這個限制後直接報錯,而不是陷入等待呢?

druid其實支援這種政策的,在maxWaitThreadCount屬性為預設值(-1)的情況下不啟用,如果maxWaitThreadCount配置大于0,表示啟用,這是druid做的一種丢棄措施,如果你不希望在池子裡的連接配接完全不夠用導阻塞的業務線程過多,就可以考慮配置該項,這個屬性的意思是說在連接配接不夠用時最多讓多少個業務線程發生阻塞,流程1.2的圖裡沒有展現這個開關的用途,可以在代碼裡檢視,每次在pollLast方法裡陷入等待前會把屬性notEmptyWaitThreadCount進行累加,阻塞結束後會遞減,由此可見notEmptyWaitThreadCount就是表示目前等待可用連接配接時阻塞的業務線程的總個數,而getConnectionInternal在每次調用pollLast前都會判斷這樣一段代碼:

if (maxWaitThreadCount > 0 && notEmptyWaitThreadCount >= maxWaitThreadCount) {
                    connectErrorCountUpdater.incrementAndGet(this);
                    throw new SQLException("maxWaitThreadCount " + maxWaitThreadCount + ", current wait Thread count "
                            + lock.getQueueLength()); //直接抛異常,而不是陷入等待狀态阻塞業務線程
                }
           

 可以看到,如果配置了maxWaitThreadCount所限制的等待線程個數,那麼會直接判斷目前陷入等待的業務線程是否超過了maxWaitThreadCount,一旦超過甚至不觸發pollLast的調用(防止新增等待線程),直接抛錯。

一般情況下不需要啟用該項,一定要啟用建議考慮好maxWaitThreadCount的取值,一般來說發生大量等待說明代碼裡存在不合理的地方:比如典型的連接配接池基本配置不合理,高qps的系統裡maxActive配置過小;比如借出去的連接配接沒有及時close歸還;比如存在慢查詢或者慢事務導緻連接配接借出時間過久。這些要比配置maxWaitThreadCount更值得優先考慮,當然配置這個做一個極限保護也是沒問題的,隻是要結合實際情況考慮好取值。

五、流程1.3:連接配接可用性測試

①init-checker

講這塊的東西之前,先來了解下如何初始化檢測連接配接用的checker,整個流程參考下圖:

池化技術(一)Druid是如何管理資料庫連接配接的?參考文章

init-checker流程圖(對應源代碼:代碼段2-1中的initValidConnectionChecker方法與MySqlValidConnectionChecker構造器)

初始化checker發生在init階段(限于篇幅,沒有在主流程2(init階段)裡展現出來,隻需要記住初始化checker也是發生在init階段就好),druid支援多種資料庫的連接配接源,是以checker針對不同的驅動程式都做了适配,是以才看到圖中checker有不同的實作,我們根據加載到的驅動類名比對不同的資料庫checker,上圖比對至mysql的checker,checker的初始化裡做了一件事情,就是判斷驅動内是否有ping方法(jdbc4開始支援,mysql-connector-java早在3.x的版本就有ping方法的實作了),如果有,則把usePingMethod置為true,用于後續啟用checker時做判斷用(下面會講,這裡置為true,則通過反射的方式調用驅動程式的ping方法,如果為false,則觸發普通的SELECT 1查詢檢測,SELECT 1就是我們非常熟悉的那個東西啦,建立statement,然後執行SELECT 1,然後再判斷連接配接是否可用)。

②testConnectionInternal

然後回到本節探讨的方法:流程1.3對應的testConnectionInternal

池化技術(一)Druid是如何管理資料庫連接配接的?參考文章

流程1.3(對應源代碼:代碼段1-4)

這個方法會利用主流程2(init階段)裡初始化好的checker對象(流程參考init-checker)裡的isValidConnection方法,如果啟用ping,則該方法會利用invoke觸發驅動程式裡的ping方法,如果不啟用ping,就采用SELECT 1方式(從init-checker裡可以看出啟不啟用取決于加載到的驅動程式裡是否存在相應的方法)。

六、流程1.4:抛棄連接配接

池化技術(一)Druid是如何管理資料庫連接配接的?參考文章

流程1.4(對應源代碼:代碼段1-5)

經過流程1.3傳回的測試結果,如果發現連接配接不可用,則直接觸發抛棄連接配接邏輯,這個過程非常簡單,如上圖所示,由流程1.2擷取到該連接配接時累加上去的activeCount,在本流程裡會再次減一,表示被取出來的連接配接不可用,并不能active狀态。其次這裡的close是拿着驅動那個連接配接對象進行close,正常情況下一個連接配接對象會被druid封裝成DruidPooledConnection對象,内部持有的conn就是真正的驅動Connection對象,上圖中的關閉連接配接就是擷取的該對象進行close,如果使用包裝類DruidPooledConnection進行close,則代表回收連接配接對象(recycle,參考主流程5)。

七、主流程3:添加連接配接的守護線程

池化技術(一)Druid是如何管理資料庫連接配接的?參考文章

主流程3(對應源代碼:代碼段3-1)

在主流程2(init初始化階段)時就開啟了該流程,該流程獨立運作,大部分時間處于等待狀态,不會搶占cpu,但是當連接配接不夠用時,就會被喚起追加連接配接,成功建立連接配接後将會喚醒其他正在等待擷取可用連接配接的線程,比如:

結合流程1.2來看,當連接配接不夠用時,會通過empty.signal喚醒該線程進行補充連接配接(阻塞在empty上的線程隻有主流程3的單線程),然後通過notEmpty阻塞自己,當該線程補充連接配接成功後,又會對阻塞在notEmpty上的線程進行喚醒,讓其進入鎖競争狀态,簡單了解就是一個生産-消費模型。這裡有一些細節,比如池子裡的連接配接使用中(activeCount)加上池子裡剩餘連接配接數(poolingCount)就是指目前一共生成了多少個連接配接,這個數不能比maxActive還大,如果比maxActive還大,則再次陷入等待。而在往池子裡put連接配接時,則判斷poolingCount是否大于maxActive來決定最終是否入池。

八、主流程4:抛棄連接配接的守護線程

池化技術(一)Druid是如何管理資料庫連接配接的?參考文章

主流程4(對應源代碼:代碼段4-1)

流程4.1:連接配接池瘦身,檢查連接配接是否可用以及丢棄多餘連接配接

整個過程如下:

池化技術(一)Druid是如何管理資料庫連接配接的?參考文章

流程4.1(對應源代碼:代碼段4-2)

整個流程分成圖中主要的幾步,首先利用poolingCount減去minIdle計算出需要做丢棄檢查的連接配接對象區間,意味着這個區間的對象有被丢棄的可能,具體要不要放進丢棄隊列evictConnections,要判斷兩個屬性:

minEvictableIdleTimeMillis:最小檢查間隙,預設值30min,官方解釋:一個連接配接在池中最小生存的時間(結合檢查區間來看,閑置時間超過這個時間,才會被丢棄)。

maxEvictableIdleTimeMillis:最大檢查間隙,預設值7h,官方解釋:一個連接配接在池中最大生存的時間(無視檢查區間,隻要閑置時間超過這個時間,就一定會被丢棄)。

如果目前連接配接對象閑置時間超過minEvictableIdleTimeMillis且下标在evictCheck區間内,則加入丢棄隊列evictConnections,如果閑置時間超過maxEvictableIdleTimeMillis,則直接放入evictConnections(一般情況下會命中第一個判斷條件,除非一個連接配接不在檢查區間,且閑置時間超過maxEvictableIdleTimeMillis)。

如果連接配接對象不在evictCheck區間内,且keepAlive屬性為true,則判斷該對象閑置時間是否超出keepAliveBetweenTimeMillis(預設值60s),若超出,則意味着該連接配接需要進行連接配接可用性檢查,則将該對象放入keepAliveConnections隊列。

兩個隊列指派完成後,則池子會進行一次壓縮,沒有涉及到的連接配接對象會被壓縮到隊首。

然後就是處理evictConnections和keepAliveConnections兩個隊列了,evictConnections裡的對象會被close最後釋放掉,keepAliveConnections裡面的對象将會其進行檢測(流程參考流程1.3的isValidConnection),碰到不可用的連接配接會調用discard(流程1.4)抛棄掉,可用的連接配接會再次被放進連接配接池。

整個流程可以看出,連接配接閑置後,也并非一下子就減少到minIdle的,如果之前産生一堆的連接配接(不超過maxActive),突然閑置了下來,則至少需要花minEvictableIdleTimeMillis的時間才可以被移出連接配接池,如果一個連接配接閑置時間超過maxEvictableIdleTimeMillis則必定被回收,是以極端情況下(比如一個連接配接池從初始化後就沒有再被使用過),連接配接池裡并不會一直保持minIdle個連接配接,而是一個都沒有,生産環境下這是非常不常見的,預設的maxEvictableIdleTimeMillis都有7h,除非是極度冷門的系統才會出現這種情況,而開啟keepAlive也不會推翻這個規則,keepAlive的優先級是低于maxEvictableIdleTimeMillis的,keepAlive隻是保證了那些檢查中不需要被移出連接配接池的連接配接在指定檢測時間内去檢測其連接配接活性,進而決定是否放入池子或者直接discard。

流程4.2:主動回收連接配接,防止記憶體洩漏

過程如下:

池化技術(一)Druid是如何管理資料庫連接配接的?參考文章

流程4.2(對應源代碼:代碼段4-3)

這個流程在removeAbandoned設定為true的情況下才會觸發,用于回收那些拿出去的使用長期未歸還(歸還:調用close方法觸發主流程5)的連接配接。

先來看看activeConnections是什麼,activeConnections用來儲存目前從池子裡被借出去的連接配接,這個可以通過主流程1看出來,每次調用getConnection時,如果開啟removeAbandoned,則會把連接配接對象放到activeConnections,然後如果長期不調用close,那麼這個被借出去的連接配接将永遠無法被重新放回池子,這是一件很麻煩的事情,這将存在記憶體洩漏的風險,因為不close,意味着池子會不斷産生新的連接配接放進connections,不符合連接配接池預期(連接配接池出發點是盡可能少的建立連接配接),然後之前被借出去的連接配接對象還有一直無法被回收的風險,存在記憶體洩漏的風險,是以為了解決這個問題,就有了這個流程,流程整體很簡單,就是将現在借出去還沒有歸還的連接配接,做一次判斷,符合條件的将會被放進abandonedList進行連接配接回收(這個list裡的連接配接對象裡的abandoned将會被置為true,标記已被該流程處理過,防止主流程5再次處理,具體可以參考代碼段5-1)。

這個如果在實踐中能保證每次都可以正常close,完全不用設定removeAbandoned=true,目前如果使用了類似mybatis、spring等開源架構,架構内部是一定會close的,是以此項是不建議設定的,視情況而定。

九、主流程5:回收連接配接

這個流程通常是靠連接配接包裝類DruidPooledConnection的close方法觸發的,目标方法為recycle,流程圖如下:

池化技術(一)Druid是如何管理資料庫連接配接的?參考文章

主流程5(對應源代碼:代碼段5-1)

這也是非常重要的一個流程,連接配接用完要歸還,就是利用該流程完成歸還的動作,利用druid對外包裝的Connecion包裝類DruidPooledConnection的close方法觸發,該方法會通過自己内部的close或者syncClose方法來間接觸發dataSource對象的recycle方法,進而達到回收的目的。

最終的recycle方法:

①如果removeAbandoned被設定為true,則通過traceEnable判斷是否需要從activeConnections移除該連接配接對象,防止流程4.2再次檢測到該連接配接對象,當然如果是流程4.2主動觸發的該流程,那麼意味着流程4.2裡已經remove過該對象了,traceEnable會被置為false,本流程就不再觸發remove了(這個流程都是在removeAbandoned=true的情況下進行的,在主流程1裡連接配接被放進activeConnections時traceEnable被置為true,而在removeAbandoned=false的情況下traceEnable恒等于false)。

②如果回收過程中發現存在有未處理完的事務,則觸發復原(比較有可能觸發這一條的是流程4.2裡強制歸還連接配接,也有可能是單純使用連接配接,開啟事務卻沒有送出事務就直接close的情況),然後利用holder.reset進行恢複連接配接對象裡一些屬性的預設值,除此之外,holder對象還會把由它産生的statement對象放到自己的一個arraylist裡面,reset方法會循環着關閉内部未關閉的statement對象,最後清空list,當然,statement對象自己也會記錄下其産生的所有的resultSet對象,然後關閉statement時同樣也會循環關閉内部未關閉的resultSet對象,這是連接配接池做的一種保護措施,防止使用者拿着連接配接對象做完一些操作沒有對打開的資源關閉。

③判斷是否開啟testOnReturn,這個跟testOnBorrow一樣,官方預設不開啟,也不建議開啟,影響性能,理由參考主流程1裡針對testOnBorrow的解釋。

④直接放回池子(目前connections的尾部),然後需要注意的是putLast方法和put方法的不同之處,putLast會把lastActiveTimeMillis置為目前時間,也就是說不管一個連接配接被借出去過久,隻要歸還了,最後活躍時間就是目前時間,這就會有造成某種特殊異常情況的發生(非常極端,幾乎不會觸發,可以選擇不看):

如果不開啟testOnBorrow和testOnReturn,并且keepAlive設定為false,那麼長連接配接可用測試的間隔依據就是利用目前時間減去上次活躍時間(lastActiveTimeMillis)得出閑置時間,然後再利用閑置時間跟timeBetweenEvictionRunsMillis(預設60s)進行對比,超過才進行長連接配接可用測試。

那麼如果一個mysql服務端的長連接配接保活時間被人為調整為60s,然後timeBetweenEvictionRunsMillis被設定為59s,這個設定是非常合理的,保證了測試間隔小于長連接配接實際保活時間,然後如果這時一個連接配接被拿出去後一直過了61s才被close回收,該連接配接對象的lastActiveTimeMillis被刷為目前時間,如果在59s内再次拿到該連接配接對象,就會繞過連接配接檢查直接報連接配接不可用的錯誤。

十、尾聲

到這裡針對druid連接配接池的初始化以及其内部一個連接配接從生産到消亡的整個流程就已經整理完了,主要是列出其運作流程以及一些主要的監控資料都是如何産生的,沒有涉及到的是一個sql的執行,因為這個基本上就跟使用原生驅動程式差不多,隻是druid又包裝了一層Statement等,用于完成一些自己的操作。

對于druid,處理連接配接隻是很小的一塊内容,卻是很核心的一塊内容。

Druid位址:https://github.com/alibaba/druid

參考文章

https://www.cnblogs.com/hama1993/p/11421576.html

繼續閱讀