一.早期我們怎么進行數據庫操作
1.原理:一般來說,java應用程序訪問數據庫的過程是:
①裝載數據庫驅動程序;
②通過jdbc建立數據庫連接;
③訪問數據庫,執行sql語句;
④斷開數據庫連接。
2.代碼
// 查詢所有用戶
- Public void FindAllUsers(){
- //1、裝載sqlserver驅動對象
- DriverManager.registerDriver(new SQLServerDriver());
- //2、通過JDBC建立數據庫連接
- Connection con =DriverManager.getConnection("jdbc:sqlserver://192.168.2.6:1433;DatabaseName=customer", "sa", "123");
- //3、創建狀態
- Statement state =con.createStatement();
- //4、查詢數據庫並返回結果
- ResultSet result =state.executeQuery("select * from users");
- //5、輸出查詢結果
- while(result.next()){
- System.out.println(result.getString("email"));
- }
- //6、斷開數據庫連接
- result.close();
- state.close();
- con.close();
- }
3.分析
程序開發過程中,存在很多問題:首先,每一次web請求都要建立一次數據庫連接。建立連接是一個費時的活動,每次都得花費0.05s~1s的時間,而且系統還要分配內存資源。這個時間對於一次或幾次數據庫操作,或許感覺不出系統有多大的開銷。可是對於現在的web應用,尤其是大型電子商務網站,同時有幾百人甚至幾千人在線是很正常的事。在這種情況下,頻繁的進行數據庫連接操作勢必占用很多的系統資源,網站的響應速度必定下降,嚴重的甚至會造成服務器的崩潰。不是危言聳聽,這就是制約某些電子商務網站發展的技術瓶頸問題。其次,對於每一次數據庫連接,使用完后都得斷開。否則,如果程序出現異常而未能關閉,將會導致數據庫系統中的內存泄漏,最終將不得不重啟數據庫。還有,這種開發不能控制被創建的連接對象數,系統資源會被毫無顧及的分配出去,如連接過多,也可能導致內存泄漏,服務器崩潰。
上述的用戶查詢案例,如果同時有1000人訪問,就會不斷的有數據庫連接、斷開操作:
通過上面的分析,我們可以看出來,“數據庫連接”是一種稀缺的資源,為了保障網站的正常使用,應該對其進行妥善管理。其實我們查詢完數據庫后,如果不關閉連接,而是暫時存放起來,當別人使用時,把這個連接給他們使用。就避免了一次建立數據庫連接和斷開的操作時間消耗。原理如下:
二. 技術演進出來的數據庫連接池
由上面的分析可以看出,問題的根源就在於對數據庫連接資源的低效管理。我們知道,對於共享資源,有一個很著名的設計模式:資源池(resource pool)。該模式正是為了解決資源的頻繁分配﹑釋放所造成的問題。為解決上述問題,可以采用數據庫連接池技術。數據庫連接池的基本思想就是為數據庫連接建立一個“緩沖池”。預先在緩沖池中放入一定數量的連接,當需要建立數據庫連接時,只需從“緩沖池”中取出一個,使用完畢之后再放回去。我們可以通過設定連接池最大連接數來防止系統無盡的與數據庫連接。更為重要的是我們可以通過連接池的管理機制監視數據庫的連接的數量﹑使用情況,為系統開發﹑測試及性能調整提供依據。
我們自己嘗試開發一個連接池,來為上面的查詢業務提供數據庫連接服務:
① 編寫class 實現DataSource 接口
② 在class構造器一次性創建10個連接,將連接保存LinkedList中
③ 實現getConnection 從 LinkedList中返回一個連接
④ 提供將連接放回連接池中方法
1、連接池代碼
- public class MyDataSource implements DataSource {
- //鏈表 --- 實現棧結構
- privateLinkedList<Connection> dataSources = new LinkedList<Connection>();
- //初始化連接數量
- publicMyDataSource() {
- //一次性創建10個連接
- for(int i = 0; i < 10; i++) {
- try {
- //1、裝載sqlserver驅動對象
- DriverManager.registerDriver(new SQLServerDriver());
- //2、通過JDBC建立數據庫連接
- Connection con =DriverManager.getConnection(
- "jdbc:sqlserver://192.168.2.6:1433;DatabaseName=customer", "sa", "123");
- //3、將連接加入連接池中
- dataSources.add(con);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- @Override
- publicConnection getConnection() throws SQLException {
- //取出連接池中一個連接
- finalConnection conn = dataSources.removeFirst(); // 刪除第一個連接返回
- returnconn;
- }
- //將連接放回連接池
- publicvoid releaseConnection(Connection conn) {
- dataSources.add(conn);
- }
- }
2、使用連接池重構我們的用戶查詢函數
- //查詢所有用戶
- Public void FindAllUsers(){
- //1、使用連接池建立數據庫連接
- MyDataSource dataSource = new MyDataSource();
- Connection conn =dataSource.getConnection();
- //2、創建狀態
- Statement state =con.createStatement();
- //3、查詢數據庫並返回結果
- ResultSet result =state.executeQuery("select * from users");
- //4、輸出查詢結果
- while(result.next()){
- System.out.println(result.getString("email"));
- }
- //5、斷開數據庫連接
- result.close();
- state.close();
- //6、歸還數據庫連接給連接池
- dataSource.releaseConnection(conn);
- }
這就是數據庫連接池的原理,它大大提供了數據庫連接的利用率,減小了內存吞吐的開銷。我們在開發過程中,就不需要再關心數據庫連接的問題,自然有數據庫連接池幫助我們處理,這回放心了吧。但連接池需要考慮的問題不僅僅如此,下面我們就看看還有哪些問題需要考慮。
三.連接池還要考慮更多的問題
1、並發問題
為了使連接管理服務具有最大的通用性,必須考慮多線程環境,即並發問題。這個問題相對比較好解決,因為java語言自身提供了對並發管理的支持,使用synchronized關鍵字即可確保線程是同步的。使用方法為直接在類方法前面加上synchronized關鍵字,如:
publicsynchronized connection getconnection()
2、多數據庫服務器和多用戶
對於大型的企業級應用,常常需要同時連接不同的數據庫(如連接oracle和sybase)。如何連接不同的數據庫呢?我們采用的策略是:設計一個符合單例模式的連接池管理類,在連接池管理類的唯一實例被創建時讀取一個資源文件,其中資源文件中存放着多個數據庫的url地址等信息。根據資源文件提供的信息,創建多個連接池類的實例,每一個實例都是一個特定數據庫的連接池。連接池管理類實例為每個連接池實例取一個名字,通過不同的名字來管理不同的連接池。
對於同一個數據庫有多個用戶使用不同的名稱和密碼訪問的情況,也可以通過資源文件處理,即在資源文件中設置多個具有相同url地址,但具有不同用戶名和密碼的數據庫連接信息。
3、事務處理
我們知道,事務具有原子性,此時要求對數據庫的操作符合“all-all-nothing”原則即對於一組sql語句要么全做,要么全不做。
在java語言中,connection類本身提供了對事務的支持,可以通過設置connection的autocommit屬性為false 然后顯式的調用commit或rollback方法來實現。但要高效的進行connection復用,就必須提供相應的事務支持機制。可采用每一個事務獨占一個連接來實現,這種方法可以大大降低事務管理的復雜性。
4、連接池的分配與釋放
連接池的分配與釋放,對系統的性能有很大的影響。合理的分配與釋放,可以提高連接的復用度,從而降低建立新連接的開銷,同時還可以加快用戶的訪問速度。
對於連接的管理可使用空閑池。即把已經創建但尚未分配出去的連接按創建時間存放到一個空閑池中。每當用戶請求一個連接時,系統首先檢查空閑池內有沒有空閑連接。如果有就把建立時間最長(通過容器的順序存放實現)的那個連接分配給他(實際是先做連接是否有效的判斷,如果可用就分配給用戶,如不可用就把這個連接從空閑池刪掉,重新檢測空閑池是否還有連接);如果沒有則檢查當前所開連接池是否達到連接池所允許的最大連接數(maxconn)如果沒有達到,就新建一個連接,如果已經達到,就等待一定的時間(timeout)。如果在等待的時間內有連接被釋放出來就可以把這個連接分配給等待的用戶,如果等待時間超過預定時間timeout 則返回空值(null)。系統對已經分配出去正在使用的連接只做計數,當使用完后再返還給空閑池。對於空閑連接的狀態,可開辟專門的線程定時檢測,這樣會花費一定的系統開銷,但可以保證較快的響應速度。也可采取不開辟專門線程,只是在分配前檢測的方法。
5、連接池的配置與維護
連接池中到底應該放置多少連接,才能使系統的性能最佳?系統可采取設置最小連接數(minconn)和最大連接數(maxconn)來控制連接池中的連接。最小連接數是系統啟動時連接池所創建的連接數。如果創建過多,則系統啟動就慢,但創建后系統的響應速度會很快;如果創建過少,則系統啟動的很快,響應起來卻慢。這樣,可以在開發時,設置較小的最小連接數,開發起來會快,而在系統實際使用時設置較大的,因為這樣對訪問客戶來說速度會快些。最大連接數是連接池中允許連接的最大數目,具體設置多少,要看系統的訪問量,可通過反復測試,找到最佳點。
如何確保連接池中的最小連接數呢?有動態和靜態兩種策略。動態即每隔一定時間就對連接池進行檢測,如果發現連接數量小於最小連接數,則補充相應數量的新連接以保證連接池的正常運轉。靜態是發現空閑連接不夠時再去檢查。
四.實際開發中有成熟的開源連接池供我們使用
理解了連接池的原理就可以了,沒有必要什么都從頭寫一遍,那樣會花費很多時間,並且性能及穩定性也不一定滿足要求。事實上,已經存在很多流行的性能優良的第三方數據庫連接池jar包供我們使用。如:
1.Apache commons-dbcp 連接池
下載:http://commons.apache.org/proper/commons-dbcp/
2.c3p0 數據庫連接池
下載:http://sourceforge.net/projects/c3p0/
- c3p0是什么
c3p0的出現,是為了大大提高應用程序和數據庫之間訪問效率的。
它的特性:
- 編碼的簡單易用
- 連接的復用
- 連接的管理
說到c3p0,不得不說一下jdbc本身,c3p0願意就是對數據庫連接的管理,那么原有的概念還是得清晰:DriverManager、Connection、StateMent、ResultMent。
jdbc:java database connective這套API,不用多說,是一套用於連接各式dbms或連接橋接器的api,兩個層級:上層供應用方調用api,下層,定義了各個dbms的spi的api(具體文檔見:這里)。
主要要提的是:datasource、DriverManager,想到哪兒寫到哪兒,datasource是更高級一點的api,原因在於相對對應用來說更透明。
Connection:同dbms的邏輯鏈接,類似於session管理概念, SQL statements are executed and results are returned within the context of a connection.
jdbc的概念就到這里,平時用得比較多。
- c3P0的概念
c3p0的bean配置如下:
1 <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> 2 <property name="driverClass" value="${jdbc.driverClassName}" /> 3 <property name="jdbcUrl" value="${jdbc.url}" /> 4 <property name="user" value="${jdbc.username}" /> 5 <property name="password" value="${jdbc.password}" /> 6 <property name="checkoutTimeout" value="30000" /> 7 <property name="maxPoolSize" value="15" /> 8 <property name="idleConnectionTestPeriod" value="180" /> 9 <property name="maxIdleTime" value="180" /> 10 </bean>
還有一些配置選項,后續詳細說明。可見c3p0的bean引用使用的是:ComboPooledDataSource,該類結構如下:
以上類圖都不是很完全,不過大體能表達出類之間的原理:
1、bean:ComboPooledDataSource的父類:AbstractPoolBackedDataSource有一個poolmanager字段,存儲着對pool管理器
2、獲取ds.getConnection()鏈接對象時,內部使用getPoolManger()獲取C3p0ConnectionPooledManager(mgr)對象,該manager管理着pool對象:C3P0PooledConnectionPool對象,mgr.getPool().checkoutPooledConnection()
3、自此該connection已經被獲取到了
4、讓我們看看該connection的真實面目吧:
ProxyConnection。
5、因此其實原理是:
從pool里獲取到的connection,是proxy包裝的connection,而對connection的釋放或者重用,是pool的管理責任:初始化池大小,維護池的大小(expand或shrink),管理unused、expired、checkout、checkin連接。
真正底層的連接是jdbc自己的連接,而c3p0的管理部分,基本上使用的是synchronized關鍵字,使用timerTask定時器工作。
配置介紹:
<c3p0-config>
<default-config>
<!--當連接池中的連接耗盡的時候c3p0一次同時獲取的連接數。Default: 3 -->
<property name="acquireIncrement">3</property>
<!--定義在從數據庫獲取新連接失敗后重復嘗試的次數。Default: 30 -->
<property name="acquireRetryAttempts">30</property>
<!--兩次連接中間隔時間,單位毫秒。Default: 1000 -->
<property name="acquireRetryDelay">1000</property>
<!--連接關閉時默認將所有未提交的操作回滾。Default: false -->
<property name="autoCommitOnClose">false</property>
<!--c3p0將建一張名為Test的空表,並使用其自帶的查詢語句進行測試。如果定義了這個參數那么
屬性preferredTestQuery將被忽略。你不能在這張Test表上進行任何操作,它將只供c3p0測試
使用。Default: null-->
<property name="automaticTestTable">Test</property>
<!--獲取連接失敗將會引起所有等待連接池來獲取連接的線程拋出異常。但是數據源仍有效
保留,並在下次調用getConnection()的時候繼續嘗試獲取連接。如果設為true,那么在嘗試
獲取連接失敗后該數據源將申明已斷開並永久關閉。Default: false-->
<property name="breakAfterAcquireFailure">false</property>
<!--當連接池用完時客戶端調用getConnection()后等待獲取新連接的時間,超時后將拋出
SQLException,如設為0則無限期等待。單位毫秒。Default: 0 -->
<property name="checkoutTimeout">100</property>
<!--通過實現ConnectionTester或QueryConnectionTester的類來測試連接。類名需制定全路徑。
Default: com.mchange.v2.c3p0.impl.DefaultConnectionTester-->
<property name="connectionTesterClassName"></property>
<!--指定c3p0 libraries的路徑,如果(通常都是這樣)在本地即可獲得那么無需設置,默認null即可
Default: null-->
<property name="factoryClassLocation">null</property>
<!--Strongly disrecommended. Setting this to true may lead to subtle and bizarre bugs.
(文檔原文)作者強烈建議不使用的一個屬性-->
<property name="forceIgnoreUnresolvedTransactions">false</property>
<!--每60秒檢查所有連接池中的空閑連接。Default: 0 -->
<property name="idleConnectionTestPeriod">60</property>
<!--初始化時獲取三個連接,取值應在minPoolSize與maxPoolSize之間。Default: 3 -->
<property name="initialPoolSize">3</property>
<!--最大空閑時間,60秒內未使用則連接被丟棄。若為0則永不丟棄。Default: 0 -->
<property name="maxIdleTime">60</property>
<!--連接池中保留的最大連接數。Default: 15 -->
<property name="maxPoolSize">15</property>
<!--JDBC的標准參數,用以控制數據源內加載的PreparedStatements數量。但由於預緩存的statements
屬於單個connection而不是整個連接池。所以設置這個參數需要考慮到多方面的因素。
如果maxStatements與maxStatementsPerConnection均為0,則緩存被關閉。Default: 0-->
<property name="maxStatements">100</property>
<!--maxStatementsPerConnection定義了連接池內單個連接所擁有的最大緩存statements數。Default: 0 -->
<property name="maxStatementsPerConnection"></property>
<!--c3p0是異步操作的,緩慢的JDBC操作通過幫助進程完成。擴展這些操作可以有效的提升性能
通過多線程實現多個操作同時被執行。Default: 3-->
<property name="numHelperThreads">3</property>
<!--當用戶調用getConnection()時使root用戶成為去獲取連接的用戶。主要用於連接池連接非c3p0
的數據源時。Default: null-->
<property name="overrideDefaultUser">root</property>
<!--與overrideDefaultUser參數對應使用的一個參數。Default: null-->
<property name="overrideDefaultPassword">password</property>
<!--密碼。Default: null-->
<property name="password"></property>
<!--定義所有連接測試都執行的測試語句。在使用連接測試的情況下這個一顯著提高測試速度。注意:
測試的表必須在初始數據源的時候就存在。Default: null-->
<property name="preferredTestQuery">select id from test where id=1</property>
<!--用戶修改系統配置參數執行前最多等待300秒。Default: 300 -->
<property name="propertyCycle">300</property>
<!--因性能消耗大請只在需要的時候使用它。如果設為true那么在每個connection提交的
時候都將校驗其有效性。建議使用idleConnectionTestPeriod或automaticTestTable
等方法來提升連接測試的性能。Default: false -->
<property name="testConnectionOnCheckout">false</property>
<!--如果設為true那么在取得連接的同時將校驗連接的有效性。Default: false -->
<property name="testConnectionOnCheckin">true</property>
<!--用戶名。Default: null-->
<property name="user">root</property>
在Hibernate(spring管理)中的配置:
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass"><value>oracle.jdbc.driver.OracleDriver</value></property>
<property name="jdbcUrl"><value>jdbc:oracle:thin:@localhost:1521:Test</value></property>
<property name="user"><value>Kay</value></property>
<property name="password"><value>root</value></property>
<!--連接池中保留的最小連接數。-->
<property name="minPoolSize" value="10" />
<!--連接池中保留的最大連接數。Default: 15 -->
<property name="maxPoolSize" value="100" />
<!--最大空閑時間,1800秒內未使用則連接被丟棄。若為0則永不丟棄。Default: 0 -->
<property name="maxIdleTime" value="1800" />
<!--當連接池中的連接耗盡的時候c3p0一次同時獲取的連接數。Default: 3 -->
<property name="acquireIncrement" value="3" />
<property name="maxStatements" value="1000" />
<property name="initialPoolSize" value="10" />
<!--每60秒檢查所有連接池中的空閑連接。Default: 0 -->
<property name="idleConnectionTestPeriod" value="60" />
<!--定義在從數據庫獲取新連接失敗后重復嘗試的次數。Default: 30 -->
<property name="acquireRetryAttempts" value="30" />
<property name="breakAfterAcquireFailure" value="true" />
<property name="testConnectionOnCheckout" value="false" />
</bean>