天天看點

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

點選檢視第一章 點選檢視第三章

第2章

資料庫連接配接池江湖

本章将為讀者介紹資料庫連接配接池的一些基礎概念。資料庫連接配接池涉及的概念很多,這裡介紹的是我認為在企業應用開發中非常基礎和重要的概念。

資料庫連接配接的建立是一種耗時長、性能低、代價高的操作,頻繁地進行資料庫連接配接的建立和關閉會極大影響系統的性能,若多線程并發量很大,這樣耗時的資料庫連接配接就可能讓系統變得卡頓。此外,資料庫同時支援的連接配接總數也是有限的,達到上限後,後續線程發起的資料庫連接配接就會失敗。是以,資料庫連接配接池是一種關鍵的、有限的、昂貴的資源,對于複雜的應用,如果頻繁地建立、關閉連接配接,那麼就會極大地影響系統的性能、伸縮性和健壯性。重用資料庫連接配接最主要的原因是減少應用程式與資料庫之間的建立和銷毀TCP連接配接的開銷,資料庫連接配接池的概念應運而生。

本章提及的概念之間的聯系并不是非常緊密,讀者可以有選擇性地閱讀自己感興趣的部分。

2.1 為什麼使用資料庫連接配接池

一些初學者可能會有這樣的疑問:為什麼要使用資料庫連接配接池?一些中進階的朋友也許會問:資料庫連接配接池本身就是一個很簡單的東西,隻是記憶體裡放了一個存連接配接的池,使用的時候拿,用完了就歸還,有什麼好講的,又有何意義,居然還能寫出一本書來?我曾經也有這樣的疑問,但是在寫這本書的兩年時間内,我認為資料庫連接配接池麻雀雖小五髒俱全,了解它還是很有必要的。

如圖2-1所示,資料庫連接配接池存在于資料庫和使用者之間,是一種管理資料庫連接配接的中間件。使用者通過資料庫連接配接池擷取連接配接,進而使用資料庫的資源,并執行通用的“增删改查”操作。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

資料庫連接配接池在百度百科中的定義是:資料庫連接配接池負責配置設定、管理和釋放資料庫連接配接,它允許應用程式重複使用一個現有的資料庫連接配接,而不是重建立立一個;釋放空閑時間超過最大空閑時間的資料庫連接配接,來避免因為沒有釋放資料庫連接配接而引起的資料庫連接配接遺漏。這項技術能明顯提高對資料庫操作的性能。

那麼為什麼使用資料庫連接配接池會明顯提高資料庫操作的性能呢?下面我們結合TCP的3次握手和4次揮手,對不使用資料庫連接配接池和使用資料庫連接配接池這兩種場景分别進行介紹。

讓我們一起重溫大學歲月,那些年在學習程式設計語言的時候,為了讓學生能夠快速學會資料庫增删改查操作,教材裡基本上都采用不使用資料庫連接配接池的講解和代碼示例。經典的JDBC連接配接資料庫的步驟大緻是這樣的:加載JDBC驅動→建立資料庫連接配接→建立preparedStatement→執行SQL語句→周遊結果集→關閉資源。這樣的教科書式的流程可以通俗了解為:應用程式用DataSource用資料庫驅動來建立一個資料庫連接配接,再通過TCP進行資料庫的讀寫,最後關閉資料庫連接配接。

以通路MySQL為例,執行一個SQL語句的完整TCP流程共經曆TCP 3次握手建立連接配接、MySQL 3次握手認證、SQL語句執行、MySQL關閉、TCP 4次揮手關閉連接配接這5個步驟,如圖2-2所示。

這樣的傳統方式比較适合教科書,但是卻不适合企業實際生産。它存在的主要問題如下所示。

1)建立連接配接和關閉連接配接的過程比較耗時,并發時系統會變得很卡頓。

2)資料庫同時支援的連接配接總數是有限的,如果并發量很大,那麼資料庫連接配接的總數就會被消耗光,增加資料庫的負載,新的資料庫連接配接請求就會失敗。這樣就會極大地浪費資料庫的資源,極易造成資料庫伺服器記憶體溢出、當機。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

3)為了執行一條SQL,卻産生了很多我們并不關心的網絡IO。

4)應用如果頻繁地建立連接配接和關閉連接配接,會導緻JVM臨時對象較多,GC頻繁。

5)頻繁關閉連接配接後,會出現大量TIME_WAIT 的TCP狀态(在2個MSL之後關閉),這點很棘手。這個問題在第1章中已經結合實踐調優經驗詳細說明過。

6)應用的響應時間及QPS較低。

使用資料庫連接配接池之後,最直接的改變就是除了第1次通路時需要建立連接配接以外,之後的通路基本上隻需複用已有的連接配接,而不是重建立立一個。

資料庫連接配接池帶來的優點如下所示。

1)資源重用更佳。由于資料庫連接配接得到複用,減少了大量建立和關閉連接配接帶來的開銷,也大大減少了記憶體碎片和資料庫臨時程序、線程的數量,使得整體系統的運作更加平穩。

2)系統調優更簡便。由于頻繁關閉連接配接會出現TCP大量TIME_WAIT狀态,如第1章的案例描述,TIME_WAIT的調優非常煩瑣。使用了資料庫連接配接池以後,由于資源重用,大大減少了頻繁關閉連接配接的開銷,大大降低了TIME_WAIT的出現頻率。當然,資料庫連接配接池有它自己獨特的配置參數,這些參數如何調優在本書的後續章節中會詳細介紹。

3)系統響應更快。資料庫連接配接池在應用初始化的過程中一般都會提前準備好一些資料庫連接配接,業務請求可以直接使用已經建立的連接配接,而不需要等待建立連接配接的開銷。初始化資料庫連接配接配合資源重用,使得資料庫連接配接池可以大大縮短系統整體響應時間。

4)連接配接管理更靈活。資料庫連接配接池作為一款中間件,除了扮演有界緩沖的角色外,在統一的連接配接管理上同樣可以做很多文章。使用者可以自行配置連接配接的最小數量、最大數量、最大空閑時間、擷取連接配接逾時間、心跳檢測等。另外,使用者也可以結合新的技術趨勢,增加資料庫連接配接池的動态配置、監控、故障演習等一系列實用的功能。

Vlad Mihalcea在他的著作《High-Performance Java Persistence》中介紹過一個實驗,可視化顯示建立和關閉資料庫連接配接的累積開銷,他比較了打開和關閉4種不同的RDBMS與HikariCP資料庫連接配接池的1000個資料庫連接配接,如圖2-3所示。可以看出,使用資料庫連接配接池HikariCP解決方案以後,連接配接擷取時間大大地縮短了。通過減少連接配接擷取間隔,整個事務響應時間也會縮短。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

綜上所述,這就是我們要在實際生産中使用資料庫連接配接池的理由。

2.2 資料庫連接配接池原理

資料庫連接配接池的原理是:在系統初始化的時候,在記憶體中開辟一片空間,将一定數量的資料庫連接配接作為對象存儲在對象池裡,并對外提供資料庫連接配接的擷取和歸還方法。使用者通路資料庫時,并不是建立一個新的連接配接,而是從資料庫連接配接池中取出一個已有的空閑連接配接對象;使用完畢歸還後的連接配接也不會馬上關閉,而是由資料庫連接配接池統一管理回收,為下一次借用做好準備。如果由于高并發請求導緻資料庫連接配接池中的連接配接被借用完畢,其他線程就會等待,直到有連接配接被歸還。整個過程中,連接配接并不會關閉,而是源源不斷地循環使用,有借有還。資料庫連接配接池還可以通過設定其參數來控制連接配接池中的初始連接配接數、連接配接的上下限數,以及每個連接配接的最大使用次數、最大空閑時間等,也可以通過其自身的管理機制來監視資料庫連接配接的數量、使用情況等。

了解完資料庫連接配接池的原理,我們繼續來看資料庫連接配接池的構成。資料庫連接配接池以連接配接池的管理為核心,主要支援連接配接池的建立和釋放這兩大核心功能。“麻雀雖小,五髒俱全”,資料庫連接配接池還可以支援其他非常實用的功能。一款商用的資料庫連接配接池、一款能夠被開發者廣泛使用的資料庫連接配接池、一款能夠在開源社群持續活躍發展的資料庫連接配接池還必須再支援一些實用的功能,如并發(鎖性能優化乃至無鎖)、連接配接數控制(不同的系統對連接配接數有不同的需求)、監控(一些自身管理機制來監視連接配接的數量及使用情況等)、外部配置(各種主流資料庫連接配接池官方文檔最核心的部分)、資源重用(資料庫連接配接池的核心思想)、檢測及容災(面對一些網絡、時間等問題的自愈)、多庫多服務(如不同的資料庫、不同的使用者名和密碼、分庫分表等情況)、事務處理(對資料庫的操作符合ALL-ALL-NOTHING原則)、定時任務(如空閑檢查、最小連接配接數控制)、緩存(如PSCache等避免對SQL重複解析)、異常處理(對JDBC通路的異常統一處理)、元件維護(如連接配接狀态、JDBC封裝的維護)等。綜上,一款商用的成熟的資料庫連接配接池的構成大緻如圖2-4所示。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

2.3 資料庫連接配接池百曉生《兵器譜》

《兵器譜》是古龍筆下的一本虛構的古籍,由小說中的人物“平湖”百曉生所著。古籍中列出了當時武林中人的兵器及武功的排名。在百曉生的《兵器譜》中,天機老人的天機棒、上官金虹的子母龍鳳環、李尋歡的小李飛刀分别排名第一、第二、第三。與此類似,資料庫連接配接池也是百花齊放、各有千秋。我們可以用百曉生的《兵器譜》作為比喻,來收錄、盤點各種各樣的資料庫連接配接池。

百度百科中介紹了在Java中開源的常見資料庫連接配接池主要有以下11種。

  • c3p0:一個開放源代碼的JDBC連接配接池,它在lib目錄中與Hibernate一起釋出,包括了實作JDBC3和JDBC2擴充規範說明的Connection 和Statement 池的DataSources 對象。
  • Proxool:一個Java SQL Driver驅動程式,提供了對選擇的其他類型的驅動程式的連接配接池封裝。可以非常簡單地移植到現存的代碼中,完全可配置,快速、成熟、健壯。可以透明地為現存的JDBC驅動程式增加連接配接池功能。
  • Jakarta DBCP:DBCP是一個依賴Jakarta commons-pool對象池機制的資料庫連接配接池。DBCP可以直接在應用程式中使用。這就是Tomcat DBCP連接配接池,Tomcat預設使用的就是該連接配接池。

    DDConnectionBroker:一個簡單、輕量級的資料庫連接配接池。

  • DBPool:一個高效、易配置的資料庫連接配接池。它除了支援連接配接池應有的功能外,還包括了一個對象池,使使用者能夠開發一個滿足自己需求的資料庫連接配接池。
  • XAPool:一個XA資料庫連接配接池。它實作了javax.sql.XADataSource,并提供了連接配接池工具。
  • Primrose:一個Java開發的資料庫連接配接池。目前支援的容器包括Tomcat4&5、Resin3與JBoss3。它同樣也有一個獨立的版本,可以在應用程式中使用而不必運作在容器中。Primrose通過一個Web接口來控制SQL處理的追蹤、配置,以及動态池管理。在重負荷的情況下可進行連接配接請求隊列處理。
  • SmartPool:一個連接配接池元件,它模仿應用伺服器對象池的特性。SmartPool能夠解決一些資料庫連接配接池的問題,如連接配接洩露(connection leaks)、連接配接阻塞、JDBC對象清理(如Statements、PreparedStatements)等。
  • MiniConnectionPoolManager:一個輕量級JDBC資料庫連接配接池。它隻需要Java 1.5版本(或更高)即可,并且沒有依賴第三方包。
  • BoneCP:一個快速、開源的資料庫連接配接池。幫使用者管理資料連接配接,讓應用程式能更快速地通路資料庫。比c3p0/DBCP連接配接池的速度快25倍。
  • Druid:它不僅是一個資料庫連接配接池,還包含一個ProxyDriver、一系列内置的JDBC元件庫、一個SQL Parser。支援所有JDBC相容的資料庫,包括Oracle、MySQL、Derby、Postgresql、SQL Server、H2等。

MySQL從One-Thread-Per-Connection進化到ThreadPool線程池模型,是以線程池可以作為MySQL進化發展的裡程碑。資料庫連接配接池有過類似的經曆,比較有代表性的是Apache Commons DBCP,在1.X版本中是單線程模型設計,從2.X版本開始采用多線程模型。按照線程模型分類,我們可以初步把過往出現的這些資料庫連接配接池産品分為第一代資料庫連接配接池和第二代資料庫連接配接池。一般來說,c3p0、Proxool、XAPool和DBCP 1.X版本屬于第一代資料庫連接配接池;DBCP2.X版本、Tomcat JDBC Pool、BoneCP、Druid和HikariCP屬于第二代資料庫連接配接池。用版本釋出時間區分也是區分兩代産品的一個比較偷懶的方法,靠近當代的資料庫連接配接池大多會選擇多線程模型。

有些資料庫連接配接池已經湮滅在曆史的演進中,而有些則“老當益壯,甯移白首之心,不墜青雲之志”,有些專攻性能,有些主打全面。

在Java領域中開源的資料庫連接配接池有很多,可以說是百家争鳴、百花齊放,但是目前還沒有一本書專門進行收錄和整理。本節博海拾貝,讓我們一起去看看在資料庫連接配接池技術的演進過程中,都出現了哪些資料庫連接配接池,它們有什麼特色,彼此之間都有什麼聯系。

2.3.1 c3p0

c3p0是Swaldman主要開發并維護的一款開源的資料庫連接配接池,它實作了資料源和JNDI綁定,支援JDBC3規範和JDBC2的标準擴充來增強傳統的JDBC。從版本0.9.5

開始,c3p0完全支援JDBC4規範。它在GitHub是這樣描述的:“c3p0 is a mature, highly concurrent JDBC Connection. pooling library, with support for caching and reuse of PreparedStatements”。中文意思是,c3p0是一個成熟的、高并發的JDBC連接配接池庫,支援緩存和重用PreparedStatements。

c3p0可以說是資料庫連接配接池界的老古董,在很長一段時間内它甚至就是資料庫連接配接池的代名詞。當年盛極一時的Hibernate和Spring都曾将其作為内置的資料庫連接配接池,它的穩定性可見一斑。c3p0的願景是它提供的DataSource可以适合更多的J2EE企業應用程式使用,是以堅持修複問題,不斷疊代版本。c3p0代碼與它的競争對手相比,代碼量巨大且結構複雜,它需要分析120個類(Vibur為34個類,HikariCP為21個類),但正因如此,它基本包含了資料庫連接配接池的所有功能。與無鎖設計的HikariCP和Vibur-DBCP不同的是,c3p0具有超過230個synchronized同步塊和方法,在不同的類中充斥着大量wait()及notifyAll()方法,這些導緻死鎖傾向的代碼造成了在網絡上搜尋“c3p0死鎖”可以查到大量的資料。由于代碼量複雜等原因,c3p0在基準測試中也始終排在最後。c3p0在預設情況下不會在getConnection的時候測試連接配接可用性,這點也是不安全的預設配置。

特别的,c3p0提供了一些有用的服務:

1)一個将傳統的基于DriverManager的JDBC驅動程式調整為較新的javax.sql.DataSource方案的類,以擷取資料庫連接配接。

2)基于DataSources的Connection和PreparedStatements的透明池,可以“包裝”傳統驅動程式或任意非池化資料源。

c3p0早在2012年就誕生了,可惜的是,自從2015年12月9日釋出c3p0的0.9.5.2版

本以後,它就銷聲匿迹了。在很長一段時間内,我一直認為它是一個已經消亡的資料庫連接配接池,在本書的初稿中我也将它列入了“死亡者”名單。但是在修訂本章的時候,我無意中在c3p0的倉庫裡發現,其在2019年1月27日的0.9.5.3版本中又默默地進行了更新,如圖2-5所示。

0.9.5.3版本主要根據GitHub的使用者建議做了兩件事,一件是将mchange-commons-java更新到0.2.15版本并支援Log4j2,另一件是不再擴充XML配置檔案中的實體引用(由于config屬性的覆寫行為會引發安全問題)。

從2015年12月9日到2019年1月27日整整3年多沒有更新,這次雖然是規模不大的改造,但卻是一次非常有意義的版本更新,至少證明作者Swaldman并沒有完全放棄這款資料庫連接配接池。

c3p0在sourceforge中也托管了其項目資訊,如果想了解更多關于c3p0的相關概念、使用方式、配置、進階特性、性能等,使用者可以觀看在GitHub下載下傳的c3p0包中的doc目錄下的index.html,當然也可以登入網址線上觀看,它們的内容是一緻的,如圖2-6所示。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖
帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

c3p0庫在以下細節上進行了打磨以確定正确性:

1)DataSources都是可引用和可序列化的,是以其适合綁定到各種基于JNDI的命名服務。

2)當引入Connection和Statement時,都會仔細清理Statement和ResultSet,這是為了防止用戶端使用Lazy模式。常見的資源管理政策僅會清理Connection而造成資源耗盡。

3)該庫采用JDBC 2和JDBC 3規範定義的方法(即使這些方法與庫作者的首選項沖突)。DataSources以JavaBean樣式編寫,提供所有必需和大多數可選屬性(以及一些非标準屬性)與無參構造函數。實作了所有JDBC定義的内部接口(ConnectionPoolDataSource,PooledConnection、ConnectionEvent-generating Connections等)。使用者可以将c3p0類與相容的第三方實作混合使用(盡管并非所有c3p0功能都可以與ConnectionPoolDataSource的外部實作一起使用)。

c3p0的設計非常簡單易用。要使用c3p0需要下載下傳其jar包,在c3p0的jar包中共有3個包。以0.9.5.2版本為例,如果使用非Oracle資料庫,則隻需導入c3p0-0.9.5.2jar包和mchange-commons-java-0.2.11.jar包即可;如果使用Oracle資料庫的話,還需要導入c3p0-oracle-thin-extras-0.9.5.2.jar包。當然,目前更新了0.9.5.3版本,若使用最新版本,對于非Oracle的資料庫,隻需要引入c3p0-0.9.5.3.jar和mchange-commons-java-0.2.15.jar。

接着可以建立一個如下的DataSource:

import com.mchange.v2.c3p0.*;

...

ComboPooledDataSource cpds = new ComboPooledDataSource();

cpds.setDriverClass( "org.postgresql.Driver" ); //loads the jdbc driver

cpds.setJdbcUrl( "jdbc:postgresql://localhost/testdb" );

cpds.setUser("dbuser");

cpds.setPassword("dbpassword");

若打開PreparedStatement,需設定maxStatements 或maxStatementsPerConnection(預設0)。

cpds.setMaxStatements( 180 );

使用者可以用上述這個根據預設參數配置的連接配接池執行任意你想通過DataSource執行的操作,也可以将DataSource綁定到JNDI名稱服務。當操作執行完畢後,使用者可以采用如下方式清理建立的DataSource:

cpds.close();

就是這麼簡單,剩下的都是細節。使用者可以根據上文提到的mchange文檔和c3p0的API文檔深入研究。從使用者的角度來看,c3p0隻提供标準的JDBC DataSource對象,但是建立這些DataSource時,使用者可以控制與池及命名相關的屬性和其他屬性。

為了更加友善讀者的了解,我也寫了一個基于maven的c3p0示例demo。該demo托管在GitHub,讀者可以按本頁腳注擷取。簡單說一下該案例,該工具類一加載進記憶體就利用c3p0的連接配接池類ComboPooledDataSource的對象來設定各個資料庫驅動和連接配接池的參數,例如資料庫連接配接驅動、資料庫URL、資料庫使用者名和密碼、連接配接池裡的最大和最小連接配接數、連接配接池初始化時的連接配接數等。這些配置隻是ComboPooledDataSource對象中設定方法的冰山一角,我們還可以通過ComboPooledDataSource對象的方法為連接配接池設定更多的功能和參數。

ComboPooledDataSource是操作資料庫連接配接池的關鍵類,擷取資料庫的連接配接可以使用ComboPooledDataSource的getConnection()方法,釋放資料庫的連接配接則是使用連接配接對象的close方法。釋放資源時(調用Connection對象的close方法)不會将連接配接銷毀,而是重新放入c3p0的連接配接池中。

運作示例代碼中的測試代碼,在控制台上可以看到c3p0連接配接池獲得的Connection對象,如圖2-7所示。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

圖2-7中的資訊是c3p0在建立資料庫連接配接池時通過日志記錄的資訊,HikariCP等其他資料庫連接配接池也可以開啟這些記錄資訊,以便于調試和問題排查。我們可以通過這些資訊來檢視資料庫連接配接池建立時的情況。

c3p0官網上列出了其自知的缺點,主要有兩方面。

1)連接配接和語句基于每個身份驗證進行池化。是以,如果一個池支援的DataSource用于擷取[ user = alice,password = secret1]和[ user = bob,password = secret2]的連接配接,則會有兩個不同的池,而DataSource可能在最壞的情況下管理maxPoolSize屬性指定的連接配接數的兩倍。

這一事實是DataSource規範定義的自然結果(允許通過多個使用者身份驗證擷取Connections),并且要求單個池中的所有Connections在功能上相同。這個“問題”不會改變或修複。這裡需要你了解過程中發生了什麼。

2)Statement pooling的開銷太高。對于未對PreparedStatements執行重要預處理的驅動程式,池化開銷超過任何節省的開銷。是以,預設情況下應關閉語句池。如果使用者的驅動程式确實需要進行預處理PreparedStatements,特别是如果通過IPC與RDBMS進行預處理,則使用者可通過打開語句池來看到性能的顯著提升(通過将配置屬性maxStatements或maxStatementsPerConnection 設定為大于零的值來執行此操作)。

這裡,我再補充第3點,即“APPARENT DEADLOCK”的問題。c3p0在從連接配接池中擷取和傳回連接配接的時候,采用了異步的處理方式,使用一個線程池異步将傳回關閉了(沒有真正關閉)的連接配接放入連接配接池中。這是一個非常嚴重的Bug,也是一個著名的c3p0問題,即高并發時會出現嚴重的性能問題。原因是,調用了上文中c3p0擷取的連接配接的close方法是異步的,異步就是将連接配接放入一個事件隊列中等待内部進行處理,而不是立即放入資料庫連接配接池中。c3p0中的AcquireTask(擷取任務)就會大量占用内部線程池,導緻沒有足夠的線程來将資料庫連接配接池外使用完的連接配接放回池内。當然,使用者可以自己寫一個優先級最高的線程來單獨、優先、迅速地歸還連接配接。

c3p0的性能在衆多連接配接池中屬于比較低的,在BoneCP的官網上明确寫到BoneCP比c3p0/DBCP連接配接池快了25倍。c3p0與DBCP的一個主要的差別就是,DBCP沒有自動回收空閑連接配接的功能,而c3p0有自動回收空閑連接配接的功能。本書的主角HikariCP在這些方面就處理得非常精妙,除了它資料結構的定義以外,單獨的HouseKeeper等都是經過精心打磨的。

2.3.2 Proxool

Proxool同樣也可以說是資料庫連接配接池領域的老古董了,與c3p0一樣,它也是托管在sourceforge下的一個開源項目。Proxool是一個資料庫連接配接池架構,也可以說是一個連接配接池的類庫,它同樣提供了JDBC驅動程式的透明連接配接池封裝。

Proxool以JDBC驅動的身份為使用者提供透明的連接配接池服務,是以Proxool移植到現有代碼中特别容易,使用者可以輕松地使用JDBC API、XML或Java屬性檔案進行配置。Proxool在那個年代另辟蹊徑,開創性地提供了連接配接池監控功能(這也是Druid資料庫連接配接池後來主打的方向),便于發現連接配接洩露等性能情況及連接配接事件。它也符合J2SE API,讓使用者對于開發标準建立了信心。

Proxool穩定性好、健壯性高,它曾經和DBCP及c3p0一起,并列為最常見的3種JDBC連接配接池技術。Hibernate官方也曾經宣布,由于Bug太多不再支援DBCP,而推薦使用 Proxool或c3p0。

Proxool建立于2001年,2008年8月23日是其最後一次更新版本,0.9.1是目前最新的一個版本。作者Billhorsman在GitHub上聲稱不再維護更多的是由于個人原因,一方面從2006年起作者本人再也沒有使用過Proxool,另一方面作者甚至不再使用Java了。作者提出願意将Proxool交給新的維護者,可惜的是,這款存在了19年的Proxool很久都沒有更新維護,最近一次代碼送出是兩年前作者對README進行的一次不維護說明。風起雲湧的資料庫連接配接池江湖不斷有新秀湧出,取而代之的有Druid、HikariCP等資料庫連接配接池。我們不得不認為,Proxool已經被淘汰了。

由于曆史悠久,Proxool當年曾做過JDK 1.2至1.5的相容性測試。從0.8.0版本開始,使用了Cglib的代理庫,使用者普遍使用其最後更新的0.9.1版本,使用方式是在其官網下載下傳Proxool源碼,下載下傳後解壓,把proxool.jar和proxool-cglib.jar放入要配置項目的lib目錄下。它的很多設計理念都被HikariCP認可并吸收,HikariCP在繼承過程中進行了獨具匠心的打磨。例如,關于Cglib等位元組碼的代理,這也是HikariCP仔細打磨的地方。

Proxool的源碼并不是很多,但是閱讀起來非常有意思,完全可以認為它是HikariCP資料庫連接配接池的前身手寫版實作。其核心的ProxoolFacade用于管理連接配接池的注冊、移除及監聽事件,可以了解為ProxoolFacade是Proxool架構的外觀,通過其暴露Proxool的各種操作及屬性,會把外來的各種ConnectionPool注冊到ConnectionPoolManaager,還會注冊listener及jmx等。在Proxool内部将連接配接池的注冊任務實作在了HouseKeeperController中,我們需要記住HouseKeeper這個名字,因為後面源碼解析HikariCP部分也使用了同樣的名字做類似的初始化工作。

Proxool的源碼還有一個重要的功能是狀态的管理。狀态主要包括:連接配接池目前連接配接數及連接配接池配置的屬性,比如可用空閑連接配接、最大連接配接數、最小連接配接數、連接配接的最大活躍時長等。當然,還涉及比較複雜的并發處理。這些也都是HikariCP同樣需要面對的問題。

由于篇幅原因,本書不對Proxool源碼做深入解析,感興趣的讀者可以閱讀Proxool注冊到連接配接池、關閉Connection及Connection真正關閉、HouseKeeper連接配接管理等三大子產品的源碼,這些都是HikariCP同樣處理的部分,隻不過HikariCP在這些問題的處理上将細節打磨得更加精彩。

2.3.3 XAPool

XA是X/Open CAE Specification(Distributed Transaction Processing)模型中定義的TM(Transaction Manager)與RM(Resource Manager)之間進行通信的接口。Java中的javax.transaction.xa.XAResource定義了XA接口,它依賴資料庫廠商對jdbc-driver的具體實作。在XA規範中,資料庫充當RM角色,應用需要充當TM的角色,即生成全局的txId,調用XAResource接口,把多個本地事務協調為全局統一的分布式事務。

事務分為本地事務和分布式事務。了解分布式事務,往往需要了解ACID(Atomicity原子性、Consistency一緻性、Isolation隔離性、Durability持久性)、CAP(對分布式應用而言,不可能同時滿足C一緻性、A可用性、P分區容錯性,在保證P的前提下往往需要在C和A之間進行平衡,如圖2-8所示)和BASE理論(Basically Available基本可用、Soft state軟狀态和Eventually consistent最終一緻性)等概念。符合傳統ACID的通常叫作剛性事務,滿足BASE理論的最終一緻性事務叫作柔性事務。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

XAPool是一個XA資料庫連接配接池,它實作了javax.sql.XADataSource,并提供了連接配接池工具。這是一款主打分布式事務的資料庫連接配接池,它支援池對象、JDBC連接配接和XA連接配接。一般來說,如果一些老項目中打算使用JOTM來實作分布式的事務管理,一般都需要配合使用XAPool。JOTM最後的更新日期是2010年,實作JTA事務管理第三方管理工具目前比較活躍的是Atomikos和Narayana等。

Atomikos是一種通過SPI注入不同的第三方元件作為事務管理器實作XA協定。同樣作為DataSource增強的Apache孵化器項目ShardingSphere也使用了Atomikos,ShardingSphere已經釋出了弱XA事務、BED最大努力送達柔性事務,規劃釋出基于Atomikos和Narayana的XA事務、基于Apache Service Comb的Saga事務、TCC(Try-Confirm-Cancel)事務,如圖2-9所示。感興趣的讀者可以關注Sharding-Sphere開源項目。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

XAPool與之前介紹的c3p0、Proxool一樣,都是第一代資料庫連接配接池的陳品,最近一次正式版釋出是2005年3月5日的1.5.0版本,最近的一次beta版釋出是2006年12月19日的1.6.beta版本。由于已經不再更新,并且其硬綁定的“老夥計”JOTM也失去了活力,我們也同樣可以認為XAPool消亡了。

主流的第一代資料庫連接配接池c3p0(2015年12月9日初步封版)、Proxool(2018年8月23日封版)、XAPool(2006年12月19日封版),都因為各種各樣的原因消逝在曆史長河中,但是它們所提出的理念,對資料庫連接配接池領域的積極探索及當年殺出重圍時的銳氣,都曾經撐起了一個時代,并為第二代資料庫連接配接池的發展指出了新的方向。

小竅門 本書至此已經介紹了很多的jar包。那麼jar包的版本是怎樣的?推薦兩個不錯的Java類、jar包及其依賴查找網站,希望對大家有所幫助。

https://mvn.repository.com https://www.findjar.com

2.3.4 DBCP

DBCP是Apache下獨立的資料庫連接配接池元件,由于Apache的緣故,它可能是使用最多的開源資料庫連接配接池,比如Jakarta commons-pool對象池機制,以及Tomcat中使用的連接配接池元件就是DBCP。作為Apache項目,DBCP在Apache的維基百科中是這樣描述的:資料庫連接配接池DBCP元件可以用于需要池化的JDBC資源的應用程式。除了JDBC連接配接外,它還支援彙集Statement和PreparedStatement執行個體。

不同于前面3種第一代資料庫連接配接池由于各種各樣的原因消逝,DBCP依托強有力的Apache不斷疊代、老而彌堅,它是前半隻腳踩在第一代資料庫連接配接池,後半隻腳踩在了第二代資料庫連接配接池的跨時代的産品。由于許多Apache項目都支援與關系資料庫的互動,是以DBCP在Apache的生态圈中的影響十分廣泛。

和其他資料庫連接配接池不同的是,Apache Commons DBCP并不是獨立實作連接配接池功能的,它内部依賴于Commons中的另一個子項目Apache Commons Pool。資料庫連接配接池中最核心的“池”,就是由Pool元件提供的,Apache Commons Pool決定着資料庫連接配接池的整體性能。單獨使用DBCP一般需要commons-dbcp.jar、commons-pool.jar這兩個jar包,通過下載下傳源碼,整理出一份Apache Commons DBCP和Apache Commons Pool的版本依賴關系表,如表2-1所示。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

在Apache Commons DBCP的下載下傳頁面我們可以看到DBCP經曆了1.2.2、1.3、1.4(第一代)、2.0.1、2.1、2.1.1、2.2.0、2.3.0、2.4.0、2.5.0(第二代)這樣的版本變遷。其中DBCP對于Java和JDBC版本的支援大緻是:2.5.0 for JDBC 4.2 on Java 8,2.4.0 for JDBC 4.1 on Java 7,1.4 for JDBC 4 on Java 6,1.3 for JDBC 3 on Java 1.4 or 5。通過觀察DBCP的ChangeNote,可以發現Apache Commons DBCP依賴的Apache Commons Pool決定了大版本的更新:從2002年到2014年這十多年間,Pool停留在1.x版本。DBCP也就跟着停留在1.x版本,這個階段就是DBCP的年代,我們也可以了解為其處于第一代資料庫連接配接池的時間段。當然,這個漫長的年代也催生了Tomcat JDBC Pool的出現,我們在後面會介紹它。

在2014年3月,DBCP終于更新到了2.x版本,基于新的線程模型的資料庫連接配接池讓DBCP重獲新生,穩定性得到提升,性能也有了質的飛躍。Apache Commons Pool?2類庫是對象池技術的一種具體實作,它的出現是為了解決頻繁的建立和銷毀對象帶來的性能損耗問題。其原理就是建立一個對象池,池中預先生成了一些對象,需要對象的時候借用,用完後進行歸還,對象不夠時靈活地自動建立,對象池滿後提供參數控制是阻塞還是非阻塞響應租借用。在SpringBoot 1.5.x版本中,資料庫連接配接池的預設配置是Tomcat Pool → HikariCP → Commons DBCP → Commons DBCP2;然而在2.x版本中,HikariCP被提升為預設的資料庫連接配接池,資料庫連接配接池的預設配置順序是HikariCP → Tomcat pool → Commons DBCP2。這也是Spring Boot在1.5.x版本中支援DBCP和DBCP2,而2.x版本中隻支援DBCP2這段曆史的由來。DBCP2的出現表明DBCP從第一代資料庫連接配接池跨越到了第二代資料庫連接配接池,線程模型是劃分衆多資料庫連接配接池曆史分代的一個參照物。

注意 關于DBCP的版本變更還有一段有趣的關于Tomcat的故事。Tomcat 在 7.0 以前的版本都是使用 commons-dbcp作為連接配接池的實作。由于DBCP飽受诟病,Tomcat作為DBCP的忠實擁護者,在單線程的1.X DBCP年代遭遇了太多的性能瓶頸後,沒來得及等到DBCP2出現,就自行開發了一套資料庫連接配接池Tomcat JDBC Pool。Tomcat 7.x的幫助文檔明确提出tomcat-dbcp.jar包含了Commons DBCP和Commons Pool。

2.3.5 Tomcat JDBC Pool

Tomcat JDBC Pool在多個版本的官方文檔上都定義為取代Apache Commons DBCP的連接配接池。

上面我們介紹了DBCP 1.x到2.x的曆史時提到了Tomcat JDBC Pool在DBCP 2.x沒有出來以前取代了DBCP 1.x的版本,JDBC 連接配接池 org.apache.tomcat.jdbc.pool 是 Apache Commons DBCP 連接配接池的一種替換或備選方案。對于熟悉 Commons DBCP 的人來說,轉而使用 Tomcat 連接配接池是非常簡單的事,當然從其他連接配接池轉換過來也非常容易。

在《Tomcat 8權威指南》一書中曾經寫到為什麼需要一個新的連接配接池,原因如下:

1)Commons DBCP 1.x 是單線程。為了線程安全,在對象配置設定或對象傳回的短期内,Commons 鎖定了全部池。但注意,這并不适用于 Commons DBCP 2.x。

2)Commons DBCP 1.x 可能會變得很慢。當邏輯 CPU 數目增長,或者試圖借出或歸還對象的并發線程增加時,性能就會受到影響。高并發系統受到的影響會更為顯著。注意,這并不适用于 Commons DBCP 2.x。

3)Commons DBCP 擁有 60 多個類,而Tomcat JDBC pool 核心隻有 8 個類。是以為了未來需求變更着想,肯定需要更少的改動。我們真正需要的隻是連接配接池本身,其餘的隻是附屬。

4)Commons DBCP 使用靜态接口,是以對于指定版本的 JRE,隻能采用正确版本的 DBCP,否則就會出現 NoSuchMethodException 異常。

5)當DBCP 可以用其他更簡便的實作來替代時,實在不值得重寫那 60 個類。

6)Tomcat JDBC 連接配接池無需為庫本身添加額外線程,就能異步擷取連接配接。

7)Tomcat JDBC 連接配接池是 Tomcat 的一個子產品,依靠 Tomcat JULI 這個簡化了的日志架構。

8)使用 javax.sql.PooledConnection 接口擷取底層連接配接。

9)防止饑餓。如果池變空,線程将等待一個連接配接。當連接配接傳回時,池就将喚醒正确的等待線程。大多數連接配接池隻會一直維持饑餓狀态。

當然,Tomcat JDBC 連接配接池還具有一些其他連接配接池實作所沒有的特點:

1)支援高并發環境與多核(CPU)系統。

2)接口的動态實作。支援 java.sql 與 java.sql 接口(隻要 JDBC 驅動),甚至在利用低版本的 JDK 來編譯時也支援。

3)驗證間隔時間。我們不必每次使用單個連接配接時都進行驗證,可以在借出或歸還連接配接時進行驗證,隻要不低于我們所設定的間隔時間就行。

4)隻執行一次查詢。當與資料庫建立起連接配接時,隻執行一次可配置查詢。這項功能對會話設定非常有用,因為你可能會想在連接配接建立的整個時段内都保持會話。

5)能夠配置自定義攔截器。通過自定義攔截器來增強功能。可以使用攔截器來采集查詢統計,緩存會話狀态,重新連接配接之前失敗的連接配接,重新查詢,緩存查詢結果等。由于可以使用大量的選項,是以這種自定義攔截器也是沒有限制的,與 java.sql/javax.sql 接口的 JDK 版本沒有任何關系。

6)高性能。後面将舉例展示一些性能差異。

7)極其簡單。它的實作非常簡單,代碼行數與源檔案都非常少,這都有賴于從一開始研發它時,就把簡潔當作重中之重。對比一下 c3p0,它的源檔案超過了200 個(最近一次統計),而 Tomcat JDBC 核心隻有 8 個檔案,連接配接池本身則大約隻有這個數目的一半,是以能夠輕易地跟蹤和修改可能出現的 Bug。

8)異步連接配接擷取。可将連接配接請求隊列化,系統傳回 Future。

9)更好地處理空閑連接配接。不再簡單粗暴地直接關閉空閑連接配接,而是把連接配接保留在池中,通過更為巧妙的算法控制空閑連接配接池的規模。

9)可以控制連接配接應被廢棄的時間。當池滿了即廢棄,或者指定一個池使用容內插補點,發生逾時就進行廢棄處理。

10)通過查詢或語句來重置廢棄連接配接計時器。允許一個使用了很長時間的連接配接不會因為逾時而被廢棄。這一點是通過使用 ResetAbandonedTimer 來實作的。

11)經過指定時間後,關閉連接配接。與傳回池的時間相類似。

12)當連接配接要被釋放時,擷取 JMX 通知并記錄所有日志。這類似于remove-AbandonedTimeout,但卻不需要采取任何行為,隻需要報告資訊即可。通過 suspectTimeout 屬性來實作。

13)可以通過 java.sql.Driver、javax.sql.DataSource 或 javax.sql.XADataSource 擷取連接配接。通過 dataSource 與 dataSourceJNDI 屬性實作這一點。

14)支援 XA 連接配接。

除了以上特點外,Tomcat JDBC還存在一些問題,比如預設配置也存在類似c3p0的問題,就是在getConnection的時候并不會預設測試連接配接可用性。

此外,Tomcat JDBC也存在一些不完全遵守JDBC規範的問題。預設不會重置連接配接狀态(如自動送出、事務隔離級别等),使用者必須手動配置名為ConnectionState的JDBCInterceptor。在自動送出中,如果連接配接池配置了autocommit=false,就需要在自己的事務中執行連接配接有效性測試isValid(),否則使用者擷取的連接配接有可能就在一個事務進行中;對于建立連接配接時可以在連接配接上運作的初始化initSQL也是如此,Tomcat不會在自己的事務中封裝連接配接測試或initSQL。連接配接池應該在Connection傳回到池時或從池中取出前,調用clearWarnings()方法清除SQL警告,然而Tomcat JDBC也沒有這麼做。JDBC規範還規定,連接配接關閉時,所有沒有關閉的、已經打開的Statements都應該自動關閉,但是預設情況下Tomcat JDBC并不會跟蹤Statements,除非手動配置一個StatementFinalizer攔截。但是不幸的是,StatementFinalizer使用一組WeakReference對象跟蹤Statements,當JVM受到gc壓力時,在Tomcat有機會關閉這些語句之前,可能會對廢棄的Statements進行垃圾收集,這可能導緻資源的洩露,但是隻有在gc壓力下才會發生,是以可能很難追蹤。

2.3.6 BoneCP

BoneCP是一個快速、免費、開源的Java資料庫連接配接池(即JDBC Pool)。如果你熟悉c3p0或DBCP,那麼也就知道它是用來幹什麼的。簡單地說,這個代碼庫将為你管理資料庫連接配接,讓你的應用具有更快的資料庫通路能力。在c3p0和DBCP已經存在的時代,BoneCP的出現就是為了追求極緻,它幾乎比下一個最快的連接配接池選項快25倍,而且BoneCP從不自旋鎖定,是以它就不會減慢應用程式速度。BoneCP也提供了完善的基準測試,圖2-10和圖2-11為BoneCP官網提供的部分基準測試資料。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖
帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

單線程模式的基準測試,在1 000 000(100萬)次的連接配接擷取/釋放連接配接請求(擷取和釋放沒有延遲)、資料庫連接配接池大小設定為20~50、助手線程為1、分區數為1、擷取連接配接增量為5的統一背景下,官方測量結果顯示BoneCP的性能遠高于DBCP和c3p0,如圖2-10所示。

多線程模式的基準測試,在500個線程且每個線程嘗試進行100次連接配接的擷取/釋放(擷取和釋放沒有延遲)、資料庫連接配接池大小設定為5、助手線程為5、擷取連接配接增量為5的統一背景下,官方測量結果同樣顯示BoneCP的性能遠遠高于DBCP和c3p0,如圖2-11所示。

BoneCP可以說是極緻資料庫連接配接池的領軍開源項目。它和HikariCP也是非常有淵源的,除了HikariCP捐贈給BoneCP幾美金的故事以外,BoneCP在浪潮之巅功成身退,将一身衣缽傳給了HikariCP。在BoneCP的GitHub上,我們可以看到它上一次送出的時間是2015年6月25日,BoneCP的README上寫着短短的一句“墓志銘”:“BoneCP是一種Java JDBC連接配接池實作,通過最小化鎖争用來為應用程式提供更高的吞吐量,進而實作高性能。它擊敗了較舊的連接配接池,如c3p0和DBCP,但現在被視為棄用。建議使用者使用HikariCP。”

BoneCP的特點如下:

1)具有高可擴充性的快速連接配接池。

2)在connection狀态改變時,可配置回調機制(鈎式攔截器)。

3)通過分區(Partitioning)來提升性能。

4)允許使用者直接通路connection或statement。

5)自動擴充pool容量。

6)支援statement caching。

7)支援異步地擷取connection(通過傳回一個Future實作)。

8)以異步的方式施放輔助線程(helper threads),來關閉connection和statement,以獲得高性能。

9)在每個新擷取的connection上,通過簡單的機制,執行自定義的statement(即通過簡單的SQL語句來測試connection是否有效,對應的配置屬性為initSQL)。

10)支援運作時切換資料庫,而不需要停止(shut down)應用。

11)能夠自動回放(replay)任何失敗的事務(如資料庫或網絡出現故障)。

12)支援JMX。

13)可以延遲初始化(lazy initialization)。

14)支援使用XML或property檔案的配置方式。

15)支援idle connection timeouts和max connection age。

16)自動檢驗connection(是否活躍等)。

17)允許直接從資料庫擷取連接配接,而不通過Driver。

18)支援Datasouce和Hibernate。

19)支援通過debugging hooks來定位擷取後未關閉的connection。

20)支援通過debugging來顯示被關閉了兩次的connection的堆棧軌迹(stack locations)。

21)支援自定義pool name。

22)代碼整潔有序。

23)免費,開源,純Java編寫,具有完整的文檔。

當然,BoneCP也存在一些問題。最大的一個問題是無法在getConnection()的時候配置資料庫連接配接池來測試連接配接。然而其他每個資料庫連接配接池大都可以這樣配置。它這樣做是為了提升速度,隻不過犧牲了可靠性。在預設配置方面,BoneCP也不會在Connection傳回到池時或從池中取出之前通過Connection.clearWarnings()方法清除SQL警告;預設情況下也不會關閉廢棄的、已經打開的statements;也不會在自己的事務中封裝連接配接測試或initSQL。

2.3.7 Druid

Druid是一個開源項目,其作者是阿裡溫少。溫少經常主要的工作是設計和實作阿裡巴巴應用監控系統Dragoon。Druid和Fastjson都是監控系統實作的副産品。

Druid是阿裡巴巴唯一使用的資料庫連接配接池,阿裡雲DRDS和阿裡TDDL都采用了Druid,可支援“雙十一”等最嚴苛的使用場景。Druid 有一句口号是“為監控而生的資料庫連接配接池”。經過多年開源積累,已經相對成熟的 Druid 收獲了不小的知名度與口碑,并陸續成為很多技術團隊解決方案中的關鍵環節。Druid 持續增強監控功能,監控功能與阿裡雲相關監控産品對接。其中的 Parser 子產品會剝離出來作為一個項目大力發展。

Druid是一個JDBC元件,由基于Filter-Chain模式的插件體系、DruidDataSource 高效可管理的資料庫連接配接池、SQLParser 3個部分組成。

Druid的主要功能如下所示。

  • 替換DBCP和c3p0。Druid提供了一個高效、功能強大、擴充性好的資料庫連接配接池。
  • 可以監控資料庫通路性能。Druid内置了一個功能強大的StatFilter插件,能夠詳細統計SQL的執行性能,這有助于對線上資料庫通路性能進行分析。
  • 資料庫加密。直接把資料庫密碼寫在配置檔案中是不好的行為,容易導緻安全問題。DruidDruiver和DruidDataSource都支援PasswordCallback。
  • SQL執行日志。Druid提供了不同的LogFilter,能夠支援Common-Logging、Log4j和JdkLog,使用者可以按需選擇相應的LogFilter,監控自己的應用資料庫通路情況。
  • 擴充JDBC。如果使用者對JDBC層有程式設計的需求,可以通過Druid提供的Filter機制,很友善地編寫JDBC層的擴充插件。

Druid的項目背景是這樣的:2010年開始,溫少負責設計一個叫作Dragoon的監控系統,需要一些監控元件,監控應用程式的運作情況,包括Web URI、Spring、JDBC等。為了監控SQL執行情況,他做了一個Filter-Chain模式的ProxyDriver,預設提供StatFilter。當時他還做了一個SQL Parser。溫少的老闆說,不如我們來一個更大的計劃,把連接配接池、SQL Parser、Proxy Driver合起來做成一個項目,命名為Druid。于是Druid就誕生了。

Druid支援所有JDBC相容的資料庫,包括Oracle、MySQL、Derby、Postgresql、SQL Server、H2等。

Druid針對Oracle和MySQL做了特别優化,比如Oracle的PS Cache記憶體占用優化,MySQL的ping檢測優化等。

Druid在DruidDataSourc和ProxyDriver上提供了Filter-Chain模式的擴充API,類似Serlvet的Filter,配置Filter攔截JDBC的方法調用。

在GitHub上我們可以看到有文檔描述Druid是最好的資料庫連接配接池,如圖2-12所示。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

為什麼說它是最好的?最好展現在哪些方面?又是如何實作的?溫少在一次訪談中是這樣說的:“阿裡巴巴是一個重度使用關系資料庫的公司,我們在生産環境中大量使用Druid,通過長期在極高負載的生産環境中實際使用、修改和完善,Druid逐漸發展成最好的資料庫連接配接池。Druid在監控、可擴充性、穩定性和性能方面都有明顯的優勢。”

1)強大的監控特性,通過Druid提供的監控功能,可以清楚地知道連接配接池和SQL的工作情況。

  • 監控SQL的執行時間、ResultSet持有時間、傳回行數、更新行數、錯誤次數、錯誤堆棧資訊。
  • SQL執行的耗時區間分布。什麼是耗時區間分布?比如,某個SQL執行了1000次,其中在0~1毫秒區間50次,在1~10毫秒800次,在10~100毫秒100次,在100~1000毫秒30次,在1~10秒15次,在10秒以上5次。通過耗時區間分布,能夠非常清楚地知道SQL的執行耗時情況。
  • 監控連接配接池的實體連接配接建立和銷毀次數、邏輯連接配接的申請和關閉次數、非空等待次數、PSCache命中率等。

    2)友善擴充。

Druid提供了Filter-Chain模式的擴充API,可以自己編寫Filter攔截JDBC中的任何方法,可以在上面做任何事情,比如性能監控、SQL審計、使用者名加密、日志等。

Druid内置了用于監控的StatFilter、日志輸出的Log系列Filter、防禦SQL注入攻擊的WallFilter。

阿裡巴巴内部實作了用于資料庫加密的CirceFilter,以及與Web、Spring關聯監控的DragoonStatFilter。

3)Druid集合了開源和商業資料庫連接配接池的優秀特性,并結合阿裡巴巴大規模苛刻生産環境的使用經驗進行了優化。

  • ExceptionSorter。當一個連接配接産生不可恢複的異常時,例如Oracle error_code_28 session has been killed,必須立刻将其從連接配接池中逐出,否則會産生大量錯誤。目前隻有Druid和JBoss DataSource實作了ExceptionSorter。
  • PSCache記憶體占用優化對于支援遊标的資料庫(如Oracle、SQL Server、DB2等,不包括MySQL)可以大幅度提升SQL執行性能。一個PreparedStatement對應伺服器的一個遊标,如果PreparedStatement被緩存起來重複執行,PreparedStatement沒有被關閉,伺服器端的遊标就不會被關閉,那麼性能提高會非常顯著。在類似“SELECT * FROM T WHERE ID = ?”這樣的場景中,性能可能是一個數量級的提升。但在Oracle JDBC Driver中,其他的資料庫連接配接池(DBCP、JBossDataSource)會占用過多記憶體,極端情況下可能大于1G。Druid調用OracleDriver提供管理PSCache内部API。
  • LRU是一個性能關鍵名額,特别是Oracle,其中每個Connection對應資料庫端的一個程序,如果資料庫連接配接池遵從LRU,則有助于資料庫伺服器優化,這是重要的名額。Druid、DBCP、Proxool、JBoss遵守LRU,而BoneCP、c3p0則不遵守。BoneCP在mock環境下的性能可能還不錯,但在真實環境中就不好了。

性能方面:性能不是Druid的設計目标,但是測試資料表明,Druid性能比DBCP、c3p0、Proxool、JBoss都要出色。

擴充性方面:Druid提供Filter-Chain模式的插件架構,通過将Filter配置編寫到Druid-

DataSource中,就可以攔截JDBC的各種API,進而實作擴充。Druid提供了一系列内置Filter。

SQL注入方面:Druid的優勢是在JDBC最底層進行攔截并判斷,不會遺漏。Druid實作了Oracle、MySQL、Postgresql以及SQL-92的Parser,基于SQL文法分析實作,了解其中的SQL語義,智能、準确、誤報率低。

遷移方面:從DBCP遷移到Druid連接配接池最友善,把org.apache.commons.dbcp.Basic-

DataSource修改為om.alibaba.druid.pool.DruidDataSource即可。Druid網站上提供了Druid/DBCP/c3p0/JBoss/WebLogic的參數對照表,通過這個對照表可以遷移使用者目前的配置。

2.4 主流資料庫連接配接池對比

英國前首相溫斯頓·丘吉爾曾經說過:“批評也許并不會令人滿意,但卻是必要的。它和人體疼痛的功能類似,它可以喚起人們對于事物不健康狀态的關注。”近幾年,手機評測比較熱門,眼花缭亂的資料庫連接配接池當然也需要一個評測。那麼該如何評測呢?評價原則是:性能固然是好的,但是卻不能以放棄可靠性為代價。

2.4.1 性能對比

在HikariCP的官網上,通過JMH Benchmarks對主流資料庫連接配接池分别進行了Connection和Statement級的性能對比,如圖2-13所示。可以看到,常見的資料庫連接配接池有c3p0、DBCP2、Tomcat、Vibur、Hikari等,其中HikariCP的性能獨占鳌頭。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

2.4.2 代碼複雜度

雖然代碼行數并不直接表明複雜性,但冗長和可了解性之間存在着無可争辯的相關性。一個被業内廣泛接受的觀點是:每千行代碼的Bug數量在不同的項目和語言中表現得相當一緻。除了測試套件外,目視檢查代碼中的邏輯錯誤和競争條件是最可靠的方法之一。HikariCP從未經曆過記錄的死鎖(deadlock)和活鎖(livelock)的情況。

我們按照截至2014年3月15日已釋出的主流資料庫連接配接池版本,不包括測試和comment,從檔案數和代碼量上,我們可以得到對比表格,如表2-2所示。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

上述資料來源于Brett Wooldridge 2014年寫的文章。寫到這裡,不妨和讀者分享一個小故事。筆者曾經在和朋友的閑聊過程中分享這組資料,筆者的朋友王振飛先生在2018年4月26日的時候也使用當時最新的TAG對Druid和HikariCP的資料做了一次比較。得到的結論如下:

1)隻統計Java檔案和xml檔案,Druid總行數430289,HikariCP總行數18372。

2)隻統計Java代碼,Druid總行數428749,HikariCP總行數17556。

3)再過濾一下test目錄,Druid總行數215232,HikariCP總行數7960。

4)不考慮性能,光一個DruidDataSource就編寫了3000行。

一個在某購物網站使用者增長組的好友也曾經告訴我,當他用HikariCP替換Durid之後,RT出現斷崖式下滑(下降至1.2~1.5毫秒),并且持續穩定、毛刺少。他還給我分享了當時的性能測試報告。目前,據我所知,“杭州有贊”“51信用卡”“海康威視”3家公司都采用HikariCP作為資料庫連接配接池,很多中小型網際網路企業也選擇HikariCP作為資料庫連接配接池。

2.4.3 功能對比

在Druid的官方文檔上,也給出了一份幾種資料庫連接配接池功能的對比表,如表2-3所示。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖
帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

在這份文檔中,Druid強調了如下幾個參數。

1)LRU。LRU是一個性能關鍵名額,特别是Oracle,其中每個Connection對應資料庫端的一個程序,如果資料庫連接配接池遵從LRU,有助于資料庫伺服器優化,這是重要的名額。在測試中,Druid、DBCP、Proxool遵守LRU,BoneCP、c3p0則不遵守。BoneCP在mock環境下性能可能不錯,但在真實環境中則不見得優秀。

2)PSCache。PSCache是資料庫連接配接池的關鍵名額。在Oracle中,類似SELECT NAME FROM USER WHERE ID = ?這樣的SQL,啟用PSCache和不啟用PSCache的性能可能會相差一個數量級。Proxool是不支援PSCache的資料庫連接配接池,如果你使用Oracle、SQL Server、DB2、Sybase這樣支援遊标的資料庫,那就完全不用考慮Proxool。

3)PSCache-Oracle-Optimized。在Oracle 10系列的Driver中,如果開啟PSCache,則會占用大量記憶體,必須做特别的處理,比如啟用内部的EnterImplicitCache等方法優化才能減少記憶體的占用。這個功能隻有DruidDataSource有。如果你使用的是Oracle JDBC,則應該毫不猶豫地采用DruidDataSource。

4)ExceptionSorter。ExceptionSorter是一個很重要的容錯特性,如果一個連接配接産生了一個不可恢複的錯誤,必須立刻将其從連接配接池中去掉,否則會連續産生大量錯誤。這個特性,目前隻有JBossDataSource和Druid實作。Druid的實作參考自JBossDataSource,經過長期生産回報進行了補充。

結合HikariCP和Druid的官方文檔,并收集整理了大量資料庫連接配接資料,筆者從線程同步、PSCache、LRU、ExceptionSorted、監控、擴充性、SQL攔截及解析、代碼複雜度、特點、連接配接池管理、JNDI、Tomcat資料源、更新維護等角度對主流資料庫連接配接池做了一份更全面的選型對比,以友善讀者在實際學習工作過程中作為工具使用,如表2-4所示。

2.4.4 資料庫中斷

在HikariCP的官方文檔上,記載了關于對比主流資料庫連接配接池資料庫中斷的測試情況,這是針對各種資料庫連接配接池都提供的逾時功能的測試對比。這個測試實驗模拟了這樣一個場景,将資料庫連接配接池執行getConnection()在5秒的調用後逾時,應用程式應在指定時間内獲得連接配接異常。該實驗設定了一個測試,可以針對遠端MySQL伺服器配置4個池(HikariCP、c3p0、DBCP2、Vibur)。每隔2秒getConnection()調用一次,執行查詢,然後關閉連接配接(傳回池)。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

每個連接配接池都如表2-5所示進行了配置,來驗證check-out時的連接配接。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

每種資料庫連接配接池逾時配置如表2-6所示。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

然後開始運作測試内容,所有的資料庫連接配接池都沒有問題,但是MySQL伺服器網絡電纜突然故意斷開,則出現了如下現象。

  • HikariCP:等待5秒,若連接配接未恢複,抛出SQLExceptions異常,後續getConnection()同樣處理。
  • c3p0:完全無反應且無提示,也不會在CheckoutTimeout配置的時長逾時後給調用者任何通知。等待2分鐘後才醒來,傳回一個error。
  • Tomcat:傳回一個connection,若調用者如果利用這個無效的connection執行SQL語句,結果可想而知。大約55秒之後醒來,此時getConnection()傳回一個error,但未像參數配置的那樣等待5秒鐘,而是立即傳回error。
  • BoneCP:與Tomcat類似,也是約55秒後醒來,有正常反應,且最終等5秒鐘後傳回error。

可見,HikariCP的處理方式是最合理的。根據這個測試結果,對于各個CP處理資料庫中斷的情況,評分如表2-7所示。

帶你讀《HikariCP資料庫連接配接池實戰》之二:資料庫連接配接池江湖資料庫連接配接池江湖

注意 這是使用JDBC 4.1驅動程式獲得的結果,結果可能因新舊驅動程式的差異而有所不同。

除了資料庫中斷以外,HikariCP在其他方面還有很多值得稱贊的地方:

  • 連接配接的測試在getConnection時同步進行,并有獨特的優化。
  • 在自己的事務中封裝内部池的查詢,含testQuery一級initSQLQuery。
  • 當連接配接Connections歸還給連接配接池的時候,執行rollback操作。
  • 在Connection.close的時候,跟蹤并關閉廢棄語句Statements。
  • 在将Connection連接配接歸還到用戶端之前清除SQL警告。
  • 預設參數支援自動送出、事務隔離級别、catalog和隻讀狀态。
  • 對于一些諸如“SQLException對象是否存在斷開連接配接錯誤”等陷阱進行檢查。

2.5 本章小結

作為一名沒有什麼資料庫連接配接池基礎的初學者,如何學習這門技術?如何了解它的基礎和産生背景?這是很多初學者會面臨的問題。

本章結合筆者自身的體會來闡述和介紹資料庫連接配接池的産生原因、概念以及原理,進一步像百曉生點《兵器譜》一樣盤點了業界主流的資料庫連接配接池,像曆史書一樣介紹了從第一代資料庫連接配接池到第二代資料庫連接配接池過渡的這20年中的風風雨雨,并對主流的資料庫連接配接池做了較為全面的對比總結。讀完本章,相信讀者可以了解到第一代資料庫連接配接池c3p0、Proxool、Xapool分别因為什麼原因像恐龍那般滅絕,也能了解到第二代資料庫連接配接池DBCP、Tomcat JDBC Pool、BoneCP、Druid的相關概念及發展曆程(DBCP經曆了第一代和第二代,而Tomcat JDBC Pool就是在DBCP第一代末期産生的,BoneCP在極緻性能上以25倍的高優勢打敗了c3p0和DBCP後,便急流勇退,将一身功力傳給了HikariCP)。

閱讀本章後,希望讀者對于資料庫連接配接池的功能、演進、架構等逐漸産生清晰的認知,并思考一款優秀的開源産品是如何不斷發展的。接下來讓我們一起進入本書的核心環節——HikariCP,看看這個後起之秀Spring Boot 2.x預設的資料庫連接配接池到底又有哪些與傳統資料庫連接配接池不一樣的特色,看看它又有哪些獨具匠心的設計。