實際工做中,咱們總會不免和數據庫打交道;只要和數據庫打交道,就免不了使用數據庫鏈接池。業界知名的數據庫鏈接池有很多,例如 DBCP、Tomcat JDBC Connection Pool、Druid 等,不過最近最火的是 HiKariCP。git
HiKariCP 號稱是業界跑得最快的數據庫鏈接池,尤爲是 Springboot 2.0 將其做爲默認數據庫鏈接池
後,地位已經是毋庸置疑了。那它爲何那麼快呢?帶着問題咱們來看下。github
在詳細分析 HiKariCP 高性能以前,咱們有必要先簡單介紹一下什麼是數據庫鏈接池。本質上,數據庫鏈接池和線程池同樣,都屬於池化資源,做用都是避免重量級資源的頻繁建立和銷燬,對於數據庫鏈接池來講,也就是避免數據庫鏈接頻繁建立和銷燬。服務端會在運行期持有必定數量的數據庫鏈接,當須要執行 SQL 時,並非直接建立一個數據庫鏈接,而是從鏈接池中獲取一個;當 SQL 執行完,也並非將數據庫鏈接真的關掉,而是將其歸還到鏈接池中。數據庫
爲了能讓你更好地理解數據庫鏈接池的工做原理,咱們不使用使用任何框架,而是原生地使用 HiKariCP。執行數據庫操做基本上是一系列規範化的步驟:數組
下面的示例代碼,經過 ds.getConnection()
獲取一個數據庫鏈接時,實際上是向數據庫鏈接池申請一個數據庫鏈接,而不是建立一個新的數據庫鏈接。一樣,經過 conn.close()
釋放一個數據庫鏈接時,也不是直接將鏈接關閉,而是將鏈接歸還給數據庫鏈接池。數據結構
// 數據庫鏈接池配置 HikariConfig config = new HikariConfig(); config.setMinimumIdle(1); config.setMaximumPoolSize(2); config.setConnectionTestQuery("SELECT 1"); config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource"); config.addDataSourceProperty("url", "jdbc:h2:mem:test"); // 建立數據源 DataSource ds = new HikariDataSource(config); Connection conn = null; Statement stmt = null; ResultSet rs = null; try { // 獲取數據庫鏈接 conn = ds.getConnection(); // 建立 Statement stmt = conn.createStatement(); // 執行 SQL rs = stmt.executeQuery("select * from abc"); // 獲取結果 while (rs.next()) { int id = rs.getInt(1); ...... } } catch(Exception e) { e.printStackTrace(); } finally { // 關閉 ResultSet close(rs); // 關閉 Statement close(stmt); // 關閉 Connection close(conn); } // 關閉資源 void close(AutoCloseable rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } }
HiKariCP官文解釋了其性能之因此如此之高的祕密。微觀上 HiKariCP 程序編譯出的字節碼執行效率更高,站在字節碼的角度去優化 Java 代碼。而宏觀上主要是和兩個數據結構有關,一個是 FastList,另外一個是 ConcurrentBag。併發
按照規範步驟,執行完數據庫操做以後,須要依次關閉 ResultSet、Statement、Connection,可是總有粗心的同窗只是關閉了 Connection,而忘了關閉 ResultSet 和 Statement。爲了解決這種問題,最好的辦法是當關閉 Connection 時,可以自動關閉 Statement。爲了達到這個目標,Connection 就須要跟蹤建立的 Statement,最簡單的辦法就是將建立的 Statement 保存在數組 ArrayList 裏,這樣當關閉 Connection 的時候,就能夠依次將數組中的全部 Statement 關閉。框架
HiKariCP 以爲用 ArrayList 仍是太慢。 由於 當經過 stmt.close()
關閉 Statement 的時候,須要調用 ArrayList 的 remove() 方法來將其從 ArrayList 中刪除,這裏是有優化餘地的。高併發
假設一個 Connection 依次建立 6 個 Statement,分別是 S一、S二、S三、S四、S五、S6,按照正常的編碼習慣,關閉 Statement 的順序通常是逆序的,關閉的順序是:S六、S五、S四、S三、S二、S1,而 ArrayList 的 remove(Object o) 方法是順序遍歷查找,逆序刪除而順序查找,這樣的查找效率就太慢了。如何優化呢?很簡單,優化成逆序查找就能夠了。工具
HiKariCP 中的 FastList 相對於 ArrayList 的一個優化點就是將remove(Object element)
方法的查找順序變成了逆序查找
。除此以外,FastList 還有另外一個優化點,是 get(int index)
方法沒有對 index 參數進行越界檢查,HiKariCP 能保證不會越界,因此不用每次都進行越界檢查。性能
若是讓咱們本身來實現一個數據庫鏈接池,最簡單的辦法就是用兩個阻塞隊列來實現,一個用於保存空閒數據庫鏈接的隊列 idle,另外一個用於保存忙碌數據庫鏈接的隊列 busy;獲取鏈接時將空閒的數據庫鏈接從 idle 隊列移動到 busy 隊列,而關閉鏈接時將數據庫鏈接從 busy 移動到 idle。這種方案將併發問題委託給了阻塞隊列,實現簡單,可是性能並非很理想。由於 Java SDK 中的阻塞隊列是用鎖實現的,而高併發場景下鎖的爭用對性能影響很大。
// 忙碌隊列 BlockingQueue<Connection> busy; // 空閒隊列 BlockingQueue<Connection> idle;
HiKariCP 並無使用 Java SDK 中的阻塞隊列,而是本身實現了一個叫作 ConcurrentBag 的併發容器。它的一個核心設計是使用 ThreadLocal 避免部分併發問題, 下面咱們來看看它是如何實現的。
ConcurrentBag 中最關鍵的屬性有 4 個,分別是:用於存儲全部的數據庫鏈接的共享隊列 sharedList、線程本地存儲 threadList、等待數據庫鏈接的線程數 waiters 以及分配數據庫鏈接的工具 handoffQueue。其中,handoffQueue 用的是 Java SDK 提供的 SynchronousQueue,SynchronousQueue 主要用於線程之間傳遞數據。
// 用於存儲全部的數據庫鏈接 CopyOnWriteArrayList<T> sharedList; // 線程本地存儲中的數據庫鏈接 ThreadLocal<List<Object>> threadList; // 等待數據庫鏈接的線程數 AtomicInteger waiters; // 分配數據庫鏈接的工具 SynchronousQueue<T> handoffQueue;
當線程池建立了一個數據庫鏈接時,經過調用 ConcurrentBag 的 add() 方法加入到 ConcurrentBag 中,下面是 add() 方法的具體實現,邏輯很簡單,就是將這個鏈接加入到共享隊列 sharedList 中,若是此時有線程在等待數據庫鏈接,那麼就經過 handoffQueue 將這個鏈接分配給等待的線程。
// 將空閒鏈接添加到隊列 void add(final T bagEntry){ // 加入共享隊列 sharedList.add(bagEntry); // 若是有等待鏈接的線程, // 則經過 handoffQueue 直接分配給等待的線程 while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) { yield(); } }
經過 ConcurrentBag 提供的 borrow() 方法,能夠獲取一個空閒的數據庫鏈接,borrow() 的主要邏輯是:
線程本地存儲中的鏈接是能夠被其餘線程竊取的,因此須要用 CAS 方法防止重複分配。在共享隊列中獲取空閒鏈接,也採用了 CAS 方法防止重複分配。
T borrow(long timeout, final TimeUnit timeUnit){ // 先查看線程本地存儲是否有空閒鏈接 final List<Object> list = threadList.get(); for (int i = list.size() - 1; i >= 0; i--) { final Object entry = list.remove(i); 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; } } // 線程本地存儲中無空閒鏈接,則從共享隊列中獲取 final int waiting = waiters.incrementAndGet(); try { for (T bagEntry : sharedList) { // 若是共享隊列中有空閒鏈接,則返回 if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { return bagEntry; } } // 共享隊列中沒有鏈接,則須要等待 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); // 超時沒有獲取到鏈接,返回 null return null; } finally { waiters.decrementAndGet(); } }
釋放鏈接須要調用 ConcurrentBag 提供的 requite() 方法,該方法的邏輯很簡單,首先將數據庫鏈接狀態更改成 STATE_NOT_IN_USE,以後查看是否存在等待線程,若是有,則分配給等待線程;若是沒有,則將該數據庫鏈接保存到線程本地存儲裏。
// 釋放鏈接 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); } }
HiKariCP 中的 FastList 和 ConcurrentBag 這兩個數據結構使用得很是巧妙,雖然實現起來並不複雜,可是對於性能的提高很是明顯,根本緣由在於這兩個數據結構適用於數據庫鏈接池這個特定的場景。FastList 適用於逆序刪除場景;而 ConcurrentBag 經過 ThreadLocal 作一次預分配,避免直接競爭共享資源,很是適合池化資源的分配。