SpringBoot 2.0 中默認 HikariCP 數據庫連接池原理解析

作為后臺服務開發,在日常工作中我們天天都在跟數據庫打交道,一直在進行各種CRUD操作,都會使用到數據庫連接池。按照發展歷程,業界知名的數據庫連接池有以下幾種:c3p0、DBCP、Tomcat JDBC Connection Pool、Druid 等,不過最近最火的是 HiKariCP。

HiKariCP 號稱是業界跑得最快的數據庫連接池,自從 SpringBoot 2.0 將其作為默認數據庫連接池后,其發展勢頭銳不可當。那它為什么那么快呢?今天咱們就重點聊聊其中的原因。

數據庫連接池?

在講解HiKariCP之前,我們先簡單介紹下什么是數據庫連接池(Database Connection Pooling),以及為什么要有數據庫連接池。

從根本上而言,數據庫連接池和我們常用的線程池一樣,都屬于池化資源,它在程序初始化時創建一定數量的數據庫連接對象并將其保存在一塊內存區中。它允許應用程序重復使用一個現有的數據庫連接,當需要執行 SQL 時,我們是直接從連接池中獲取一個連接,而不是重新建立一個數據庫連接,當 SQL 執行完,也并不是將數據庫連接真的關掉,而是將其歸還到數據庫連接池中。我們可以通過配置連接池的參數來控制連接池中的初始連接數、最小連接、最大連接、最大空閑時間等參數,來保證訪問數據庫的數量在一定可控制的范圍類,防止系統崩潰,同時保證用戶良好的體驗。數據庫連接池示意圖如下所示:

因此使用數據庫連接池的核心作用,就是避免數據庫連接頻繁創建和銷毀,節省系統開銷。因為數據庫連接是有限且代價昂貴,創建和釋放數據庫連接都非常耗時,頻繁地進行這樣的操作將占用大量的性能開銷,進而導致網站的響應速度下降,甚至引起服務器崩潰。

?HikariCP 數據庫連接池簡介

HikariCP 號稱是史上性能最好的數據庫連接池,SpringBoot 2.0將它設置為默認的數據源連接池。Hikari相比起其它連接池的性能高了非常多,那么,這是怎么做到的呢?通過查看HikariCP官網介紹,對于HikariCP所做優化總結如下:

1. 字節碼精簡 :優化代碼,編譯后的字節碼量極少,使得CPU緩存可以加載更多的程序代碼;

HikariCP在優化并精簡字節碼上也下了功夫,使用第三方的Java字節碼修改類庫Javassist來生成委托實現動態代理.動態代理的實現在ProxyFactory類,速度更快,相比于JDK Proxy生成的字節碼更少,精簡了很多不必要的字節碼。

2. 優化代理和攔截器:減少代碼,例如HikariCP的Statement proxy只有100行代碼,只有BoneCP的十分之一;

3. 自定義數組類型(FastStatementList)代替ArrayList:避免ArrayList每次get()都要進行range check,避免調用remove()時的從頭到尾的掃描(由于連接的特點是后獲取連接的先釋放);

4. 自定義集合類型(ConcurrentBag):提高并發讀寫的效率;

5. 其他針對BoneCP缺陷的優化,比如對于耗時超過一個CPU時間片的方法調用的研究。

當然作為一個數據庫連接池,不能說快就會被消費者所推崇,它還具有非常好的健壯性及穩定性。HikariCP從15年推出以來,已經經受了廣大應用市場的考驗,并且成功地被SpringBoot2.0作為默認數據庫連接池進行推廣,在可靠性上面是值得信任的。其次借助于其代碼量少,占用cpu和內存量小的優點,使得它的執行率非常高。最后,Spring配置HikariCP和druid基本沒什么區別,遷移過來非常方便,這些都是為什么HikariCP目前如此受歡迎的原因。

HikariCP 核心源碼解析?

?1、FastList 是如何優化性能問題的

首先我們來看一下執行數據庫操作規范化的操作步驟:

  1. 通過數據源獲取一個數據庫連接;
  2. 創建 Statement;
  3. 執行 SQL;
  4. 通過 ResultSet 獲取 SQL 執行結果;
  5. 釋放 ResultSet;
  6. 釋放 Statement;
  7. 釋放數據庫連接

當前所有數據庫連接池都是嚴格地根據這個順序來進行數據庫操作的,為了防止最后的釋放操作,各類數據庫連接池都會把創建的 Statement 保存在數組 ArrayList 里,來保證當關閉連接的時候,可以依次將數組中的所有 Statement 關閉。HiKariCP 在處理這一步驟中,認為 ArrayList 的某些方法操作存在優化空間,因此對List接口的精簡實現,針對List接口中核心的幾個方法進行優化,其他部分與ArrayList基本一致 。

首先是get()方法,ArrayList每次調用get()方法時都會進行rangeCheck檢查索引是否越界,FastList的實現中去除了這一檢查,是因為數據庫連接池滿足索引的合法性,能保證不會越界,此時rangeCheck就屬于無效的計算開銷,所以不用每次都進行越界檢查。省去頻繁的無效操作,可以明顯地減少性能消耗。

  • FastList get()操作
public T get(int index)
{// ArrayList 在此多了范圍檢測 rangeCheck(index);return elementData[index];
}

其次是remove方法,當通過 conn.createStatement() 創建一個 Statement 時,需要調用 ArrayList 的 add() 方法加入到 ArrayList 中,這個是沒有問題的;但是當通過 stmt.close() 關閉 Statement 的時候,需要調用 ArrayList 的 remove() 方法來將其從 ArrayList 中刪除,而ArrayList的remove(Object)方法是從頭開始遍歷數組,而FastList是從數組的尾部開始遍歷,因此更為高效。

假設一個 Connection 依次創建 6 個 Statement,分別是 S1、S2、S3、S4、S5、S6,而關閉 Statement 的順序一般都是逆序的,從S6 到 S1,而 ArrayList 的 remove(Object o) 方法是順序遍歷查找,逆序刪除而順序查找,這樣的查找效率就太慢了。因此FastList對其進行優化,改成了逆序查找。如下代碼為FastList 實現的數據移除操作,相比于ArrayList的 remove()代碼, FastList 去除了檢查范圍 和 從頭到尾遍歷檢查元素的步驟,其性能更快。

?

public boolean remove(Object element)
{// 刪除操作使用逆序查找for (int index = size - 1; index >= 0; index--) {if (element == elementData[index]) {final int numMoved = size - index - 1;// 如果角標不是最后一個,復制一個新的數組結構if (numMoved > 0) {System.arraycopy(elementData, index + 1, elementData, index, numMoved);}//如果角標是最后面的 直接初始化為nullelementData[--size] = null;return true;}}return false;
}

通過上述源碼分析,FastList 的優化點還是很簡單的。相比ArrayList僅僅是去掉了rage檢查,擴容優化等細節處,刪除時數組從后往前遍歷查找元素等微小的調整,從而追求性能極致。當然FastList 對于 ArrayList 的優化,我們不能說ArrayList不好。所謂定位不同、追求不同,ArrayList作為通用容器,更追求安全、穩定,操作前rangeCheck檢查,對非法請求直接拋出異常,更符合 fail-fast(快速失敗)機制,而FastList追求的是性能極致。

下面我們再來聊聊 HiKariCP 中的另外一個數據結構 ConcurrentBag,看看它又是如何提升性能的。

2、ConcurrentBag 實現原理分析?

當前主流數據庫連接池實現方式,大都用兩個阻塞隊列來實現。一個用于保存空閑數據庫連接的隊列 idle,另一個用于保存忙碌數據庫連接的隊列 busy;獲取連接時將空閑的數據庫連接從 idle 隊列移動到 busy 隊列,而關閉連接時將數據庫連接從 busy 移動到 idle。這種方案將并發問題委托給了阻塞隊列,實現簡單,但是性能并不是很理想。因為 Java SDK 中的阻塞隊列是用鎖實現的,而高并發場景下鎖的爭用對性能影響很大。

HiKariCP 并沒有使用 Java SDK 中的阻塞隊列,而是自己實現了一個叫做 ConcurrentBag 的并發容器,在連接池(多線程數據交互)的實現上具有比LinkedBlockingQueue和LinkedTransferQueue更優越的性能。

ConcurrentBag 中最關鍵的屬性有 4 個,分別是:用于存儲所有的數據庫連接的共享隊列 sharedList、線程本地存儲 threadList、等待數據庫連接的線程數 waiters 以及分配數據庫連接的工具 handoffQueue。其中,handoffQueue 用的是 Java SDK 提供的 SynchronousQueue,SynchronousQueue 主要用于線程之間傳遞數據。

  • ConcurrentBag 中的關鍵屬性

// 存放共享元素,用于存儲所有的數據庫連接
private final CopyOnWriteArrayList<T> sharedList;
// 在 ThreadLocal 緩存線程本地的數據庫連接,避免線程爭用
private final ThreadLocal<List<Object>> threadList;
// 等待數據庫連接的線程數
private final AtomicInteger waiters;
// 接力隊列,用來分配數據庫連接
private final SynchronousQueue<T> handoffQueue;

ConcurrentBag 保證了全部的資源均只能通過 add() 方法進行添加,當線程池創建了一個數據庫連接時,通過調用 ConcurrentBag 的 add() 方法加入到 ConcurrentBag 中,并通過 remove() 方法進行移出。下面是 add() 方法和 remove() 方法的具體實現,添加時實現了將這個連接加入到共享隊列 sharedList 中,如果此時有線程在等待數據庫連接,那么就通過 handoffQueue 將這個連接分配給等待的線程。

  • ConcurrentBag 的 add() 與 remove() 方法
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()");}// 新添加的資源優先放入sharedListsharedList.add(bagEntry);// 當有等待資源的線程時,將資源交到等待線程 handoffQueue 后才返回while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) {yield();}
}
public boolean remove(final T bagEntry)
{// 如果資源正在使用且無法進行狀態切換,則返回失敗if (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) && !bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) && !closed) {LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry);return false;}// 從sharedList中移出/*fin查看線程本地存儲 threadList 中是否有空閑連接,如果有,則返回一個空閑的連接;如果線程本地存儲中無空閑連接,則從共享隊列 sharedList 中獲取;如果共享隊列中也沒有空閑的連接,則請求線程需要等待。*/if (!removed && !closed) {LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry);}return removed;
}

同時ConcurrentBag通過提供的 borrow() 方法來獲取一個空閑的數據庫連接,并通過requite()方法進行資源回收,borrow() 的主要邏輯是:

  1. 查看線程本地存儲 threadList 中是否有空閑連接,如果有,則返回一個空閑的連接;
  2. 如果線程本地存儲中無空閑連接,則從共享隊列 sharedList 中獲取;
  3. 如果共享隊列中也沒有空閑的連接,則請求線程需要等待。
  • ConcurrentBag 的 borrow() 與 requite() 方法

// 該方法會從連接池中獲取連接, 如果沒有連接可用, 會一直等待timeout超時
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
{// 首先查看線程本地資源threadList是否有空閑連接final List<Object> list = threadList.get();// 從后往前反向遍歷是有好處的, 因為最后一次使用的連接, 空閑的可能性比較大, 之前的連接可能會被其他線程提前借走了for (int i = list.size() - 1; i >= 0; i--) {final Object entry = list.remove(i);@SuppressWarnings("unchecked")final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;// 線程本地存儲中的連接也可以被竊取, 所以需要用CAS方法防止重復分配if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {return bagEntry;}}// 當無可用本地化資源時,遍歷全部資源,查看可用資源,并用CAS方法防止資源被重復分配final int waiting = waiters.incrementAndGet();try {for (T bagEntry : sharedList) {if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {// 因為可能“搶走”了其他線程的資源,因此提醒包裹進行資源添加if (waiting > 1) {listener.addBagItem(waiting - 1);}return bagEntry;}}listener.addBagItem(waiting);timeout = timeUnit.toNanos(timeout);do {final long start = currentTime();// 當現有全部資源都在使用中時,等待一個被釋放的資源或者一個新資源final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {return bagEntry;}timeout -= elapsedNanos(start);} while (timeout > 10_000);return null;}finally {waiters.decrementAndGet();}
}public void requite(final T bagEntry)
{// 將資源狀態轉為未在使用bagEntry.setState(STATE_NOT_IN_USE);// 判斷是否存在等待線程,若存在,則直接轉手資源for (int i = 0; waiters.get() > 0; i++) {if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {return;}else if ((i & 0xff) == 0xff) {parkNanos(MICROSECONDS.toNanos(10));}else {yield();}}// 否則,進行資源本地化處理final List<Object> threadLocalList = threadList.get();if (threadLocalList.size() < 50) {threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);}
}

borrow() 方法可以說是整個 HikariCP 中最核心的方法,它是我們從連接池中獲取連接的時候最終會調用到的方法。需要注意的是 borrow() 方法只提供對象引用,不移除對象,因此使用時必須通過 requite() 方法進行放回,否則容易導致內存泄露。requite() 方法首先將數據庫連接狀態改為未使用,之后查看是否存在等待線程,如果有則分配給等待線程;否則將該數據庫連接保存到線程本地存儲里

ConcurrentBag 實現采用了queue-stealing的機制獲取元素:首先嘗試從ThreadLocal中獲取屬于當前線程的元素來避免鎖競爭,如果沒有可用元素則再次從共享的CopyOnWriteArrayList中獲取。此外,ThreadLocal和CopyOnWriteArrayList在ConcurrentBag中都是成員變量,線程間不共享,避免了偽共享(false sharing)的發生。同時因為線程本地存儲中的連接是可以被其他線程竊取的,在共享隊列中獲取空閑連接,所以需要用 CAS 方法防止重復分配。

總結

本文首先對為什么使用數據庫連接池做介紹,通過分析HiKariCP官網介紹及其源碼,可以發現HiKariCP主要通過對字節碼進行精簡、優化代理和攔截器、自定義數組類型 FastList 及自定義并發集合類型 ConcurrentBag 等內容進行優化,文中重點講解了FastList 與ConcurrentBag 的優化原理(FastList 適用于逆序刪除場景;而 ConcurrentBag 本質上是通過 ThreadLocal 將連接池中的連接按照線程做一次預分配,避免直接競爭共享資源,減少并發CAS帶來的CPU CACHE的頻繁失效,從而提高性能,非常適合池化資源的分配),達到顯著提升數據庫連接池性能的效果。需要注意的是threadLocal可能帶來連接池關閉時引用還存在的情況,有可能導致內存泄露,因此一定要使用requite()方法來進行資源回收處理。

Hikari 作為 SpringBoot2.0默認的連接池,目前在行業內使用范圍非常廣,對于大部分業務來說,都可以實現快速接入使用,做到高效連接。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/536803.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/536803.shtml
英文地址,請注明出處:http://en.pswp.cn/news/536803.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

阿里云服務器記錄

阿里云服務器記錄 CentOS 8.4 64位 SCC版 CentOS 7.9 64位 SCC版 CentOS 7.9 64位 CentOS 7.9 64位 UEFI版 Alibaba Cloud Linux Anolis OS CentOS Windows Server Ubuntu Debian Fedora OpenSUSE Rocky Linux CentOS Stream AlmaLinux 阿里云服務器有個scc版&#xff0c;這個…

Flask+Mysql項目docker-compose部署(Pythondocker-compose詳細步驟)

一、前言 環境&#xff1a; Linux、docker、docker-compose、python(Flask)、Mysql 簡介&#xff1a; 簡單使用Flask框架寫的查詢Mysql數據接口&#xff0c;使用docker部署&#xff0c;shell腳本啟動 優勢&#xff1a; 采用docker方式部署更加便于維護&#xff0c;更加簡單快…

如何在Go中使用模板

引言 您是否需要以格式良好的輸出、文本報告或HTML頁面呈現一些數據?你可以使用Go模板來做到這一點。任何Go程序都可以使用text/template或html/template包(兩者都包含在Go標準庫中)來整齊地顯示數據。 這兩個包都允許你編寫文本模板并將數據傳遞給它們,以按你喜歡的格式呈…

“C語言“——scanf()、getchar() 、putchar()、之間的關系

scanf函數說明 scanf函數是對來自于標準輸入流的輸入數據作格式轉換&#xff0c;并將轉換結果保存至format后面的實參所指向的對象。 而const char*format 指向的字符串為格式控制字符串&#xff0c;它指定了可輸入的字符串以及賦值時轉換方法。 簡單來說給一個打印格式(輸入…

【并發編程篇】源碼分析,手動創建線程池

文章目錄 &#x1f6f8;前言&#x1f339;Executors的三大方法 &#x1f354;簡述線程池&#x1f386;手動創建線程池?源碼分析?代碼實現&#xff0c;手動創建線程池&#x1f388;CallerRunsPolicy()&#x1f388;AbortPolicy()&#x1f388;DiscardPolicy()&#x1f388;Dis…

LNPMariadb數據庫分離|web服務器集群

LNP&Mariadb數據庫分離&#xff5c;web服務器集群 網站架構演變單機版LNMP獨立數據庫服務器web服務器集群與Session保持 LNP與數據庫分離1. 準備一臺獨立的服務器&#xff0c;安裝數據庫軟件包2. 將之前的LNMP網站中的數據庫遷移到新的數據庫服務器3. 修改wordpress網站配置…

2023.12.24 關于 Redis 中 String 類型內部編碼 及 應用場景

目錄 String 類型內部編碼 3 種內部編碼方式 String 類型應用場景 Cache 緩存 鍵名命名規則 計數&#xff08;Counter&#xff09; 共享會話&#xff08;Session &#xff09; 手機驗證碼 總結 String 類型內部編碼 3 種內部編碼方式 int&#xff1a;用來表示 64 位 —…

vue3菜單權限管理實現

前提 你的菜單是根據路由動態生成的&#xff0c;具體可以參考這篇博客對el-menu組件進行遞歸封裝&#xff08;根據路由配置動態生成&#xff09; 描述 首先將路由分為常量路由constantRoute&#xff08;所有用戶都有的路由&#xff09;和異步路由asyncRoute&#xff08;需要動…

Gradle 插件

自定義Gradle插件 - 簡書

小天使的小難題:新生兒疝氣的關注與溫馨呵護

引言&#xff1a; 新生兒疝氣是一種在出生后可能出現的常見情況&#xff0c;雖然通常不會造成長期影響&#xff0c;但對于家長而言&#xff0c;了解如何正確應對新生兒疝氣是至關重要的。本文將深入探討新生兒疝氣的原因、癥狀&#xff0c;以及家長在面對這一問題時應該采取的…

1224. 交換瓶子(藍橋杯/圖論)

題目&#xff1a; 1224. 交換瓶子 - AcWing題庫 輸入樣例1&#xff1a; 5 3 1 2 5 4輸出樣例1&#xff1a; 3輸入樣例2&#xff1a; 5 5 4 3 2 1輸出樣例2&#xff1a; 2 思路&#xff1a;圖論 1.將對應的位置與當前的瓶子序列相連形成環。 2.最少交換次數能形成的最多…

Vue中的事件委托(事件代理)使用方法介紹

事件委托&#xff08;事件代理&#xff09; 將原本需要綁定在子元素上的事件監聽器委托在父元素上&#xff0c;讓父元素充當事件監聽的職務。 事件委托是一種利用事件冒泡的特性&#xff0c;在父節點上響應事件&#xff0c;而不是在子節點上響應事件的技術。它能夠改善性能&a…

如何理解JDK、JRE、JVM區別與聯系

摘要&#xff1a;JDK是 Java 語言的軟件開發工具包(SDK)。在JDK的安裝目錄下有一個jre目錄&#xff0c;里面有兩個文件夾bin和lib&#xff0c;在這里可以認為bin里的就是jvm&#xff0c;lib中則是jvm工作所需要的類庫&#xff0c;而jvm和 lib合起來就稱為jre。 一、JDK JDK(Ja…

【【迭代16次的CORDIC算法-verilog實現】】

迭代16次的CORDIC算法-verilog實現 -32位迭代16次verilog代碼實現 CORDIC.v module cordic32#(parameter DATA_WIDTH 8d32 , // we set data widthparameter PIPELINE 5d16 // Optimize waveform)(input …

第十四章Java博客

lambda就是數學中的“λ”的讀音&#xff0c;lambda表達式是基于λ演算而得名的&#xff0c;因為lambda抽象&#xff08;lambda abstraction&#xff09;表示一個匿名的函數&#xff0c;于是開發語言也將lambda表達式用來表示匿名函數&#xff0c;也就是沒有函數名字的函數。C#…

maven管理工具使用package打包的時候無法將lib文件夾下的第三方jar包打入,上線打jar包后運行異常問題

問題描述&#xff1a; 調用第三方接口的時候通過手動引入了第三方的兩個jar包到我本項目的lib文件夾下&#xff0c;并在pom文件添加入下配置&#xff1a; <dependency><groupId>cn.xxxx.xxxx.core</groupId><artifactId>xxxx-core</artifactId>…

Spring Boot 中實現跨域的幾種方式

前言 在現代Web應用中&#xff0c;由于安全性和隱私的考慮&#xff0c;瀏覽器限制了從一個域向另一個域發起的跨域HTTP請求。解決這個問題的一種常見方式是實現跨域資源共享&#xff08;CORS&#xff09;。Spring Boot提供了多種方式來處理跨域請求&#xff0c;本文將介紹其中的…

C語言字符串處理提取時間(ffmpeg返回的時間字符串)

【1】需求 需求&#xff1a;有一個 “00:01:33.90” 這樣格式的時間字符串&#xff0c;需要將這個字符串的時間值提取打印出來&#xff08;提取時、分、秒、毫秒&#xff09;。 這個時間字符串從哪里來的&#xff1f; 是ffmpeg返回的時間&#xff0c;也就是視頻的總時間。 下…

vs快捷鍵

ctrlMo 折疊代碼塊 ctrlML 打開代碼塊

電子電器架構(E/E)演化 —— 主流主機廠域集中架構概述

電子電器架構(E/E)演化 —— 主流主機廠域集中架構概述 我是穿拖鞋的漢子,魔都中堅持長期主義的汽車電子工程師。 老規矩,分享一段喜歡的文字,避免自己成為高知識低文化的工程師: 屏蔽力是信息過載時代一個人的特殊競爭力,任何消耗你的人和事,多看一眼都是你的不對。…