簡單的MySQL連接池
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- />
當tomcat讀到type="javax.sql.DataSource"屬性時會自動重新安裝DBCP,除非你指定不同的factory。factory object 本身就是創建和配置連接池的。
在Apache Tomcat中有兩種方式配置 Resource elements
配置全局連接池
編輯conf/server.xml
- <GlobalNamingResources>
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- />
- </GlobalNamingResources>
然后你需要創建一個 ResourceLink element使這個連接池對於web應用是可用的。如果你想要用同一個名字讓連接池對於所有的應用有效,最簡單的方法就是編輯 conf/context.xml文件
- <Context>
- <ResourceLink type="javax.sql.DataSource"
- name="jdbc/LocalTestDB"
- global="jdbc/TestDB"
- />
- <Context>
注意,如果你不想要全局的連接池,可以從 server.xml移除 Resource element到你的web應用的 context.xml 文件。
然后從剛配置好的連接池中獲得連接,簡單java代碼:
- Context initContext = new InitialContext();
- Context envContext = (Context)initContext.lookup("java:/comp/env");
- DataSource datasource = (DataSource)envContext.lookup("jdbc/LocalTestDB");
- Connection con = datasource.getConnection();
使用java很簡單
還可以使用Java syntax
- DataSource ds = new DataSource();
- ds.setDriverClassName("com.mysql.jdbc.Driver");
- ds.setUrl("jdbc:mysql://localhost:3306/mysql");
- ds.setUsername("root");
- ds.setPassword("password");
- PoolProperties pp = new PoolProperties();
- pp.setDriverClassName("com.mysql.jdbc.Driver");
- pp.setUrl("jdbc:mysql://localhost:3306/mysql");
- pp.setUsername("root");
- pp.setPassword("password");
- DataSource ds = new DataSource(pp);
設置連接池
我們將使用下面這些屬性設置連接池
- initialSize
- maxActive
- maxIdle
- minIdle
去了解這些屬性是很重要的,它們看起來很明顯但又有一些神秘
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- initialSize="10"
- maxActive="100"
- maxIdle="50"
- minIdle="10"
- />
initialSize=10 設置連接池建立時連接的數目
- 當連接池定義在GlobalNamingResources中,連接池在Tomcat啟動時創鍵
- 當連接池定義在Context中,連接池在第一次查找JNDI時創建
maxActive=100 連接數據庫的最大連接數。這個屬性用來限制連接池中能夠打開連接的數量,可以方便數據庫做連接容量規划。
minIdle=10 連接池中存在的最小連接數目。連接池中連接數目可以變很少,如果使用了maxAge屬性,有些空閑的連接會被關閉因為離它最近一次連接的時間過去太久了。但是,我們看到的打開的連接不會少於minIdle。
maxIdle屬性有一點麻煩。它的不同的行為取決於是否使用了pool sweeper。pool sweeper是一個可以在連接池正在使用的時候測試空閑連接和重置連接池大小的后台線程。還負責檢測連接泄露。 pool sweeper 通過如下方式定義的:
- public boolean isPoolSweeperEnabled() {
- boolean timer = getTimeBetweenEvictionRunsMillis()>0;
- boolean result = timer && (isRemoveAbandoned() && getRemoveAbandonedTimeout()>0);
- result = result || (timer && getSuspectTimeout()>0);
- result = result || (timer && isTestWhileIdle() && getValidationQuery()!=null);
- return result;
- }
sweeper每 timeBetweenEvictionRunsMillis milliseconds運行一次。
maxIdle定義如下
- Pool sweeper關閉,如果空閑連接池大於maxIdle,返回的連接將被關閉。
- Pool sweeper開啟,空閑的連接數可以超過maxIdle,但如果連接空閑的時間已經超過minEvictableIdleTimeMillis,能縮小到minIdle。聽起來很奇怪連接池為什么不關閉連接當空閑連接數量大於maxIdle。想想下面的情況:
- 100個線程處理100個並發請求
- 在一個請求中每個線程請求一個連接3次
在這種場景下,如果我們設置maxIdle=50,那么我們會關閉和打開50*3的連接數。這樣增加了數據庫的負重並且減慢了應用的速度。當達到連接高峰時,我們希望能夠充分利用連接池中的所有連接。因此,我們強烈希望打開pool sweeper 。我們將在下一個部分探討具體的事項。我們在這里額外說明maxAge這個屬性。maxAge定義連接能夠打開或者存在的時間,單位為毫秒。當一個連接返回到了連接池,如果這個連接已經使用過,並且距離它第一次被使用的時間大於maxAge時,這個連接會被關閉。
正如我們所看到的 isPoolSweeper算法實現,sweeper 將會被打開,當以下任一條件滿足時
- timeBetweenEvictionRunsMillis>0 AND removeAbandoned=true ANDremoveAbandonedTimeout>0
- timeBetweenEvictionRunsMillis>0 AND suspectTimeout>0
- timeBetweenEvictionRunsMillis>0 AND testWhileIdle=true AND validationQuery!=null
As of version 1.0.9 the following condition has been added
- timeBetweenEvictionRunsMillis>0 AND minEvictableIdleTimeMillis>0
(timer && getMinEvictableIdleTimeMillis()>0);
因此設置最理想的連接池,我們最好修改我們的配置滿足這些條件
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- initialSize="10"
- maxActive="100"
- maxIdle="50"
- minIdle="10"
- suspectTimeout="60"
- timeBetweenEvictionRunsMillis="30000"
- minEvictableIdleTimeMillis="60000"
- />
有效的連接
數據庫連接池提出了一個挑戰,因為連接池中的連接會過時。這是常有的事,要么數據庫,或者可能是連接池和數據庫中的一個設備,連接超時。唯一確定會話連接是活躍的真正辦法是使連接在服務器和數據庫做一個來回訪問。在Java 6中,JDBC API處理驗證連接是否是有效的方法是通過提供isValid變量來調用java.sql.Connection接口。在此之前,連接池不得不采用執行一個查詢的方法,比如在MySQL上執行SELECT 1.數據庫分析這句查詢很簡單,不需要任何的磁盤訪問。isValid被計划實施,但 Apache Tomcat 6的連接池,也必須保存對Java 5的兼容性。
校驗查詢
校驗查詢會有一些挑戰
- 如果它們頻繁使用,會降低系統的性能
- 如果使用的間隔太久,會導致連接失效
- 如果應用調用setTransactionIsolation並設置autoCommit=false,如果應用再次調用setTransactionIsolation,會產生一個SQLException異常,因為校驗查詢可能在數據庫中已經產生了一個新的transaction。
讓我們看看最典型的配置:
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- testOnBorrow="true"
- validationQuery="SELECT 1"
- />
在這個配置中,java代碼每次調用 Connection con = dataSource.getConnection()時都會執行一次 SELECT 1查詢。
這樣保證了在連接提交給應用之前都已經測試過了。但是,對於在短時間內頻繁使用連接的應用,會對性能有嚴重的影響。這有兩個其他的配置選項:
- testWhileIdle
- testOnReturn
當在錯誤的時間對連接做測試,它們也不是真正的很有幫助。
對於很多應用來說,沒有校驗不是一個真正的困難。一些應用可以繞過校驗通過設置minIdle=0和給minEvictableIdleTimeMillis一個很小的值,所以如果連接空閑了足夠長的時間會讓數據庫會話超時,在此之前連接池將會移除這些空閑太久的連接。
最好的解決辦法就是測試那些有一段時間沒被測試過的連接。
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- testOnBorrow="true"
- validationQuery="SELECT 1"
- validationInterval="30000"
- />
在這個配置中,連接校驗的間隔不會超過30s。這是在性能和連接驗證上的折中。正如前面提到的,如果我們想僥幸驗證所有的連接,我們可以配置連接池中所有空閑連接超時。
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- timeBetweenEvictionRunsMillis="5000"
- minEvictableIdleTimeMillis="5000"
- minIdle="0"
- />
建立數據庫客戶會話
在一些案例中,當初始化一個新的數據庫會話時需要執行一些任務。可能包括執行一個簡單的SQL聲明或者執行一個存儲過程。
當你創建觸發器時候,這是在數據庫層面上的典型操作。
- create or replace trigger logon_alter_session after logon on database
- begin
- if sys_context('USERENV', 'SESSION_USER') = 'TEMP' then
- EXECUTE IMMEDIATE 'alter session ....';
- end if;
- end;
- /
這將影響所有的用戶,在后面這種情況下這是不夠的,當創建一個新的會話的時候我們希望執行一個自定義查詢。
- <Resource name="jdbc/TestDB" auth="Container"
- type="javax.sql.DataSource"
- description="Oracle Datasource"
- url="jdbc:oracle:thin:@//localhost:1521/orcl"
- driverClassName="oracle.jdbc.driver.OracleDriver"
- username="default_user"
- password="password"
- maxActive="100"
- validationQuery="select 1 from dual"
- validationInterval="30000"
- testOnBorrow="true"
- initSQL="ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY MM DD HH24:MI:SS'"/>
initSQL會被存在的每一條連接執行。
連接池泄露和長時間運行的查詢
連接池包含一些診斷操作。jdbc-pool和Common DBCP都能夠檢測和減輕沒有返回連接池中的連接。這里演示是被稱為拋出內存泄露的連接。
- Connection con = dataSource.getConnection();
- Statement st = con.createStatement();
- st.executeUpdate("insert into id(value) values (1'); //SQLException here
- con.close();
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- maxActive="100"
- timeBetweenEvictionRunsMillis="30000"
- removeAbandoned="true"
- removeAbandonedTimeout="60"
- logAbandoned="true"
- />
- removeAbandoned-如果我們想檢測內存泄露的連接,可以設置為true
- removeAbandonedTimeout-調用dataSource.getConnection開始到丟棄檢測到泄露連接的時間(seconds)
- logAbandoned-如果想用log記錄丟棄的連接,可以設置為true。當設置為true時,調用dataSource.getConnection 時會記錄一個堆棧追蹤,並且被打印出來當連接沒有返回的時候。
但我們想要這種類型的診斷,當然有可以使用的例子。也可以運行批處理作業一次執行一個連接幾分鍾。我們該如何處理這些問題?
兩個額外的選項已經被加入來支持這些工作
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- maxActive="100"
- timeBetweenEvictionRunsMillis="30000"
- removeAbandoned="true"
- removeAbandonedTimeout="60"
- logAbandoned="true"
- abandonWhenPercentageFull="50"
- />
- abandonWhenPercentageFull-一條連接必須滿足臨界值 removeAbandonedTimeout和打開連接的數量必須超過這個百分比。
使用這個屬性可能會在一次錯誤判斷中產生在其他地方已經被認為丟棄的連接。設置這個值為100時意味着連接數除非到了maxActive限制時,是不會被考慮丟棄的。這給連接池增加了一些靈活性,但是不會讓批處理作業使用單獨連接5分鍾。在這種情況,我們想確定當我們檢測到連接仍然被使用時,我們重置超時計時器,因此,連接不會被考慮丟棄。我們通過插入一個攔截器實現。
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- maxActive="100"
- timeBetweenEvictionRunsMillis="30000"
- removeAbandoned="true"
- removeAbandonedTimeout="60"
- logAbandoned="true"
- abandonWhenPercentageFull="50"
- jdbcInterceptors="ResetAbandonedTimer"
- />
攔截器在 org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer中被指定完全限定名稱,或者在 org.apache.tomcat.jdbc.pool.interceptor包中使用短類名
每次准備語句或者執行一次查詢,連接池中的計時器會被重置放棄計時器。因為如此,在5分鍾的批處理作業中執行多次查詢和更新,都不會超時。
這是你當然想知道的情形,但你不會想去kill或者回收連接,因為你不會知道會對你的系統產生什么影響。
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- maxActive="100"
- timeBetweenEvictionRunsMillis="30000"
- logAbandoned="true"
- suspectTimeout="60"
- jdbcInterceptors="ResetAbandonedTimer"
- />
suspectTimeout屬性的工作方式與 removeAbandonedTimeout 相似,除了不關閉連接,而只是簡單的記錄警告和發布一個JMX通知信息。通過這種方式,你可以在不用改變你系統行為的情況下發現泄漏或者長查詢。
從其它的數據源形成連接池
到目前為止我們處理連接池連接的獲得是通過java.sql.Driver接口。因此我們使用屬性
- driverClassName
- url
然而,一些連接配置是使用 javax.sql.DataSource 甚至是javax.sql.XADataSource接口,因此我們需要支持這些配置選項。
使用java相對是很容易的。
- PoolProperties pp = new PoolProperties();
- pp.setDataSource(myOtherDataSource);
- DataSource ds = new DataSource(pp);
- Connection con = ds.getConnection();
- DataSource ds = new DataSource();
- ds.setDataSource(myOtherDataSource);
- Connection con = ds.getConnection();
在我們處理XA連接時很方便。
在XML配置中,jdbc-pool會使用org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory類,一個能夠允許配置任何類型的命名資源的簡單類。為了設置 Apache Derby XADataSource 我們可以創建了下面的代碼
- <Resource factory="org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory"
- name="jdbc/DerbyXA1"
- type="org.apache.derby.jdbc.ClientXADataSource"
- databaseName="sample1"
- createDatabase="create"
- serverName="localhost"
- portNumber="1527"
- user="sample1"
- password="password"/>
這是一個簡單的通過端口1527連接到網絡上的相鄰實例的 XADataSource.
如果你想要從這個數據源形成XA連接池,我們可以在它后面建立這個連接池節點。
- <Resource factory="org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory"
- name="jdbc/DerbyXA1"
- type="org.apache.derby.jdbc.ClientXADataSource"
- databaseName="sample1"
- createDatabase="create"
- serverName="localhost"
- portNumber="1527"
- user="sample1"
- password="password"/>
- <Resource factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- dataSourceJNDI="DerbyXA1"<!--Links to the Derby XADataSource-->
- name="jdbc/TestDB1"
- auth="Container"
- type="javax.sql.XADataSource"
- testWhileIdle="true"
- testOnBorrow="true"
- testOnReturn="false"
- validationQuery="SELECT 1"
- validationInterval="30000"
- timeBetweenEvictionRunsMillis="5000"
- maxActive="100"
- minIdle="10"
- maxIdle="20"
- maxWait="10000"
- initialSize="10"
- removeAbandonedTimeout="60"
- removeAbandoned="true"
- logAbandoned="true"
- minEvictableIdleTimeMillis="30000"
- jmxEnabled="true"
- jdbcInterceptors="ConnectionState;StatementFinalizer;SlowQueryReportJmx(threshold=10000)"
- abandonWhenPercentageFull="75"/>
這里我們通過 dataSourceJNDI=DerbyXA1屬性鏈接這兩個數據源。這兩個數據源都不得不存在同一個命名空間,在我們的例子中,是jdbc命名空間。
目前JNDI通過DataSource.setDataSourceJNDI(...)查找不被支持,只能通過factory對象。
如果你加入一個
- javax.sql.DataSource對象-連接池將會調用 javax.sql.DataSource.getConnection()方法
- javax.sql.DataSource 對象但是在連接池中指定了username/password-連接池將會調用javax.sql.DataSource.getConnection(String username, String password) 方法
- javax.sql.XADataSource對象-連接池將會調用 javax.sql.XADataSource.getXAConnection() 方法
- javax.sql.XADataSource 對象但是在連接池中指定了 username/password-連接池將會調用javax.sql.DataSource.getXAConnection(String username, String password) 方法
這是一個有趣的現象當你處理 XADataSources。你可以把返回的對象轉換為java.sql.Connection對象或者javax.sql.XAConnection對象,並且對同一個對象的兩個接口調用方法。
- DataSource ds = new DataSource();
- ds.setDataSource(myOtherDataSource);
- Connection con = ds.getConnection();
- if (con instanceof XAConnection) {
- XAConnection xacon = (XAConnection)con;
- transactionManager.enlistResource(xacon.getXAResource());
- }
- Statement st = con.createStatement();
- ResultSet rs = st.executeQuery(SELECT 1);
JDBC 攔截器
JDBC 攔截器創建是為了實現靈活性。javax.sql.PooledConnection 從底層驅動封裝了java.sql.Connection/javax.sql.XAConnection或者數據源本身就是一個攔截器。攔截器以java.lang.reflect.InvocationHandler接口為基礎。攔截器是一個繼承自org.apache.tomcat.pool.jdbc.JdbcInterceptor的類。
在本文中,我們將介紹如果配置攔截器。在我們下一篇文章,我們將介紹如果實現自定義攔截器和它們的生命周期。
- <Resource factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- ...
- jdbcInterceptors="ConnectionState;StatementFinalizer;SlowQueryReportJmx(threshold=10000)"
- />
與下面的相同
- <Resource factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- ...
- jdbcInterceptors="org.apache.tomcat.jdbc.pool.interceptor.ConnectionState;
- org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer;
- org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReportJmx(threshold=10000)"
- />
攔截器可以使用一個短小的名稱,比如 ConnectionState,如果這個攔截器定義在 org.apache.tomcat.jdbc.pool.interceptor 包中。
否則,必須使用一個完全限定名稱。
攔截器定義在以;分割的字符串中。攔截器可以在括號內定義0個或多個參數。參數是以逗號分割的簡單鍵值對。
連接狀態
java.sql.Connection接口有如下屬性
- autoCommit
- readOnly
- transactionIsolation
- catalog
這些屬性的默認值可以使用如下的內容為連接池配置
- defaultAutoCommit
- defaultReadOnly
- defaultTransactionIsolation
- defaultCatalog
如果設置了這些屬性,當建立連接到數據庫時配置這個連接。如果沒有配置 ConnectionState攔截器,在建立連接時設置這些屬性會是一次性操作。如果配置了ConnectionState攔截器,每次從連接池取出的連接會將被重置為期望的狀態。
其中有些方法在執行查詢時會導致往返數據庫。比如,調用 Connection.getTransactionIsolation()會導致驅動查詢當前會話的事務隔離級別。這種往返會導致嚴重的性能問題並影響應用在頻繁的使用連接執行非常短和快的操作的時候。 ConnectionState 攔截器可以緩存這些操作的值並調用方法查詢它們從而避免往返數據庫。
Statement Finalizer
java代碼在使用java.sql對象后需要清除和釋放使用過的資源。
一個清理代碼示例
- Connection con = null;
- Statement st = null;
- ResultSet rs = null;
- try {
- con = ds.getConnection();
- ...
- } finally {
- if (rs!=null) try { rs.close(); } catch (Exception ignore){}
- if (st!=null) try { st.close(); } catch (Exception ignore){}
- if (con!=null) try { con.close();} catch (Exception ignore){}
- }
當一個連接返回連接池的時候,StatementFinalizer攔截器確保 java.sql.Statement和它的子類正確關閉。
獲得真正的JDBC連接
使用javax.sql.PooledConnection工具返回代理連接,因此取出連接十分直接,不需要轉換為特殊的類。
同樣適用於你配置了處理javax.sql.XAConnection的連接池。
另一個有趣的取出底層連接的方法是
- Connection con = ds.getConnection();
- ction underlyingconnection = con.createStatement().getConnection();
PS:翻譯原文——http://www.tomcatexpert.com/blog/2010/04/01/configuring-jdbc-pool-high-concurrency
自己能力有限,翻譯中難免會有失誤,可能不能充分理解原作者的表達含義,自己在邊學習相關知識的時候也會邊修改