1. 程式人生 > >資料庫連線池原理詳解與自定義連線池實現

資料庫連線池原理詳解與自定義連線池實現

實現原理

資料庫連線池在初始化時將建立一定數量的資料庫連線放到連線池中,這些資料庫連線的數量是由最小資料庫連線數制約。無論這些資料庫連線是否被使用,連線池都將一直保證至少擁有這麼多的連線數量。連線池的最大資料庫連線數量限定了這個連線池能佔有的最大連線數,當應用程式向連線池請求的連線數超過最大連線數量時,這些請求將被加入到等待佇列中。
連線池基本的思想是在系統初始化的時候,將資料庫連線作為物件儲存在記憶體中,當用戶需要訪問資料庫時,並非建立一個新的連線,而是從連線池中取出一個已建立的空閒連線物件。使用完畢後,使用者也並非將連線關閉,而是將連線放回連線池中,以供下一個請求訪問使用。而連線的建立、斷開都由連線池自身來管理。同時,還可以通過設定連線池的引數來控制連線池中的初始連線數、連線的上下限數以及每個連線的最大使用次數、最大空閒時間等等。也可以通過其自身的管理機制來監視資料庫連線的數量、使用情況等。

注意事項

1、資料庫連線池的最小連線數是連線池一直保持的資料庫連線,所以如果應用程式對資料庫連線的使用量不大,將會有大量的資料庫連線資源被浪費。
2、資料庫連線池的最大連線數是連線池能申請的最大連線數,如果資料庫連線請求超過此數,後面的資料庫連線請求將被加入到等待佇列中,這會影響之後的資料庫操作。
3、最大連線數具體值要看系統的訪問量.要經過不斷測試取一個平衡值
4、隔一段時間對連線池進行檢測,發現小於最小連線數的則補充相應數量的新連線
5、最小連線數與最大連線數差距,最小連線數與最大連線數相差太大,那麼最先的連線請求將會獲利,之後超過最小連線數量的連線請求等價於建立一個新的資料庫連線。不過,這些大於最小連線數的資料庫連線在使用完不會馬上被釋放,它將被放到連線池中等待重複使用或是空閒超時後被釋放。

資料庫連線池配置屬性

目前資料庫連線池種類繁多,不同種類基本的配置屬性大同小異,例如c3p0、Proxool、DDConnectionBroker、DBPool、XAPool、Druid、dbcp,這裡我們以dbcp為例說說主要的配置項:

#最大連線數量:連線池在同一時間能夠分配的最大活動連線的數量,,如果設定為非正數則表示不限制,預設值8
maxActive=15
#最小空閒連線:連線池中容許保持空閒狀態的最小連線數量,低於這個數量將建立新的連線,如果設定為0則不建立,預設值0
minIdle=5
#最大空閒連線:連線池中容許保持空閒狀態的最大連線數量,超過的空閒連線將被釋放,如果設定為負數表示不限制,預設值8
maxIdle=10 #初始化連線數:連線池啟動時建立的初始化連線數量,預設值0 initialSize=5 #連線被洩露時是否列印 logAbandoned=true #是否自動回收超時連線 removeAbandoned=true #超時時間(以秒數為單位) removeAbandonedTimeout=180 # 最大等待時間:當沒有可用連線時,連線池等待連線被歸還的最大時間(以毫秒計數),超過時間則丟擲異常,如果設定為-1表示無限等待,預設值無限 maxWait=3000 #在空閒連接回收器執行緒執行期間休眠的時間值(以毫秒為單位). timeBetweenEvictionRunsMillis=10000 #在每次空閒連接回收器執行緒(如果有)執行時檢查的連線數量 numTestsPerEvictionRun=8 #連線在池中保持空閒而不被空閒連接回收器執行緒 minEvictableIdleTimeMillis=10000 #用來驗證從連線池取出的連線 validationQuery=SELECT 1 #指明是否在從池中取出連線前進行檢驗 testOnBorrow=true #testOnReturn false 指明是否在歸還到池中前進行檢驗 testOnReturn=true #設定為true後如果要生效,validationQuery引數必須設定為非空字串 testWhileIdle
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

自定義資料庫連線池示例

首先看一下連線池的定義。它通過建構函式初始化連線的最大上限,通過一個雙向佇列來維護連線,呼叫方需要先呼叫fetchConnection(long)方法來指定在多少毫秒內超時獲取連線,當連線使用完成後,需要呼叫releaseConnection(Connection)方法將連線放回執行緒池

public class ConnectionPool {
    private LinkedList<Connection> pool = new LinkedList<Connection>();

    /**
     * 初始化連線池的大小
     * @param initialSize
     */
    public ConnectionPool(int initialSize) {
        if (initialSize > 0) {
            for (int i = 0; i < initialSize; i++) {
                pool.addLast(ConnectionDriver.createConnection());
            }
        }
    }

    /**
     * 釋放連線,放回到連線池
     * @param connection
     */
    public void releaseConnection(Connection connection){
        if(connection != null){
            synchronized (pool) {
                // 連線釋放後需要進行通知,這樣其他消費者能夠感知到連線池中已經歸還了一個連線
                pool.addLast(connection);
                pool.notifyAll();
            }
        }
    }

    /**
     * 在mills內無法獲取到連線,將會返回null
     * @param mills
     * @return
     * @throws InterruptedException
     */
    public Connection fetchConnection(long mills) throws InterruptedException{
        synchronized (pool) {
            // 無限制等待
            if (mills <= 0) {
                while (pool.isEmpty()) {
                    pool.wait();
                }
                return pool.removeFirst();
            }else{
                long future = System.currentTimeMillis() + mills;
                long remaining = mills;
                while (pool.isEmpty() && remaining > 0) {
                    // 等待超時
                    pool.wait(remaining);
                    remaining = future - System.currentTimeMillis();
                }
                Connection result = null;
                if (!pool.isEmpty()) {
                    result = pool.removeFirst();
                }
                return result;
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

由於java.sql.Connection是一個介面,最終的實現是由資料庫驅動提供方來實現的,考慮到只是個示例,我們通過動態代理構造了一個Connection,該Connection的代理實現僅僅是在commit()方法呼叫時休眠100毫秒

public class ConnectionDriver {
    static class ConnectionHandler implements InvocationHandler{
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if(method.equals("commit")){
                TimeUnit.MILLISECONDS.sleep(100);
            }
            return null;
        }
    }

    /**
     * 建立一個Connection的代理,在commit時休眠100毫秒
     * @return
     */
    public static final Connection createConnection(){
        return (Connection) Proxy.newProxyInstance(ConnectionDriver.class.getClassLoader(),
                new Class[] { Connection.class },new ConnectionHandler());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

下面通過一個示例來測試簡易資料庫連線池的工作情況,模擬客戶端ConnectionRunner獲取、使用、最後釋放連線的過程,當它使用時連線將會增加獲取到連線的數量,反之,將會增加未獲取到連線的數量

public class ConnectionPoolTest {
    static ConnectionPool pool = new ConnectionPool(10);
    // 保證所有ConnectionRunner能夠同時開始
    static CountDownLatch start = new CountDownLatch(1);
    // main執行緒將會等待所有ConnectionRunner結束後才能繼續執行
    static CountDownLatch end;
    public static void main(String[] args) {
        // 執行緒數量,可以修改執行緒數量進行觀察
        int threadCount = 10;
        end = new CountDownLatch(threadCount);
        int count = 20;
        AtomicInteger got = new AtomicInteger();
        AtomicInteger notGot = new AtomicInteger();
        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(new ConnetionRunner(count, got, notGot), "ConnectionRunnerThread");
            thread.start();
        }
        start.countDown();
        try {
            end.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("total invoke: " + (threadCount * count));
        System.out.println("got connection: " + got);
        System.out.println("not got connection " + notGot);
    }

    static class ConnetionRunner implements Runnable {

        int count;
        AtomicInteger got;
        AtomicInteger notGot;
        public ConnetionRunner(int count, AtomicInteger got, AtomicInteger notGot) {
            this.count = count;
            this.got = got;
            this.notGot = notGot;
        }
        @Override
        public void run() {
            try {
                start.await();
            } catch (Exception ex) {
            }
            while (count > 0) {
                try {
                    // 從執行緒池中獲取連線,如果1000ms內無法獲取到,將會返回null
                    // 分別統計連接獲取的數量got和未獲取到的數量notGot
                    Connection connection = pool.fetchConnection(1);
                    if (connection != null) {
                        try {
                            connection.createStatement();
                            connection.commit();
                        } finally {
                            pool.releaseConnection(connection);
                            got.incrementAndGet();
                        }
                    } else {
                        notGot.incrementAndGet();
                    }
                } catch (Exception ex) {
                } finally {
                    count--;
                }
            }
            end.countDown();
        }

    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

CountDownLatch類是一個同步計數器,構造時傳入int引數,該引數就是計數器的初始值,每呼叫一次countDown()方法,計數器減1,計數器大於0 時,await()方法會阻塞程式繼續執行CountDownLatch如其所寫,是一個倒計數的鎖存器,當計數減至0時觸發特定的事件。利用這種特性,可以讓主執行緒等待子執行緒的結束。這這裡保證讓所有的ConnetionRunner
都執行完再執行main進行列印。

執行結果:
20個客戶端

total invoke: 200
got connection: 200
not got connection 0
  • 1
  • 2
  • 3

50個客戶端

total invoke: 1000
got connection: 999
not got connection 1
  • 1
  • 2
  • 3

100個客戶端

total invoke: 2000
got connection: 1842
not got connection 158
  • 1
  • 2
  • 3

在資源一定的情況下(連線池中的10個連線),隨著客戶端執行緒的逐步增加,客戶端出現超時無法獲取連線的比率不斷升高。雖然客戶端執行緒在這種超時獲取的模式下會出現連線無法獲取的情況,但是它能夠保證客戶端執行緒不會一直掛在連接獲取的操作上,而是“按時”返回,並告知客戶端連接獲取出現問題,是系統的一種自我保護機制。資料庫連線池的設計也可以複用到其他的資源獲取的場景,針對昂貴資源(比如資料庫連線)的獲取都應該加以超時限制。