數據庫連接池簡介和 C3P0的JDBC配置

前面一節我們介紹瞭如何利用jdbc連接數據庫,已經實現了數據庫的連接,但是在實際的項目開發中,可以發現基本上都使用了數據庫連接池技術,爲什麼要使用數據庫連接池呢?根源在於對數據庫連接的低效管理
答: 普通的JDBC數據庫連接,用戶請求一次查詢的時候就會向數據庫發起一次連接,執行完後就斷開連接,這樣的方式會消耗大量的資源和時間,數據庫的連接資源並沒有得到很好的重複利用。若是同時有幾百人甚至幾千人在線,頻繁地進行數據庫連接操作,這將會佔用很多的系統資源,嚴重的甚至會造成服務器的奔潰。這樣頻繁的創建銷燬數據庫連接十分耗費時間和資源,而且開發者也不能很好的控制數據庫的連接數,有可能因爲分配的連接過多而導致內存耗盡。

數據庫連接池的實現原理解析,以及設計連接池時需要考慮的因素

  1. 併發問題
    首先必須考慮多線程的環境, 可以使用synchronized關鍵字,lock等即可確保線程是同步的,public synchronized Connection getConnection()
  2. 多數據庫服務器和多用戶
    設計一個符合單例模式的連接池管理類,在連接池管理類的唯一實例被創建時讀取一個資源文件,其中資源文件中存放着多個數據庫的url地址等信息。根據資源文件提供的信息,創建多個連接池類的實例,每一個實例都是一個特定數據庫的連接池。連接池管理類實例爲每個連接池實例取一個名字,通過不同的名字來管理不同的連接池。
    對於同一個數據庫有多個用戶使用不同的名稱和密碼訪問的情況,也可以通過資源文件處理,即在資源文件中設置多個具有相同url地址,但具有不同用戶名和密碼的數據庫連接信息。
  3. 事務處理
    Connection類本身提供了對事務的支持,可以通過設置connection的autocommit屬性爲false 然後顯式的調用commit或rollback方法來實現。
      try{
          connect.setAutoCommit(false);
          ........ 進行的數據庫操作語句
          connect.commit();
      }catch{
          connect.rollback();
      }finally{
         connect.close();
      }

但是當2個線程共用一個連接Connection對象,而且各自都有自己的事務要處理時候,對於連接池是一個很頭疼的問題,因爲即使Connection類提供了相應的事務支持,可是我們仍然不能確定那個數據庫操作是對應那個事務的,這是由於我們有2個線程都在進行事務操作而引起的。但要高效的進行Connection複用,就必須提供相應的事務支持機制。可採用每一個事務獨佔一個連接來實現,雖然這種方法有點浪費連接池資源,但是可以大大降低事務管理的複雜性。
4. 連接池的分配與釋放
合理的分配與釋放,可以提高連接的複用度,從而降低建立新連接的開銷,同時還可以加快用戶的訪問速度。
對於連接的管理可使用空閒池。即把已經創建但尚未分配出去的連接按創建時間存放到一個空閒池中。每當用戶請求一個連接時,系統首先檢查空閒池內有沒有空閒連接。如果有就把建立時間最長(通過容器的順序存放實現)的那個連接分配給他(實際是先做連接是否有效的判斷,如果可用就分配給用戶,如不可用就把這個連接從空閒池刪掉,重新檢測空閒池是否還有連接);如果沒有則檢查當前所開連接池是否達到連接池所允許的最大連接數(maxconn)如果沒有達到,就新建一個連接,如果已經達到,就等待一定的時間(timeout)。如果在等待的時間內有連接被釋放出來就可以把這個連接分配給等待的用戶,如果等待時間超過預定時間timeout 則返回空值(null)。系統對已經分配出去正在使用的連接只做計數,當使用完後再返還給空閒池,但是會發生無法對正在使用的連接進行管理的狀況,所以建議使用一個鏈表存儲。對於空閒連接的狀態,可開闢專門的線程定時檢測,這樣會花費一定的系統開銷,但可以保證較快的響應速度。也可採取不開闢專門線程,只是在分配前檢測的方法

再分配、釋放策略對於有效複用連接非常重要,引用記數模式在複用資源方面用的非常廣泛,每一個數據庫連接,保留一個引用記數,用來記錄該連接的使用者的個數,我們對Connection類進行進一步包裝來實現引用記數,確定當前被引用多少,具體是哪個用戶引用了該連接將在連接池中登記,一旦一個連接被分配出去,那麼就會對該連接的申請者進行登記,並且增加引用記數,當被釋放回來時候就刪除他已經登記的信息,同時減少一次引用記數

5、連接池的配置與維護
連接池中到底應該放置多少連接,才能使系統的性能最佳?系統可採取設置最小連接數(minconn)和最大連接數(maxconn)來控制連接池中的連接。最小連接數是系統啓動時連接池所創建的連接數。如果創建過多,則系統啓動就慢,但創建後系統的響應速度會很快;如果創建過少,則系統啓動的很快,響應起來卻慢。這樣,可以在開發時,設置較小的最小連接數,開發起來會快,而在系統實際使用時設置較大的,因爲這樣對訪問客戶來說速度會快些。最大連接數是連接池中允許連接的最大數目,具體設置多少,要看系統的訪問量,可通過反覆測試,找到最佳點。
如何確保連接池中的最小連接數呢?有動態和靜態兩種策略。動態即每隔一定時間就對連接池進行檢測,如果發現連接數量小於最小連接數,則補充相應數量的新連接以保證連接池的正常運轉。靜態是發現空閒連接不夠時再去檢查。

自己實現一個簡易的數據庫連接池,只考慮了連接池的一些方面

// 連接池類
我在設計的時候需要考慮的幾點
1. 首先是數據庫連接的存儲,要能很容易的管理和獲取數據庫連接,將數據庫連接分爲兩部分,一部分是空閒池,一部分是正在使用的數據庫連接使用LinkedList 實現棧來存儲空閒的數據庫連接,(優勢在於每一次獲取到的連接都是新的連接,這樣的連接基本上都是可用的,基本上不會發生連接不可用導致重新再去獲取連接的操作), 使用LinkedList 實現隊列來存儲正在使用中數據庫連接(優勢在於,隊列的頭部就是目前使用時間最長的連接,方便進行檢查,回收這個使用時間超過限制的數據庫連接)
2. 如何回收分配出去的連接,即當外部的連接調用了close方法之後,如何讓它返回到數據庫連接池中,而不是銷燬?
方法: 使用動態代理, 當請求一個Connection 時,返回用戶一個代理的Connection 對象,這樣就可以對close方法進行攔截,調用了close方法,會自動執行代理類中的invoke方法, 在invoke方法裏面就可以實現對實際連接的一些操作了, 具體實現請查看 getConnection() 方法。
3. 其實獲取連接的時候應當首先檢查空閒池是否有空閒連接再檢查空閒連接是否可用當數據庫連接池沒有連接的時候,要進行一次性創建新的連接,同時要進行檢查看是否能進行連接的創建,是否達到了最大值等, 所以數據庫的一些配置屬性需要在靜態代碼塊中通過Properties類讀取出來。

public class MyDatabasePool {

    private LinkedList<Connection> idlelist; // 使用LinkedList實現棧存儲數據庫連接,存放的空閒連接
    private LinkedList<Connection> usinglist; // 使用LinkedList實現隊列存儲數據庫連接,存放的正在使用的連接
    private static Properties props; // 讀取配置文件信息
    private static int initialPoolSize; // 初始連接池大小
    private static int maxPoolSize; // 連接池最大連接數
    private static int acquireIncrement; // 無連接時,一次性創建連接數
    static {
        props = new Properties();
        try {
            props.load(new FileInputStream("myPool.properties"));
            initialPoolSize = Integer.parseInt(props
                    .getProperty("initialPoolSize"));
            maxPoolSize = Integer.parseInt(props.getProperty("maxPoolSize"));
            acquireIncrement = Integer.parseInt(props
                    .getProperty("acquireIncrement"));
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    // 構造函數,在數據庫連接池裏先創建幾個連接
    // 我看了一下c3p0的源碼,裏面是用的代理連接 new ,而不是真實的物理連接
    public MyDatabasePool() throws ClassNotFoundException, SQLException {
        idlelist = new LinkedList<Connection>();
        usinglist = new LinkedList<Connection>();
        Class.forName(props.getProperty("MySQLdriverClass"));
        for (int i = 0; i < initialPoolSize; i++) {
            Connection conn = DriverManager.getConnection(
                    props.getProperty("MySQLurl"),
                    props.getProperty("MySQLusername"),
                    props.getProperty("MySQLpassword"));
            idlelist.addLast(conn);
        }
    }

    // 獲取數據庫連接
    public Connection getConnection() throws SQLException {
        if (idlelist.size() > 0) {
            usinglist.addFirst(idlelist.getLast()); // 只是獲取第一個連接並沒有刪除
            Connection conn = idlelist.removeLast(); // 獲取第一個連接並刪除
            // return conn; //返回真實的物理連接

            // 返回一個真實物理連接的動態代理連接對象
            return (Connection) Proxy.newProxyInstance(ProxyConnection.class
                    .getClassLoader(), new Class<?>[] { Connection.class },
                    new ProxyConnection(conn));
        } else {
            // 創建新的數據庫連接
            boolean flag = dynamicIncrement();
            if (flag) {
                usinglist.add(idlelist.getLast()); // 只是獲取第一個連接並沒有刪除
                Connection conn = idlelist.removeLast(); // 獲取第一個連接並刪除
                // return conn; //返回真實的物理連接

                // 返回一個真實物理連接的動態代理連接對象
                return (Connection) Proxy.newProxyInstance(
                        ProxyConnection.class.getClassLoader(),
                        new Class[] { Connection.class }, new ProxyConnection(
                                conn));
            } else {
                throw new SQLException("沒連接了");
            }
        }
    }

    // 連接池裏無連接,動態增長
    private boolean dynamicIncrement() throws SQLException {
        int num = idlelist.size() + usinglist.size();
        int num2 = maxPoolSize - num;
        // 如果可以創建連接,而且創建的連接數就是acquireIncrement
        if (num2 >= acquireIncrement) {
            for (int i = 0; i < acquireIncrement; i++) {
                Connection conn = DriverManager.getConnection(
                        props.getProperty("MySQLurl"),
                        props.getProperty("MySQLusername"),
                        props.getProperty("MySQLpassword"));
                idlelist.addLast(conn);
            }
            return true;
        }
        // 如果可以創建連接,但是創建的連接數只能是num2個
        if (num2 > 0) {
            for (int i = 0; i < num2; i++) {
                Connection conn = DriverManager.getConnection(
                        props.getProperty("MySQLurl"),
                        props.getProperty("MySQLusername"),
                        props.getProperty("MySQLpassword"));
                idlelist.addLast(conn);
            }
            return true;
        }
        return false;
    }

    // Connection的動態代理類
    class ProxyConnection implements InvocationHandler {
        private Connection conn;
        public ProxyConnection(Connection conn) {
            this.conn = conn;
        }
        // 關閉數據庫連接,放回到空閒池中
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            // TODO Auto-generated method stub
            // 分配出去的代理連接調用了close方法,進行攔截,實現我們自己想要的操作
            if (method.getName().equals("close")) {
                // conn.close(); // 這一句的話就直接關閉連接了,所以不寫
                // 應該事先的操作是將 conn 放到空閒池中去,從使用池中移除
                System.out.println(idlelist.size());
                System.out.println(usinglist.size());
                idlelist.addLast(conn);
                usinglist.remove(conn);
                System.out.println(idlelist.size());
                System.out.println(usinglist.size());
                return null;
            }
            // 其他方法仍然調用真實對象的方法
            return method.invoke(conn, args);
        }
    }
}

配置文件 myPool.properties

# mysql database driver
MySQLdriverClass=com.mysql.jdbc.Driver
MySQLurl=jdbc:mysql://127.0.0.1/test?useSSL=false
MySQLusername=root
MySQLpassword=root
initialPoolSize=3
minPoolSize=2
maxPoolSize=50
acquireIncrement=3

JDBC的API中沒有提供連接池的方法,所以可以使用如下常見的幾種數據庫連接池:
C3P0
C3P0是一個開源的JDBC連接池,支持JDBC3規範和JDBC2的標準擴展。c3p0是異步操作的,緩慢的JDBC操作通過幫助進程完成。擴展這些操作可以有效的提升性能。目前使用它的開源項目有Hibernate,Spring等c3p0有自動回收空閒連接功能,穩定性好,大併發量的壓力下穩定性也有一定的保證 無連接池監控
c3p0所需jar:
c3p0-0.9.2.1.jar
mchange-commons-java-0.2.3.4.jar

DBCP
是 apache 上的一個 java 連接池項目,也是 tomcat 使用的連接池組件,連接池的基本功能都有,一般不建議使用,無連接池監控
使用dbcp需要2個包:
commons-dbcp.jar
commons-pool.jar

Proxool
Proxool是一種Java數據庫連接池技術。Sourceforge下的一個開源項目,這個項目提供一個健壯、易用的連接池,最爲關鍵的是這個連接池提供監控數據庫連接的功能,方便易用,便於發現連接泄漏的情況。

Druid
Druid是阿里巴巴開源平臺上的一個項目,整個項目由數據庫連接池、插件框架和SQL解析器組成。該項目主要是爲了擴展JDBC的一些限制,可以讓程序員實現一些特殊的需求,比如向密鑰服務請求憑證、統計SQL信息、SQL性能收集、SQL注入檢查、SQL翻譯等,程序員可以通過定製來實現自己需要的功能。

利用C3PO進行配置實現數據庫連接池的使用
第一步: 導入jar包 c3p0-0.9.2.1.jar 和 mchange-commons-java-0.2.3.4.jar
第二步: 書寫配置文件 c3p0-config.xml; 裏面有一個參數是需要注意的,可以詳細看看下面的配置文件
注意的是:
1. 文件名必須爲c3p0-config.xml, 這是因爲C3P0會默認讀取文件名爲c3p0-config.xml的配置文件進而對數據庫連接池進行配置。
2. c3p0-config.xml 必須和你寫的java代碼在同一個目錄下,一般就是放在項目的 src目錄下

<?xml version="1.0" encoding="utf-8"?>  
<c3p0-config>  
 <!-- c3p0也可以指定配置文件,而且配置文件可以是properties,也可騍xml的。
 當然xml的高級一些了。但是c3p0的配置文件名必須爲c3p0-config.xml,
 並且必須放在類路徑下 -->

 <!--  默認的配置這裏我們默認使用mysql數據庫 -->  
  <default-config>
    <!--  設置數據庫的驅動,url, 用戶名, 密碼 -->  
    <property name="driverClass">com.mysql.jdbc.Driver</property>  
    <property name="jdbcUrl">jdbc:mysql://127.0.0.1/test?useSSL=false</property>  
    <property name="user">root</property>  
    <property name="password">root</property>
     <!--  建立連接池時初始分配的連接池數 = 3-->  
    <property name="initialPoolSize">3</property>

     <!--  連接池中的最少連接數 = 2 -->  
    <property name="minPoolSize">2</property>

     <!--  連接池中的最大連接數 = 50-->  
    <property name="maxPoolSize">50</property>

    <!--  當連接池中連接耗盡時再一次新生成多少個連接 Default: 3 -->  
    <property name="acquireIncrement">3</property>

     <!--  最大空閒時間,超過多長時間連接自動銷燬,秒爲單位,默認爲0,即永遠不會自動銷燬  -->
    <property name="maxIdleTime">1800</property> 

    <!--每60秒檢查所有連接池中的空閒連接。Default: 0 --> 
 <property name="idleConnectionTestPeriod">60</property>

    <!--  c3p0還可以爲某個用戶設置單獨的連接數-->
    <user-overrides user="test-user">  
      <property name="maxPoolSize">10</property>  
      <property name="minPoolSize">1</property>  
      <property name="maxStatements">0</property>  
    </user-overrides>     
  </default-config>

    <!-- c3p0的配置文件中可以配置多個數據庫連接信息,可以給每個配置起個名字,這樣可以方便的通過配置名稱來切換配置信息 -->
  <!-- 名字爲Oracle-config的配置 -->  
  <named-config name="Oracle-config">   
    <property name="driverClass">oracle.jdbc.driver.OracleDriver</property>  
    <property name="jdbcUrl">jdbc:oracle:thin:@localhost:1521:test</property>  
    <property name="user">scott</property>  
    <property name="password">tiger</property>  
  </named-config>

</c3p0-config>

第三步: 可以自己創建一個工具類,從數據庫的連接池中獲取連接,我這裏只是寫了一個建議的連接池工具類,當然你可以根據自己的需要進行擴展

import java.sql.Connection;
import java.sql.SQLException;

import com.mchange.v2.c3p0.ComboPooledDataSource;

// 數據庫連接池的工具類
public final class PoolUtil {
    private static ComboPooledDataSource ds = null;
    static {
        // 兩種創建數據庫連接池的辦法
        // 第一種: 使用配置文件中的默認配置<default-config>
        ds = new ComboPooledDataSource();

        // 第二種: 使用配置文件中設置的其他配置名字的配置 name-config
        // ds = new ComboPooledDataSource("Oracle-config");

        // 第三種: 我們可以顯式的在程序中進行設置數據庫連接池的信息
        // ds.setDriverClass("com.mysql.jdbc.Driver");
        // ds.setJdbcUrl("jdbc:mysql://127.0.0.1/test?useSSL=false");
        // ds.setUser("root");
        // ds.setPassword("root");
    }
    // 獲取數據庫的連接
    public static Connection getConnection() throws SQLException {
        return ds.getConnection();
    }
}

第四步 :進行數據庫連接池的測試,觀測是否成功使用了連接池
在這裏需要注意的一點,當你在程序裏使用完數據庫連接之後,必須顯示的調用close()方法, 此時的close語句並不會關閉與數據庫的TCP連接,而是將連接歸還回到連接池中去,變爲空閒狀態, 如果不close掉的話,這個連接將會一直被佔用

public class PoolTest {
    public static void main(String[] args) throws SQLException {
        Connection conn = null;
        try {
            conn = PoolUtil.getConnection();
        } catch (SQLException e) {
            System.out.println("未獲取數據庫連接");
            e.printStackTrace();
        }
        String sql = "select * from user where id = ?";
        PreparedStatement prep = null;
        prep = (PreparedStatement) conn.prepareStatement(sql);
        prep.setInt(1, 1);
        // 查詢sql 語句, 返回一個結果集
        ResultSet result = prep.executeQuery();
        // 處理結果集, 釋放資源
        while (result.next()) {
            System.out.println(result.getInt("id"));
            System.out.println(result.getString("name"));
            System.out.println(result.getInt("age"));
            System.out.println(result.getString("salary"));
        }
        // 注意的是,即使使用了數據庫連接池之後,這裏也必須顯式的調用close語句,
        // 此時的close語句並不會關閉與數據庫的TCP連接,而是將連接歸還回到池中去,變爲空閒狀態
        // 如果不close掉的話,這個連接將會一直被佔用
        result.close();
        prep.close();
        conn.close();
    }
}

C3P0的源代碼的一些關鍵解析:
幾個關鍵的類:
C3P0PooledConnectionPoolManager是連接池的管理類,
C3P0PooledConnectionPool是連接池類,
BasicResourcePool是真正管理數據庫連接池的類

獲取一個連接的代碼:

public Connection getConnection() throws SQLException
    {
        PooledConnection pc = getPoolManager().getPool().checkoutPooledConnection();
        return pc.getConnection();
    }

public Object checkoutResource(long timeout)
1) 關鍵步驟代碼:Object resc = prelimCheckoutResource(timeout);
查看池中是否有未使用的connection,有就返回(還要判斷是否空閒、是否過期);沒有,如果沒有達到最大數,就生成一個,或者就等待
2) 關鍵步驟代碼:
boolean refurb = attemptRefurbishResourceOnCheckout (resc);
得到連接後,檢測連接的可用性。
3) 連接可用,接着判斷連接是否處於管理中,不在就再調用本方法獲取一個,在就返回本連接

C3P0**從連接池拿到的連接都是代理的連接,一個對PooledConnection類型對象的代理對象,所以可以放心調用close方法,只是連接進行了歸還,不會關閉物理連接 而且C3p0中實際創建的對象是實現了PooledConnection(接口,位於javax.sql)它本身包含Connection,和這個connection相關的所有Statement,Result,可以實現對連接的一些管理,添加監聽器等, 所做的所有數據庫操作,都被PooledConnection所管理。**c3p0默認的實現是NewPooledConnection

C3P0使用了LinkedList來存放空閒池中的連接,每次取連接的時候是get(0), 然後remove(0) 應該是使用的隊列來實現的空閒池
C3P0 使用的HashMap 來存放的正在使用的連接,這樣方便進行查找,連接返回的時候,根據連接可以快速的定位到要remove的那個連接
C3P0 使用HashSet來存儲一些失效的,但是仍舊被使用或者檢查的資源。
這些數據結構可以在 BasicResourcePool 類中查看到

本博客參考了
http://blog.csdn.net/shuaihj/article/details/14223015

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章