|
劉冬 (winter.lau@163.com) 珠海市創我科技發展有限公司軟體工程師 2002 年 12 月 資料庫連接池在編寫應用服務是經常需要用到的模組,太過頻繁的串連資料庫對服務效能來講是一個瓶頸,使用緩衝池技術可以來消除這個瓶頸。我們可以在互連網上找到很多關於資料庫連接池的來源程式,但是都發現這樣一個共同的問題:這些串連池的實現方法都不同程度地增加了與使用者之間的耦合度。很多的串連池都要求使用者通過其規定的方法擷取資料庫的串連,這一點我們可以理解,畢竟目前所有的應用伺服器取資料庫連接的方式都是這種方式實現的。但是另外一個共同的問題是,它們同時不允許使用者顯式的調用Connection.close()方法,而需要用其規定的一個方法來關閉串連。這種做法有兩個缺點: 第一:改變了使用者使用習慣,增加了使用者的使用難度。 首先我們來看看一個正常的資料庫操作過程:
int executeSQL(String sql) throws SQLException{Connection conn = getConnection();//通過某種方式擷取資料庫連接PreparedStatement ps = null;int res = 0;try{ps = conn.prepareStatement(sql);res = ps.executeUpdate();}finally{try{ps.close();}catch(Exception e){}try{conn.close();//}catch(Exception e){}}return res;}
|
使用者在用完資料庫連接後通常是直接調用串連的方法close來釋放資料庫資源,如果用我們前面提到的串連池的實現方法,那語句conn.close()將被某些特定的語句所替代。 第二:使串連池無法對之中的所有串連進行獨佔控制。由於串連池不允許使用者直接調用串連的close方法,一旦使用者在使用的過程中由於習慣問題直接關閉了資料庫連接,那麼串連池將無法正常維護所有串連的狀態,考慮串連池和應用由不同開發人員實現時這種問題更容易出現。 綜合上面提到的兩個問題,我們來討論一下如何解決這兩個要命的問題。 首先我們先設身處地的考慮一下使用者是想怎麼樣來使用這個資料庫連接池的。使用者可以通過特定的方法來擷取資料庫的串連,同時這個串連的類型應該是標準的java.sql.Connection。使用者在擷取到這個資料庫連接後可以對這個串連進行任意的操作,包括關閉串連等。 通過對使用者使用的描述,怎樣可以接管Connection.close方法就成了我們這篇文章的主題。 為了接管資料庫連接的close方法,我們應該有一種類似於鉤子的機制。例如在Windows編程中我們可以利用Hook API來實現對某個Windows API的接管。在JAVA中同樣也有這樣一個機制。JAVA提供了一個Proxy類和一個InvocationHandler,這兩個類都在java.lang.reflect包中。我們先來看看SUN公司提供的文檔是怎麼描述這兩個類的。
public interface InvocationHandlerInvocationHandler is the interface implemented by the invocation handler of a proxy instance. Each proxy instance has an associated invocation handler. When a method is invoked on a proxy instance, the method invocation is encoded and dispatched to the invoke method of its invocation handler.
|
SUN的API文檔中關於Proxy的描述很多,這裡就不羅列出來。通過文檔對介面InvocationHandler的描述我們可以看到當調用一個Proxy執行個體的方法時會觸發Invocationhanlder的invoke方法。從JAVA的文檔中我們也同時瞭解到這種動態代理機制只能接管介面的方法,而對一般的類無效,考慮到java.sql.Connection本身也是一個介面由此就找到瞭解決如何接管close方法的出路。 首先,我們先定義一個資料庫連接池參數的類,定義了資料庫的JDBC驅動程式類名,串連的URL以及使用者名稱口令等等一些資訊,該類是用於初始化串連池的參數,具體定義如下:
public class ConnectionParam implements Serializable{private String driver;//資料庫驅動程式private String url;//資料連線的URLprivate String user;//資料庫使用者名稱private String password;//資料庫密碼private int minConnection = 0;//初始化串連數private int maxConnection = 50;//最大串連數private long timeoutValue = 600000;//串連的最大空閑時間private long waitTime = 30000;//取串連的時候如果沒有可用串連最大的等待時間
|
其次是串連池的工廠類ConnectionFactory,通過該類來將一個串連池對象與一個名稱對應起來,使用者通過該名稱就可以擷取指定的串連池對象,具體代碼如下:
/** * 串連池類廠,該類常用來儲存多個資料來源名稱合資料庫連接池對應的雜湊 * @author liusoft */public class ConnectionFactory{//該雜湊表用來儲存資料來源名和串連池對象的關係表static Hashtable connectionPools = null;static{connectionPools = new Hashtable(2,0.75F);} /** * 從串連池工廠中擷取指定名稱對應的串連池對象 * @param dataSource串連池對象對應的名稱 * @return DataSource返回名稱對應的串連池對象 * @throws NameNotFoundException無法找到指定的串連池 */public static DataSource lookup(String dataSource) throws NameNotFoundException{Object ds = null;ds = connectionPools.get(dataSource);if(ds == null || !(ds instanceof DataSource))throw new NameNotFoundException(dataSource);return (DataSource)ds;}/** * 將指定的名字和資料庫連接配置綁定在一起並初始化資料庫連接池 * @param name對應串連池的名稱 * @param param串連池的配置參數,具體請見類ConnectionParam * @return DataSource如果綁定成功後返回串連池對象 * @throws NameAlreadyBoundException一定名字name已經綁定則拋出該異常 * @throws ClassNotFoundException無法找到串連池的配置中的驅動程式類 * @throws IllegalAccessException串連池配置中的驅動程式類有誤 * @throws InstantiationException無法執行個體化驅動程式類 * @throws SQLException無法正常串連指定的資料庫 */public static DataSource bind(String name, ConnectionParam param)throws NameAlreadyBoundException,ClassNotFoundException,IllegalAccessException,InstantiationException,SQLException{DataSourceImpl source = null;try{lookup(name);throw new NameAlreadyBoundException(name);}catch(NameNotFoundException e){source = new DataSourceImpl(param);source.initConnection();connectionPools.put(name, source);}return source;}/** * 重新綁定資料庫連接池 * @param name對應串連池的名稱 * @param param串連池的配置參數,具體請見類ConnectionParam * @return DataSource如果綁定成功後返回串連池對象 * @throws NameAlreadyBoundException一定名字name已經綁定則拋出該異常 * @throws ClassNotFoundException無法找到串連池的配置中的驅動程式類 * @throws IllegalAccessException串連池配置中的驅動程式類有誤 * @throws InstantiationException無法執行個體化驅動程式類 * @throws SQLException無法正常串連指定的資料庫 */public static DataSource rebind(String name, ConnectionParam param)throws NameAlreadyBoundException,ClassNotFoundException,IllegalAccessException,InstantiationException,SQLException{try{unbind(name);}catch(Exception e){}return bind(name, param);}/** * 刪除一個資料庫連接池對象 * @param name * @throws NameNotFoundException */public static void unbind(String name) throws NameNotFoundException{DataSource dataSource = lookup(name);if(dataSource instanceof DataSourceImpl){DataSourceImpl dsi = (DataSourceImpl)dataSource;try{dsi.stop();dsi.close();}catch(Exception e){}finally{dsi = null;}}connectionPools.remove(name);}}
|
ConnectionFactory主要提供了使用者將將串連池綁定到一個具體的名稱上以及取消綁定的操作。使用者只需要關心這兩個類即可使用資料庫連接池的功能。下面我們給出一段如何使用串連池的代碼:
String name = "pool";String driver = " sun.jdbc.odbc.JdbcOdbcDriver ";String url = "jdbc:odbc:datasource";ConnectionParam param = new ConnectionParam(driver,url,null,null);param.setMinConnection(1);param.setMaxConnection(5);param.setTimeoutValue(20000);ConnectionFactory.bind(name, param);System.out.println("bind datasource ok.");//以上代碼是用來登記一個串連池對象,該操作可以在程式初始化只做一次即可//以下開始就是使用者真正需要寫的代碼DataSource ds = ConnectionFactory.lookup(name);try{for(int i=0;i<10;i++){Connection conn = ds.getConnection();try{testSQL(conn, sql);}finally{try{conn.close();}catch(Exception e){}}}}catch(Exception e){e.printStackTrace();}finally{ConnectionFactory.unbind(name);System.out.println("unbind datasource ok.");System.exit(0);}
|
從使用者的範例程式碼就可以看出,我們已經解決了常規串連池產生的兩個問題。但是我們最最關心的是如何解決接管close方法的辦法。接管工作主要在ConnectionFactory中的兩句代碼:
source = new DataSourceImpl(param);source.initConnection();
|
DataSourceImpl是一個實現了介面javax.sql.DataSource的類,該類維護著一個串連池的對象。由於該類是一個受保護的類,因此它暴露給使用者的方法只有介面DataSource中定義的方法,其他的所有方法對使用者來說都是不可視的。我們先來關心使用者可訪問的一個方法getConnection
/** * @see javax.sql.DataSource#getConnection(String,String) */public Connection getConnection(String user, String password) throws SQLException {//首先從串連池中找出閒置對象Connection conn = getFreeConnection(0);if(conn == null){//判斷是否超過最大串連數,如果超過最大串連數//則等待一定時間查看是否有空閑串連,否則拋出異常告訴使用者無可用串連if(getConnectionCount() >= connParam.getMaxConnection())conn = getFreeConnection(connParam.getWaitTime());else{//沒有超過串連數,重新擷取一個資料庫的串連connParam.setUser(user);connParam.setPassword(password);Connection conn2 = DriverManager.getConnection(connParam.getUrl(), user, password);//代理將要返回的連線物件_Connection _conn = new _Connection(conn2,true);synchronized(conns){conns.add(_conn);}conn = _conn.getConnection();}}return conn;}/** * 從串連池中取一個閒置串連 * @param nTimeout如果該參數值為0則沒有串連時只是返回一個null * 否則的話等待nTimeout毫秒看是否還有空閑串連,如果沒有拋出異常 * @return Connection * @throws SQLException */protected synchronized Connection getFreeConnection(long nTimeout) throws SQLException{Connection conn = null;Iterator iter = conns.iterator();while(iter.hasNext()){_Connection _conn = (_Connection)iter.next();if(!_conn.isInUse()){conn = _conn.getConnection();_conn.setInUse(true);break;}}if(conn == null && nTimeout > 0){//等待nTimeout毫秒以便看是否有空閑串連try{Thread.sleep(nTimeout);}catch(Exception e){}conn = getFreeConnection(0);if(conn == null)throw new SQLException("沒有可用的資料庫連接");}return conn;}
|
DataSourceImpl類中實現getConnection方法的跟正常的資料庫連接池的邏輯是一致的,首先判斷是否有閒置串連,如果沒有的話判斷串連數是否已經超過最大串連數等等的一些邏輯。但是有一點不同的是通過DriverManager得到的資料庫連接並不是及時返回的,而是通過一個叫_Connection的類中介一下,然後調用_Connection.getConnection返回的。如果我們沒有通過一個中介也就是JAVA中的Proxy來接管要返回的介面對象,那麼我們就沒有辦法截住Connection.close方法。 終於到了核心所在,我們先來看看_Connection是如何?的,然後再介紹是用戶端調用Connection.close方法時走的是怎樣一個流程,為什麼並沒有真正的關閉串連。
/** * 資料連線的自封裝,屏蔽了close方法 * @author Liudong */class _Connection implements InvocationHandler{private final static String CLOSE_METHOD_NAME = "close";private Connection conn = null;//資料庫的忙狀態private boolean inUse = false;//使用者最後一次訪問該串連方法的時間private long lastAccessTime = System.currentTimeMillis();_Connection(Connection conn, boolean inUse){this.conn = conn;this.inUse = inUse;}/** * Returns the conn. * @return Connection */public Connection getConnection() {//返回資料庫連接conn的接管類,以便截住close方法Connection conn2 = (Connection)Proxy.newProxyInstance(conn.getClass().getClassLoader(),conn.getClass().getInterfaces(),this);return conn2;}/** * 該方法真正的關閉了資料庫的串連 * @throws SQLException */void close() throws SQLException{//由於類屬性conn是沒有被接管的串連,因此一旦調用close方法後就直接關閉串連conn.close();}/** * Returns the inUse. * @return boolean */public boolean isInUse() {return inUse;}/** * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object) */public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {Object obj = null;//判斷是否調用了close的方法,如果調用close方法則把串連置為無用狀態if(CLOSE_METHOD_NAME.equals(m.getName()))setInUse(false);elseobj = m.invoke(conn, args);//設定最後一次訪問時間,以便及時清除逾時的串連lastAccessTime = System.currentTimeMillis();return obj;}/** * Returns the lastAccessTime. * @return long */public long getLastAccessTime() {return lastAccessTime;}/** * Sets the inUse. * @param inUse The inUse to set */public void setInUse(boolean inUse) {this.inUse = inUse;}}
|
一旦使用者調用所得到串連的close方法,由於使用者的連線物件是經過接管後的對象,因此JAVA虛擬機器會首先調用_Connection.invoke方法,在該方法中首先判斷是否為close方法,如果不是則將代碼轉給真正的沒有被接管的連線物件conn。否則的話只是簡單的將該串連的狀態設定為可用。到此您可能就明白了整個接管的過程,但是同時也有一個疑問:這樣的話是不是這些已建立的串連就始終沒有辦法真正關閉?答案是可以的。我們來看看ConnectionFactory.unbind方法,該方法首先找到名字對應的串連池對象,然後關閉該串連池中的所有串連並刪除掉串連池。在DataSourceImpl類中定義了一個close方法用來關閉所有的串連,詳細代碼如下:
/** * 關閉該串連池中的所有資料庫連接 * @return int 返回被關閉串連的個數 * @throws SQLException */public int close() throws SQLException{int cc = 0;SQLException excp = null;Iterator iter = conns.iterator();while(iter.hasNext()){try{((_Connection)iter.next()).close();cc ++;}catch(Exception e){if(e instanceof SQLException)excp = (SQLException)e;}}if(excp != null)throw excp;return cc;}
|
該方法一一調用串連池中每個對象的close方法,這個close方法對應的是_Connection中對close的實現,在_Connection定義中關閉資料庫連接的時候是直接調用沒有經過接管的對象的關閉方法,因此該close方法真正的釋放了資料庫資源。 以上文字只是描述了介面方法的接管,具體一個實用的串連池模組還需要對空閑串連的監控並及時釋放串連,詳細的代碼請參照附件。 參考資料: http://java.sun.com JAVA的官方網站 關於作者: 劉冬,珠海市創我科技發展有限公司軟體工程師,主要從事J2EE方面的開發。電子郵件:winter.lau@163.com |