數(shù)據(jù)庫連接池在J2EE領域是一個不可缺失的組件,盡管DRUID越來越流行,但是DBCP作為一個老牌的數(shù)據(jù)庫連接池一直在企業(yè)系統(tǒng)中默默的奉獻著自己。比如我們的大部分系統(tǒng)還都是使用DBCP,那么我們很有必要對DBCP有一個熟悉的認識。鑒于這樣的需求,我根據(jù)相關資料(文后都有列出)結合自己的認知重新做了一個梳理,一來自己可以在以后的工作中回過頭來溫習,二來也希望能夠幫助其他同學對DBCP以及涉及到的相關概念和知識比如超時機制,連接池原理等有一個復習。我們都知道操作一個數(shù)據(jù)庫的流程,創(chuàng)建數(shù)據(jù)源,獲得鏈接,構造statement,執(zhí)行請求,接下來我們逐步梳理總結。
一、相關概念復習
在談DBCP之前我們先來復習一下幾個相關的概念:
1.1、JNDI
Java Naming and Directory Interface (JNDI)JNDI API被用于執(zhí)行名字和目錄服務。它提供了一致的模型來存取和操作企業(yè)級的資源。JNDI的api位于javax.naming包中,它的作用是,它可以把對象放到一個容器中(JNDI容器),JAVA對象在這個容器中都有一個名稱,程序則可以通過這個名稱來獲取對象,例如下面這樣:
// Construct BasicDataSource
BasicDataSource bds = new BasicDataSource();
bds.setDriverClassName("org.apache.commons.dbcp2.TesterDriver");
bds.setUrl("jdbc:apache:commons:testdriver");
bds.setUsername("username");
bds.setPassword("password");
ic.rebind("jdbc/basic", bds);
// Use
InitialContext ic2 = new InitialContext();
DataSource ds = (DataSource) ic2.lookup("jdbc/basic");
assertNotNull(ds);
Connection conn = ds.getConnection();
assertNotNull(conn);
conn.close();
1.2、JDBC
JAVA數(shù)據(jù)庫連接(Java Database Connectivity,簡稱JDBC),是java語言中用來規(guī)范話我們的客戶端應用程序,比如我們的web應用程序等如何訪問關系型數(shù)據(jù)庫的應用程序接口,提供了諸如查詢和更新數(shù)據(jù)庫中數(shù)據(jù)的方法。
JDBC驅動程序一共有四種類型:
- 類型1-JDBC-ODBC橋
- 類型2-本地API驅動
- 類型3-網(wǎng)絡協(xié)議驅動
- 類型4-本地協(xié)議驅動
我們常用的是類型4-本地協(xié)議驅動,這種類型的驅動使用socket鏈接,直接在客戶端和數(shù)據(jù)庫之間進行通信。
優(yōu)點是1-訪問速度快 2-最直接,最純粹的JAVA實現(xiàn)
缺點是1-需要每個數(shù)據(jù)庫廠商提供自己的JDBC驅動。2-需要針對不同的數(shù)據(jù)庫使用不同的驅動程序。
JDBC的API在jdk的java.sql包中,擴展的內(nèi)容在javax.sql包中。主要包括(斜體代表接口,需驅動程序提供者來具體實現(xiàn)): - DriverManager:負責加載各種不同驅動程序(Driver),并根據(jù)不同的請求,向調(diào)用者返回相應的數(shù)據(jù)庫連接(Connection)。
- Driver:驅動程序,會將自身加載到DriverManager中去,并處理相應的請求并返回相應的數(shù)據(jù)庫連接(Connection)。
- Connection:數(shù)據(jù)庫連接,負責進行與數(shù)據(jù)庫間的通訊,SQL執(zhí)行以及事務處理都是在某個特定Connection環(huán)境中進行的。可以產(chǎn)生用以執(zhí)行SQL的Statement。
- Statement:用以執(zhí)行SQL查詢和更新(針對靜態(tài)SQL語句和單次執(zhí)行)。
- PreparedStatement:用以執(zhí)行包含動態(tài)參數(shù)的SQL查詢和更新(在服務器端編譯,允許重復執(zhí)行以提高效率)。
- CallableStatement:用以調(diào)用數(shù)據(jù)庫中的存儲過程。
- SQLException:代表在數(shù)據(jù)庫連接的創(chuàng)建和關閉和SQL語句的執(zhí)行過程中發(fā)生了例外情況(即錯誤)。
1.3、DBCP數(shù)據(jù)庫連接池和JDBC之間的關系
數(shù)據(jù)庫連接池負責創(chuàng)建(通過JDBC API)、管理、銷毀數(shù)據(jù)庫的連接。應用程序可以從數(shù)據(jù)庫連接池中重復使用一個現(xiàn)有的連接,而不是重新創(chuàng)建一個。連接池,common-pool中的GenericObjectPool它負責緩存和管理連接;連接,這是是指PoolableConnection;連接池和連接一對多的關系。池化技術是通過commons-pool來實現(xiàn)的,每個連接是一個對象,換言之,是對象池的使用與管理。DBCP連接池是基于commons-pool這種對象池來實現(xiàn)的。
二、commons-pool的理解
Apache commons-pool是一種對象池技術,我們使用的很多涉及池的場景一般都是基于該組件來實現(xiàn)的,DBCP數(shù)據(jù)庫連接池也是基于commons-pool來實現(xiàn)的,因此我們先來了解下這種對象池技術。下面這個圖是對象池的對象生命周期流程圖。
三、DBCP核心類圖及序列圖
3.1、BasicDataSource.java
3.2、ConnectionFactory.java
3.3、PoolingDataSource.java
3.4、PoolingConnection.java
3.5、Delegating.java
3.6、AbandonedObjectPool.java
3.7、創(chuàng)建數(shù)據(jù)源createDataSource
3.8、創(chuàng)建連接getConnection
3.9、創(chuàng)建statement prepareStatement
四、DBCP配置及使用
DBCP是Apache下的一個開源數(shù)據(jù)庫連接池,我們在使用的時候需要兩個JAR文件,分別是commons-dbcp.jar(連接池的實現(xiàn))和commons-pool.jar(連接池實現(xiàn)的依賴庫),不過我們在使用的時候只需要引入下面mvaen坐標,commons-pool是在commons-dbcp里面隱含引用了。注意一點,就是從1.x升級到2.x的時候,由于dbcp的包路徑已經(jīng)變了,需要升級者修改局部代碼。
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>2.2</version>
</dependency>
DBCP可以在應用程序中獨立的使用,也可以與web應用服務器整合使用。
4.1、tomcat中使用
比如tomcat的連接池就是采用該連接池來實現(xiàn)的。如下:
<Context>
<Resource name="jdbc/datasource" auth="Container" type="javax.sql.DataSource"
username="root" password="root" driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://192.168.0.1:3306/test" maxActive="8" maxIdle="4" />
</Context>
4.2、應用程序中獨立使用
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!--如果沒有配置 destroy-method="close" ,但是重啟次數(shù)如果太頻繁的話,將造成重啟tomcat后舊的數(shù)據(jù)庫連接池的連接不釋放,連接堆滿了,后續(xù)啟動無法建立連接-->
<!--數(shù)據(jù)庫連接相關配置,用戶名、密碼、連接地址、驅動-->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
<!--數(shù)據(jù)庫連接屬性 connectTimeout:建立連接的超時時間,socketTimeout:客戶端和服務進行數(shù)據(jù)交互的時間,是指兩者之間如果兩個數(shù)據(jù)包之間的時間大于該時間則認為超時-->
<property name="connectionProperties"
value="connectTimeout=2000;socketTimeout=15000"/>
<!--以下屬性也有的建議,值配置一樣-->
<!--initialSize: 初始化連接-->
<property name="initialSize" value="30"/>
<!--maxActive: 最大連接數(shù)量-->
<property name="maxActive" value="150"/>
<!--minIdle: 最小空閑連接-->
<property name="minIdle" value="5"/>
<!--maxIdle: 最大空閑連接-->
<property name="maxIdle" value="20"/>
<!--等待獲取連接池連接的時間(在1.x版本是maxWait),不要太大-->
<property name="maxWaitMillis" value="500"/>
<!--連接池中的連接空閑多久,從池中刪除,單位MS。小于0,則表示不啟動-->
<property name="minEvictableIdleTimeMillis" value="" />
<!--檢查失效連接的定時器執(zhí)行間隔,單位MS,小于0,則表示不啟動。注意:mysql數(shù)據(jù)庫如果8小時內(nèi)沒有連接請求,則連接會自動斷開,因此這個參數(shù)如果并發(fā)量不大的情況下還是要配上-->
<property name="timeBetweenEvictionRunsMillis" value="" />
</bean>
更多的配置請參照https://commons.apache.org/proper/commons-dbcp/configuration.html
五、連接超時機制
對網(wǎng)絡資源訪問的時候,超時設置是必須的。沒有超時的保護,一旦依賴資源發(fā)生故障或者網(wǎng)絡故障,就會引起線程堆積,甚至發(fā)生雪崩。
5.1、超時層級
從這張圖中我們也能夠看出DBCP并不參與數(shù)據(jù)庫超時的處理,它只負責管理連接。根據(jù)上圖我們可以看到超時有一個依賴層級,上層超時依賴下層超時。依次為:事務超時->Statement超時->JDBC Driver socket超時->操作系統(tǒng)超時。
5.1.1、事務超時:
事務是應用層級的概念,我們知道事務是有一組SQL執(zhí)行單元組成的。那么事務超時的時間閾值,實際是Statement超時N個需要執(zhí)行的Statement數(shù)量。比如一個事務里面有3條Statement,每條Statement的執(zhí)行時間是50MS,其它業(yè)務上的邏輯執(zhí)行時間+框架執(zhí)行時間未100ms,那么最終事務的超時時間為:350+100=250ms。
5.1.2、statement超時:
用來限制sql語句的執(zhí)行時間,通過setQueryTimeout(int timeout)來設置,不過現(xiàn)在大都是ibatis了,可以通過 SqlMapConfig.xml 中的 setting 屬性defaultStatementTimeout 來設置全局的 statement 超時缺省值<settings defaultStatementTimeout="15"/>,還可以在每個sql.xml文件中,根據(jù)業(yè)務實際需要來設置<select timeout="10"/> <insert timeout="10"/> <update timeout="10"/>,這樣就會覆蓋掉全局的值,而采用具體的閾值。
5.1.3、Socket超時:
這是底層的一種超時,因為我們使用的JDBC驅動類型是TYPE4,它是基于socket來通信的。mysql的jdbc驅動中的connectTimeout 和 socketTimeout 的默認值是 0 ,這意味著不會發(fā)生超時。所以我們必須在dbcp配置中設置這兩個值。<property name="connectionProperties" value="connectTimeout=2000;socketTimeout=15000"/>
connectTimeout為建立連接的超時時間,socketTimeout為JDBC客戶端和數(shù)據(jù)庫服務器之間數(shù)據(jù)交互的時間。注意這里配置的socketTimeout的值必須要大于Statement的超時時間值。否則Statement超時就沒有意義,也不能生效。
5.1.4、操作系統(tǒng)的socket超時
linux操作系統(tǒng)也會設置socket超時,比如我們這邊的服務器一般配置的是20分鐘,因為公司的linux服務器的KeepAlive檢查周期為20分鐘。這樣即使上面的socketTimeout值為0用不超時,也還是要收到linux服務器的超時限制,也就是由于網(wǎng)絡原因引起的數(shù)據(jù)庫網(wǎng)絡連接問題也不會超過20分鐘。
5.2、Mysql處理超時的機制及原理
上圖是一mysql在執(zhí)行一個命令的過程中的步驟以及發(fā)生超時現(xiàn)象后的處理機制
1、通過Connection的createStatement()方法去創(chuàng)建一個Statement,以便后續(xù)進行讀寫操作
2、執(zhí)行第1步創(chuàng)建的Statement的executeQuery()方法
3、將查詢請求命令發(fā)送到mysql數(shù)據(jù)庫服務器
4、創(chuàng)建一個超時線程(從5.1版本以后在,創(chuàng)建每個連接的時候,會隨之創(chuàng)建一個處理超時的線程timeout-execution)
5、把當前的statement對象注冊到超時線程timeout-execution中
6、發(fā)生了超時(閾值是你在Statement執(zhí)行前候配置的setQueryTimeout(int timeout),如果是mybatis則是在配置文件里面配置的值defaultStatementTimeout="15",單位s)
7、超時線程會重新創(chuàng)建一個新的Connection,這個Connection的屬性配置都跟先前的一樣
8、用新創(chuàng)建的這個Connection去發(fā)送取消查詢請求
六、各種數(shù)據(jù)庫連接池性能對比
測試環(huán)境,OS: OS X 10.8.2 CPU:intel i7 2GHz 4 core JVM:java version "1.7.0_05"
測試執(zhí)行申請歸還連接1,000,000(一百萬)次總耗時性能對比,Java7 的基準測試結果如下:
Druid是性能最好的數(shù)據(jù)庫連接池,DBCP性能屬于中上,我們在以后的業(yè)務場景中可以根據(jù)實際需要做選擇。這個是DRUID的官方測試,不過可以利用測試代碼自己進行驗證一遍,測試代碼如下:https://github.com/alibaba/druid/blob/master/src/test/java/com/alibaba/druid/benckmark/pool/Case1.java
七、總結
我們梳理了數(shù)據(jù)庫連接相關的概念,JNDI、JDBC、連接池,DBCP的核心類和時序圖,另外我們還梳理了commons的池化組件,最后我們描述了JDBC連接過程中的超時機制,包括超時依賴和超時的機制原理,超時的知識點對與我們非常重要,超時是一種防護措施,當依賴DB性能變慢或者網(wǎng)絡故障的情況下,可以快速失敗。以便保護我們的應用程序不會導致線程堆積而發(fā)生雪崩這類致命事故。DBCP經(jīng)歷了各種互聯(lián)網(wǎng)應用的驗證,穩(wěn)定可靠的性能會一直服務于我們的系統(tǒng)中。當今比較流行的spring boot內(nèi)部集成的Tomcat應用依然在使用dbcp連接池。
轉載請注明出處,并附上鏈接http://www.lxweimin.com/p/7e1b36c7fb82
參考資料
https://www.cubrid.org/blog/understanding-jdbc-internals-and-timeout-configuration
https://zh.wikipedia.org/wiki/Java%E6%95%B0%E6%8D%AE%E5%BA%93%E8%BF%9E%E6%8E%A5
http://shift-alt-ctrl.iteye.com/blog/1917782
https://commons.apache.org/proper/commons-pool/index.html
https://commons.apache.org/proper/commons-dbcp/index.html