做爲後臺服務開發,在平常工做中咱們每天都在跟數據庫打交道,一直在進行各類CRUD操做,都會使用到數據庫鏈接池。按照發展歷程,業界知名的數據庫鏈接池有如下幾種:c3p0、DBCP、Tomcat JDBC Connection Pool、Druid 等,不過最近最火的是 HiKariCP。git
HiKariCP 號稱是業界跑得最快的數據庫鏈接池,自從 SpringBoot 2.0 將其做爲默認數據庫鏈接池後,其發展勢頭銳不可當。那它爲何那麼快呢?今天我們就重點聊聊其中的緣由。github
在講解HiKariCP以前,咱們先簡單介紹下什麼是數據庫鏈接池(Database Connection Pooling),以及爲何要有數據庫鏈接池。sql
從根本上而言,數據庫鏈接池和咱們經常使用的線程池同樣,都屬於池化資源,它在程序初始化時建立必定數量的數據庫鏈接對象並將其保存在一塊內存區中。它容許應用程序重複使用一個現有的數據庫鏈接,當須要執行 SQL 時,咱們是直接從鏈接池中獲取一個鏈接,而不是從新創建一個數據庫鏈接,當 SQL 執行完,也並非將數據庫鏈接真的關掉,而是將其歸還到數據庫鏈接池中。咱們能夠經過配置鏈接池的參數來控制鏈接池中的初始鏈接數、最小鏈接、最大鏈接、最大空閒時間等參數,來保證訪問數據庫的數量在必定可控制的範圍類,防止系統崩潰,同時保證用戶良好的體驗。數據庫鏈接池示意圖以下所示:數據庫
所以使用數據庫鏈接池的核心做用,就是避免數據庫鏈接頻繁建立和銷燬,節省系統開銷。由於數據庫鏈接是有限且代價昂貴,建立和釋放數據庫鏈接都很是耗時,頻繁地進行這樣的操做將佔用大量的性能開銷,進而致使網站的響應速度降低,甚至引發服務器崩潰。數組
這裏詳細總結了常見數據庫鏈接池的各項功能比較,咱們重點分析下當前主流的阿里巴巴Druid與HikariCP,HikariCP在性能上是徹底優於Druid鏈接池的。而Druid的性能稍微差點是因爲鎖機制的不一樣,而且Druid提供更豐富的功能,包括監控、sql攔截與解析等功能,二者的側重點不同,HikariCP追求極致的高性能。緩存
下面是官網提供的性能對比圖,在性能上面這五種數據庫鏈接池的排序以下:HikariCP>druid>tomcat-jdbc>dbcp>c3p0:tomcat
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目前如此受歡迎的緣由。
字節碼精簡、優化代理和攔截器、自定義數組類型。
首先咱們來看一下執行數據庫操做規範化的操做步驟:
經過數據源獲取一個數據庫鏈接;
建立 Statement;
執行 SQL;
經過 ResultSet 獲取 SQL 執行結果;
釋放 ResultSet;
釋放 Statement;
- 釋放數據庫鏈接。
當前全部數據庫鏈接池都是嚴格地根據這個順序來進行數據庫操做的,爲了防止最後的釋放操做,各種數據庫鏈接池都會把建立的 Statement 保存在數組 ArrayList 裏,來保證當關閉鏈接的時候,能夠依次將數組中的全部 Statement 關閉。HiKariCP 在處理這一步驟中,認爲 ArrayList 的某些方法操做存在優化空間,所以對List接口的精簡實現,針對List接口中核心的幾個方法進行優化,其餘部分與ArrayList基本一致 。
首先是get()方法,ArrayList每次調用get()方法時都會進行rangeCheck檢查索引是否越界,FastList的實現中去除了這一檢查,是由於數據庫鏈接池知足索引的合法性,能保證不會越界,此時rangeCheck就屬於無效的計算開銷,因此不用每次都進行越界檢查。省去頻繁的無效操做,能夠明顯地減小性能消耗。
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,分別是 S一、S二、S三、S四、S五、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); } //若是角標是最後面的 直接初始化爲null elementData[--size] = null; return true; } } return false; }
經過上述源碼分析,FastList 的優化點仍是很簡單的。相比ArrayList僅僅是去掉了rage檢查,擴容優化等細節處,刪除時數組從後往前遍歷查找元素等微小的調整,從而追求性能極致。固然FastList 對於 ArrayList 的優化,咱們不能說ArrayList很差。所謂定位不一樣、追求不一樣,ArrayList做爲通用容器,更追求安全、穩定,操做前rangeCheck檢查,對非法請求直接拋出異常,更符合 fail-fast(快速失敗)機制,而FastList追求的是性能極致。
下面咱們再來聊聊 HiKariCP 中的另一個數據結構 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 主要用於線程之間傳遞數據。
// 存放共享元素,用於存儲全部的數據庫鏈接 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 將這個鏈接分配給等待的線程。
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 sharedList.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中移出 final boolean removed = sharedList.remove(bagEntry); if (!removed && !closed) { LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry); } return removed; }
同時ConcurrentBag經過提供的 borrow() 方法來獲取一個空閒的數據庫鏈接,並經過requite()方法進行資源回收,borrow() 的主要邏輯是:
// 該方法會從鏈接池中獲取鏈接, 若是沒有鏈接可用, 會一直等待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 方法防止重複分配。
Hikari 做爲 SpringBoot2.0默認的鏈接池,目前在行業內使用範圍很是廣,對於大部分業務來講,均可以實現快速接入使用,作到高效鏈接。
做者:vivo 遊戲技術團隊