- HikariCP數據庫連接池實戰
- 朱政科
- 12334字
- 2019-09-02 17:55:04
2.3 數據庫連接池百曉生《兵器譜》
《兵器譜》是古龍筆下的一本虛構的古籍,由小說中的人物“平湖”百曉生所著。古籍中列出了當時武林中人的兵器及武功的排名。在百曉生的《兵器譜》中,天機老人的天機棒、上官金虹的子母龍鳳環、李尋歡的小李飛刀分別排名第一、第二、第三。與此類似,數據庫連接池也是百花齊放、各有高下。我們可以用百曉生的《兵器譜》作為比喻,來收錄、盤點各種各樣的數據庫連接池。
百度百科中介紹了在Java中開源的常見數據庫連接池主要有以下十一種。
?c3p0:是一個開放源代碼的JDBC連接池,它在lib目錄中與Hibernate一起發布,包括了實現JDBC3和JDBC2擴展規范說明的Connection和Statement池的DataSources對象。
?Proxool:是一個Java SQL Driver驅動程序,提供了對選擇的其他類型的驅動程序的連接池封裝。可以非常簡單地移植到現存的代碼中,完全可配置,快速、成熟、健壯。可以透明地為現存的JDBC驅動程序增加連接池功能。
?Jakarta DBCP:DBCP是一個依賴Jakartacommons-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的透明池,可以“包裝”傳統驅動程序或任意非池化數據源。
它早在2012年就誕生了,可惜的是,自從2015年12月9日發布c3p0的0.9.5.2版本以后,它就銷聲匿跡了。在很長一段時間內,我一直認為它是一個已經死去的數據庫連接池,在本書的初稿中我也將它列為“死亡者”名單。但是在修訂本章的時候,我無意中在c3p0的倉庫里發現,在2019年1月27日0.9.5.3版本又默默地更新了,如圖2-5所示。

圖2-5 c3p0版本信息
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所示。

圖2-6 C3P0在線網址
c3p0庫在以下細節上進行了打磨以確保正確性:
1)DataSources都是可引用和可序列化的,因此其適合綁定到各種基于JNDI的命名服務。
2)當引入Connection和Statement時,都會仔細清理Statement和ResultSets,這是為了防止客戶端使用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所示。

圖2-7 c3p0測試代碼控制臺內容輸出
圖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等都是精心打磨的。在HikariCP源碼解析的章節中我們會詳細介紹。
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是目前最新的一個版本。為什么不再維護?作者在GitHub上聲稱,更多的是作者個人原因,一方面從2006年起作者本人再也沒有使用過Proxool,另一方面作者甚至不再使用Java了。作者提出愿意將Proxool交給新的維護者,可惜的是,這款存在了19年的Proxool很久沒有更新并且不維護了,最近一次代碼提交是兩年前Billhorsman本人對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仔細打磨的地方,在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理論的最終一致性事務叫作柔性事務。

圖2-8 CAP取舍
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開源項目。

圖2-9 ShardingSphere事務一覽
XAPool與之前介紹的c3p0、Proxool一樣,都是第一代數據庫連接池的陳品,最近一次正式版發布是14年前2005年3月5日的1.5.0版本,最近的一次beta版發布是13年前2006年12月19日的1.6.beta版本。由于已經不再更新,并且其硬綁定的老伙計JOTM也失去活力,我們也可以同樣認為XAPool夭折了。
“人猿相揖別。只幾個石頭磨過,小兒時節。”
主流的第一代數據庫連接池c3p0(2015年12月9日初步封版)、Proxool(2018年8月23日封版)、XAPool(2006年12月19日封版),都因為各種各樣的原因消逝在歷史長河中,但是它們所提出的理念、對數據庫連接池領域的積極探索及當年殺出重圍時的銳氣,都曾經撐起了一個時代,并為第二代數據庫連接池的發展標注了嶄新的方位。
小竅門
整本書才剛剛開始,就已經介紹了很多的jar包。那么jar包的版本是怎樣的呢?推薦兩個不錯的java類、jar包及其依賴查找網站,希望對大家有幫助。
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所示。
表2-1 版本依賴關系表

在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:
The default database connection pool implementation in Apache Tomcat relies on the libraries from the Apache Commons project. The following libraries are used:Commons
DBCP、Commons Pool。
These libraries are located in a single JAR at $CATALINA_HOME/lib/tomcat-dbcp.jar. However, only the classes needed for connection pooling have been included, and the packages have been renamed to avoid interfering with applications.
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<Connection>。
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官網提供的部分基準測試數據。

圖2-10 單線程基準測試對比圖

圖2-11 多線程基準測試對比圖
單線程模式的基準測試,在1000000(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<Connection>實現)。
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三個組成部分。
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所示。

圖2-12 GitHub上對Druid的描述
為什么說它是最好的?最好體現在哪些方面?又是如何實現的?溫少在一次訪談中是這樣說的:
阿里巴巴是一個重度使用關系數據庫的公司,我們在生產環境中大量使用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配置到DruidDataSource中,就可以攔截JDBC的各種API,從而實現擴展。Druid提供了一系列內置Filter。
在SQL注入方面:Druid的優勢是在JDBC最低層進行攔截并判斷,不會遺漏。Druid實現了Oracle、MySQL、Postgresql、SQL-92的Parser,基于SQL語法分析實現,理解其中的SQL語義,智能、準確、誤報率低。
在遷移方面:從DBCP遷移到Druid連接池最方便,把org.apache.commons.dbcp. BasicDataSource修改為om.alibaba.druid.pool.DruidDataSource即可。Druid網站上提供了Druid/DBCP/c3p0/JBoss/WebLogic的參數對照表,通過這個對照表可以遷移用戶目前的配置。
- 一步一步學Spring Boot 2:微服務項目實戰
- JMeter 性能測試實戰(第2版)
- Learning Python Design Patterns(Second Edition)
- 從學徒到高手:汽車電路識圖、故障檢測與維修技能全圖解
- Mastering Drupal 8 Views
- 劍指MySQL:架構、調優與運維
- 計算機應用基礎實踐教程
- Frank Kane's Taming Big Data with Apache Spark and Python
- Unity&VR游戲美術設計實戰
- ScratchJr趣味編程動手玩:讓孩子用編程講故事
- PHP+MySQL Web應用開發教程
- C語言程序設計教程
- Offer來了:Java面試核心知識點精講(框架篇)
- 用Python動手學統計學
- 精通Oracle 12c 數據庫管理