hikari數據庫連接池源碼分析以及相關概念

hikari簡介以及相關的概念

"Simplicity is prerequisite for reliability."
- Edsger Dijkstra

hikari

hikari,日語中“光”的意思,作者為這個數據庫連接池命名為光,寓意是像光一樣快。在分析hikariCP之前簡單介紹下JDBC和數據庫連接池。

JDBC

全稱Java Database Connectivity,java入門課本中基本都會介紹到的部分。

以常見的MySQL數據庫為例,JDBC可以簡單概括為就是一個jar包,這個jar包中提供了相應的interface與class,封裝好了與MySQL服務端連接的協議,通過Java代碼就可以實現mysql client上的select、update、delete等功能。

JDBC

原始的JDBC使用姿勢是這樣的(爺青回):


 try {
    // 初始化驅動類com.mysql.jdbc.Driver
    Class.forName("com.mysql.jdbc.Driver");
    // 獲取connection連接
    conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?characterEncoding=UTF-8","root", "admin");

    // 查詢
    sql = "select * from test"
    pstmt = (PreparedStatement) conn.prepareStatement(sql);
    rs = (ResultSet) pstmt.executeQuery();
    // 后續將ResultSet轉化為對象返回...
} catch (ClassNotFoundException e) {                
    e.printStackTrace();
}catch (SQLException e) {                           
    e.printStackTrace();
}

數據庫連接池(Connection Pool)

以上DriverManager.getConnection建立jdbc連接的過程是很昂貴的,需要經歷 TCP握手+MySQL認證,如果服務在不斷并發處理多個請求的時候每次都重新建立JDBC連接并且在請求結束后關閉JDBC連接,那么將會導致:

  • 網絡IO多,因為不斷的TCP握手和TCP關閉,大量新建后立刻關閉的TIME_WAIT狀態的TCP連接占用系統資源
  • 數據庫負載高,數據庫也要重新為連接建立各種數據結構,并且在短暫的查詢結束后又要銷毀
  • 查詢耗時長,多了TCP握手和MySQL認證的耗時。同時,不斷產生又回收掉JDBC連接資源會頻繁觸發GC

為了能夠 復用已經辛辛苦苦建立好的JDBC連接,就有了數據庫連接池的概念,同理線程池、redis連接池、HBase連接池等等都同理。

數據庫連接池應該具有的功能

  • 在連接應用啟動的時候建立連接,并且保存到Java的容器或自定義的容器中維護
  • 在應用運行的過程中,間歇性測試連接的可用性(select 1),踢出不可用的連接,加入新的連接
  • 在應用結束后關閉JDBC連接

Spring-JDBC

基于以上原JDBC的使用方式可以看出,JDBC在使用上存在大量可以封裝起來的功能(connection的獲取、構造PreparedStatement、connection的回收等)。總之,Spring-JDBC通過對JDBC的封裝,簡化了SQL的執行步驟,我們只需要輸入sql語句,框架會輸出sql的執行結果(類似于mysql-client控制臺的效果)。

Spring-JDBC

如上圖,有了Spring-JDBC等框架封裝JDBC連接獲取,prepareStatement賦值,JDBC連接的釋放,使用者就只需要寫好sql,調用Spring-JDBC提供的JdbcTemplate類封裝的方法,等待獲取結果即可。

JdbcTemplate類會通過Spring依賴注入等方式設置好它的dataSource屬性,而這個dataSource就是一個數據連接池(Connection Pool,CP),CP通過選擇合適的數據結構來動態維護著與MySQL的Jdbc連接。整個封裝起來對使用者來說無需感知,只需要知道輸入SQL語句和輸出結果就好。

hikari核心功能

HikariDataSource

HikariDataSource的繼承結構

正如上圖所示,dataSource代表著一個數據庫連接池,而hikari的數據庫連接池就是HikariDataSource,這個類是hikari的核心類。

HikariDataSource

HikariDataSource的繼承關系如上圖,重點關注HikariConfigDataSource

  • DataSource,是一個接口,就是數據庫連接池在Java類中的體現,對JDBC框架來說不需要關注Connection怎么來的,只需要調這個接口的方法獲取即可


    DataSource
  • HikariConfig,hikari配置類,有著jdbcUrl、用戶名密碼、超時時間、數據庫連接池連接數等成員變量屬性

HikariDataSource的成員結構

HikariDataSource的主要成員結構如下圖:

HikariDataSource的結構
  • isShutdown,一個AtomicBoolean(cas樂觀鎖)用來標識該dataSource是否已經關閉
  • fastPathPool 和 pool,都是指向HikariPool結構的引用(Hikari優化用來提升性能的手段)
  • 若干繼承自HikariConfig的數據庫連接池配置屬性

HikariPool

HouseKeeper管家

HikariPool的主要屬性如上圖所示,主要通過30s執行一次的定時任務HouseKeeper來維護管理連接池中的連接。
HouseKeeper任務執行流程的偽代碼如下:

def run():
    # hikari的連接池維護機制大量依賴時間戳,如果發現系統時間倒退了,則會將連接池清空重來
    if detect_retrograde_time():
        softEvictConnections()
        return
    if detect_forward_time(): # 如果發現時間推前了,打日志,警告維持的連接會提前退休
        log()
    if idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize():
        # 清理過期連接
        for connection in connectionBag_NotInUse:
            if is_timeout(connection):
                closeConnection(connection)
    # 初始化或清理過后
    fillPool() # 填充連接池,保證最小的連接數量(idleConnections),同時保證不超過最大連接數量(maximumPoolSize)        

從偽代碼中可以看出,HouseKeeper主要的工作分為:

  1. 檢測系統時間的準確性,系統時間被用戶篡改過,則處理清空連接池
  2. 對空閑的連接,根據idleTimeout屬性清理過期連接
  3. 在初始化或清理過后,填充新的連接到連接池中

關閉連接的過程(closeConnection)和填充的過程(fillPool)則分別是通過向上圖中的closeConnectionExecutoraddConnectionExecutor連接池提交異步任務的方式,分別執行JDBC連接關閉和異步的建立JDBC連接的。

hikari優化的連接池存儲容器 - ConcurrentBag

通過上面的分析,我們知道了hikari是通過在HikariPool這樣的數據結構中執行30s一次的定時任務動態關閉/新增連接到連接池的。維護連接池中的連接就需要用到一種 容器,由于新增和關閉連接都是通過線程池異步執行的,而且getConnection()的操作大多數情況下是并發的,必然涉及到支持 并發。在Hikari中是通過自定義的容器類ConcurrentBag來維護JDBC連接的。

ConcurrentBag的結構

ConcurrentBag的結構如下圖,重點關注

  • sharedList和threadList,是兩個關于PoolEntry(JdbcConnection的封裝)的列表,用來實際存儲連接池中維護的Jdbc連接
  • listener,是指向HikariPool的引用,當ConcurrentBag獲取連接時發現用完了,則通過listener的回調接口請求HikariPool多擴充點Jdbc連接入池
  • handoffQueue,和listener相關,HikariPool擴充連接池后將新的連接通過該隊列提供給正在等待的線程,正在getConnection的線程會在queue的另一端阻塞住,等待擴充連接后喂給它們
ConcurrentBag的結構

PoolEntry,可以簡單理解為Hikari對JdbcConnection的一層封裝,有四種狀態

  • int STATE_NOT_IN_USE = 0; 沒在用
  • int STATE_IN_USE = 1; 使用中
  • int STATE_REMOVED = -1; 已移除
  • int STATE_RESERVED = -2; 預定

ConcurrentBag#add

新增的接口很簡單,這里直接貼出源碼。

public void add(final T bagEntry)
   {
      if (closed) {
         LOGGER.info("ConcurrentBag has been closed, ignoring add()");
         throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()");
      }

      sharedList.add(bagEntry);

      // spin until a thread takes it or none are waiting
      while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) {
         yield();
      }
   }

從代碼中可以看出主要的工作分為兩步:

  1. sharedList.add()
  2. 如果有等待中的線程(正在getConnection),則嘗試將新加入的JDBC連接通過隊列handoffQueue喂給它

ConcurrentBag#remove

  1. 通過cas確保當前這個connection在使用中或被預定,使用中是在getConnection的時候通過探活(select 1)發現連接已經已經失效時,將其關閉;被預定是在HouseKeeper管家第2步對空閑的連接清理的時候,提前將要清理的連接鎖住,并且執行close操作時。
  2. sharedList.remove(bagEntry); -- 從list中移除

ConcurrentBag#borrow

borrow方法的偽代碼如下。

def borrow():
   if (bagEntry = find_from_threadList()) != null: # 1.優先從threadList中找
       return bagEntry
   waiters++
   try:
       for bagEntry in sharedList: # 2. 從sharedList中找
           if not_in_use(bagEntry):
               if waiters > 1: 
                   # 2.1 如果從sharedList中找到了,但是有waiting的,大概率是在并發情況下搶到了別人的,那就幫他再申請一個JDBC連接
                   listener.addBagItem(waiters - 1);
               return bagEntry
       # 3. sharedList中沒找到,則申請新的JDBC連接
       listener.addBagItem(waiting);
       # 4. 申請了新的JDBC連接后,站在handoffQueue的一邊,等待JDBC連接創建好的結果
       while(not_timeout()):
           bagEntry = handoffQueue.poll()
           if not_in_use(bagEntry):
               return bagEntry
    finally:
        waiters--

ConcurrentBag、hikari與Spring-JDBC

上面的解釋可能太過底層和抽象,基于上面對ConcurrentBag主要方法的源碼分析,我們這里可以將hikari的ConcurrentBag與Spring-JDBC整個串起來,分析一條sql語句執行的整個過程。

ConcurrentBag、hikari與Spring-JDBC

以語句select * from xxx為例,

  1. 在業務代碼層面通過調用Spring-JDBC提供的JdbcTemplate的query方法,獲取查詢結果
  2. JdbcTemplate調用內部的execute方法時,調用了HikariDataSource#getConnection方法(dataSource可以通過手動注入,或SpringIOC注入)
  3. HikariDataSource通過向其內部維護的連接池對象HikariPool請求getConnection獲取連接(注意,這里有兩個HikariPool,是hikari的一種優化手段,下文中會詳細分析)
  4. HikariPool通過向其內部維護的connectionBag(即ConcurrentBag對象)借用(borrow)來獲取連接,借用的過程就是從寫時拷貝list sharedListthreadLocal threadList獲取連接,最終再一層層返回給JdbcTemplate,由JdbcTemplate執行sql語句,并將最終結果返回給業務代碼
  5. 在第4步中,可能存在ConcurrentBag中維護的連接不夠用的情況,這時候會通過listener指向的HikariPool請求擴充連接池中的連接

綜上,我們可以看出,最終一條sql是通過這樣層層抽象封裝來實現的,

  • HikariDataSource只是為了對Spring-JDBC的DataSource接口做適配而產生的
  • HikariPool才是Hikari的核心功能,它像是一個專門負責管理DB連接的管家,背著一個名叫ConcurrentBag的重重的背包,在你(業務側)需要JDBC連接的時候提供給你,同時管家自己會是不是檢查和擴充背包里的連接(就像跟女神和高富帥一起爬山時跟在最后背包的工具人)淚目( ? ^ ? )
工具人

Hikari的優化點

通過上面的分析,讀者應該已經清楚了Hikari的大致原理,有了大致的了解之后,Hikari的代碼實現的很多細節通過閱讀源碼也可以細化了解。我們這里回到最關鍵的問題,Hikari為什么這么快,為什么敢用 來命名自己。

字節碼精簡

字節碼精簡:優化代碼,直到編譯后的字節碼最少,這樣,CPU緩存可以加載更多的程序代碼;
優化代理和攔截器:減少代碼,例如HikariCP的Statement proxy只有100行代碼,只有BoneCP的十分之一

以上是從網上搜到的,確實可以體現出Hikari極致優化的特性,但是對性能影響可能微乎其微。

fastPathPool 和 pool

如上面關于HikariDataSource與HikariPool的關系時,我們可以看到HikariDataSource內部有兩個指向HikariPool的引用。這里也是Hikari優化性能的一種手段。

fastPathPool 和 pool

HikariDataSource有兩種初始化的方式:

  • 一種是無參數的初始化方式,這種時候當執行到getConnection的時候,會通過 懶漢單例模式 懶加載初始化HikariPool,賦值給pool引用,懶漢模式涉及到并發情況,所以一般會考慮采用 Double-Check-Lock(DCL) 方式加鎖,DCL方式中涉及到JVM的字節碼指令重排優化的問題,所以需要將pool引用設置為volatile,廢棄掉JVM的字節碼指令優化。(【5】Java中DCL(Double-Check-Lock)對volatile必要性的疑惑)但是同時因為volatile關鍵字,導致pool相關的加載都會有性能問題。所以獲取HikariPool時優先通過fastPathPool引用。
  • 另一種是帶參數的初始化,在構造函數中同時將初始化完的HikariPool賦值給pool和fastPathPool引用,在獲取HikariPool引用的時候總是優先獲取fastPathPool,以防止volatile關鍵字對性能的影響


    構造函數

自定義的容器類型

1、 定義數組類型(FastStatementList)代替ArrayList:避免每次get()調用都要進行range check,避免調用remove()時的從頭到尾的掃描;【6】【追光者系列】HikariCP源碼分析之FastList

2、 自定義集合類型(ConcurrentBag):提高并發讀寫的效率;
通過之前對ConcurrentBag的borrow方法的了解,我們知道,hikariPool是通過ConcurrentBag#borrow方法來獲取連接的,而ConcurrentBag中的邏輯則是:優先從threadList(ThreadLocal)獲取;獲取不到時再從sharedList(CopyOnWriteArrayList)獲取

ThreadLocal:例如業務代碼執行了兩次SQL,獲取了兩次JDBC連接,在第一次執行完畢之后ConcurrentBag會回收該連接,但是會回收到ThreadLocal中。當業務代碼第二次執行SQl需要獲取JDBC連接時,只要是同一個線程,則會從ThreadLocal中獲取到連接。(關于ThreadLocal的原理,可以參考筆者的往期文章 【7】圖解分析ThreadLocal的原理與應用場景

threadList

CopyOnWriteArrayList:寫時拷貝技術,并發情況下線程安全版本的ArrayList,寫時拷貝技術源自于unix系統的fork系統調用,指的是讀操作時不加鎖,只有當寫操作執行的時候鎖住整個list,然后執行替換(貍貓換太子)

CopyOnWriteArrayList
CopyOnWriteArrayList

擴展知識

事務管理器transactionManager

在需要支持事務操作的方法上,我們通常會加上這樣的注解, @Transactional(rollbackFor = Exception.class, value = "txManagerXXX"), 用來表示注解包裹住的方法內部是一個事務,需要符合事務的特性(如原子性)
在Spring中的實現是通過AOP切面技術生成了動態代理,封裝了@Transactional注解的方法

transactionManager

JNDI

JNDI,全稱是 Java Naming and Directory Interface,Java 命名與目錄接口,也是跟原始的JDBC用法息息相關的概念。數據庫的實現有很多種,Java服務是通過JDBC驅動類來獲取JDBC連接的,那么原始的JDBC用法就會有幾種問題:

  1. 數據庫的地址、賬戶名、密碼、連接池的參數發生改變,要修改代碼
  2. 數據庫可能從MySQL改為Oracle或者其它DB,需要修改底層代碼,換JDBC驅動jar包,由此會引發一系列的相關功能的回歸

基于以上問題,J2EE規范提出了JNDI的規范,對于業務代碼來說只需要關心獲取的DataSource是哪個(通過naming-名字),不需要知道這個DataSource背后的細節(url地址、賬戶名密碼等),即findResouceByName,而resources的相關參數是配置在xml文件里的。
經常用Spring的讀者可以看出,這就是SpringIOC容器提供的getBean(String beanName)方法,即Spring的xml文件,所以SpringIOC就是在J2EE的規范上實現的。(【8】Spring IOC 前世今生之 JDNI - binarylei - 博客園

references

  1. ^ Introduction to HikariCP | Baeldung
  2. ^ GitHub - brettwooldridge/HikariCP: 光 HikariCP?A solid, high-performance, JDBC connection pool at last.
  3. ^ HikariCP連接池 - 簡書
  4. ^ 源碼詳解系列(八) ------ 全面講解HikariCP的使用和源碼 - 子月生 - 博客園
  5. ^ Java中DCL(Double-Check-Lock)對volatile必要性的疑惑_u010131029的博客-CSDN博客
  6. ^ 【追光者系列】HikariCP源碼分析之FastList_weixin_34304013的博客-CSDN博客
  7. ^ 圖解分析ThreadLocal的原理與應用場景
  8. ^ Spring IOC 前世今生之 JDNI - binarylei - 博客園
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379