本篇講訴數據庫連接池的概念和如何使用JDBC來創建自定義的數據庫連接池。
我們在操作數據庫的時候首先最重要的就是獲取數據庫的連接,只有獲取了連接才能有后面對數據庫的一系列操作。但是獲取連接的過程是非常消耗數據庫資源的,並且也非常耗時,這一點看看TCP三次握手取得連接也可以想象的到。多次獲取連接比長連接還要耗費資源,因此在會大量操作數據庫的情況下,減少數據庫創建連接的次數是能極大地優化數據庫的性能。
如果不用連接池,則數據庫為每一個用戶的來訪創建一個鏈接,雖然數據庫本身在安裝的時候會指定最大連接數,但是我們說過資源的耗費和耗時其實是在這些有限連接的不斷創建和銷毀的循環中。如下圖所示:
數據庫連接池(DataSource)
數據庫連接池,也稱為數據源。是在應用開始前就將數據庫的連接創建多個而不銷毀,同時管理起來,猶如放在一個池子里,那么只要用戶來訪問數據庫,則不在是直接通過數據庫獲取連接,而是從連接池中獲取連接,再通過連接操縱數據庫,最后再將連接返回給連接池,以便別的訪問再從池子中獲取連接進行操作。
數據庫連接池在初始化時將創建一定數量的數據庫連接放到連接池中,這些數據庫連接的數量是由最小數據庫連接數來設定的。無論這些數據庫連接是否被使用,連接池都將一直保證至少擁有這么多的連接數量。連接池的最大數據庫連接數量限定了這個連接池能占有的最大連接數,當應用程序向連接池請求的連接數超過最大連接數量時,這些請求將被加入到等待隊列中。
數據庫連接池的最小連接數和最大連接數的設置要考慮到以下幾個因素:
① 最小連接數:是連接池一直保持的數據庫連接,所以如果應用程序對數據庫連接的使用量不大,將會有大量的數據庫連接資源被浪費。
② 最大連接數:是連接池能申請的最大連接數,如果數據庫連接請求超過次數,后面的數據庫連接請求將被加入到等待隊列中,這會影響以后的數據庫操作。
③ 如果最小連接數與最大連接數相差很大:那么最先連接請求將會獲利,之后超過最小連接數量的連接請求等價於建立一個新的數據庫連接。不過,這些大於最小連接數的數據庫連接在使用完不會馬上被釋放,他將被放到連接池中等待重復使用或是空間超時后被釋放。
使用JDBC編寫自定義的數據庫連接池(DataSource)
要編寫自定義的數據庫連接池,必須要實現DataSource接口。
之前我們創建連接Connection對象都是通過DriveManager驅動管理器來獲取的,但這是直接使數據庫為我們創建連接,而上圖DataSource接口的API也說明了,我們要想獲取連接的首選方法應該是通過DataSource接口來獲取連接池中的連接。
DataSource接口自身主要就兩個方法,但是DataSource還繼承了別的接口,因此如果要實現DataSource接口的話,還必須將其父類的方法均實現:
因此我們在編寫自己的數據庫連接池類時,需要添加覆寫多個未實現的方法,雖然我們也不會在這些方法中編寫任何內容。
我們要想實現自己定義的一個連接池,需要實現以下幾個步驟:
1定義一個類,實現DataSource接口。
2在這個類的初始化方法(例如構造函數或者靜態代碼塊)中,通過DriveManager驅動管理器構建多個連接,這個還是跟以前獲取Connection對象一樣的用法。這一步是因為我們要先從數據庫獲取連接,才能將這些連接保存,即將多次創建連接變為一次創建而使用長連接。
3將上面從數據庫直接創建的連接存到LinkedList集合中,而這個集合就相當於一個池子,使用鏈表結構的集合有利於增刪操作。
4 這個自定義的類當然要實現DataSource接口中的方法,其中最重要的就是我們要覆蓋getConnection()方法,這是給別的要操作數據庫的方法提供Connection對象,所以這次要從剛才保存連接的LinkedList集合取出連接給別的方法(即從連接池中取出連接)。但是我們從集合取出給別的方法的又不能直接就是數據庫提供的Connection對象(也就是剛才初始化存入集合中的Connection對象),因為最后必須要釋放資源,而一旦直接調用數據庫提供的Connection對象的close()方法則就是將該連接銷毀了,因此我們通過集合(連接池)取出Connection對象應該進行功能增強,最后再在getConnection()方法返回出去給別的方法使用。而這一步也是整個步驟中最難的,也是最重要的一個知識點。
以上的關鍵在於第4步,我們要返回的是一個即是Connection接口的實例對象,又不能因為調用close()方法而銷毀了這個鏈接,而是調用close()方法能將該Connection接口的實例對象返回給集合中。因此我們必須要對Connection中的close()方法進行覆寫。但如何對Connection的對象進行功能增強,主要有下面三種方法:
① 編寫一個Connection的子類,覆寫close()方法。
② 使用包裝設計模式。
③ 使用動態代理。
使用子類的方式不合理,且不說我們的Connection對象中封裝了很多和數據庫相關的信息,單是Connection接口就需要現實很多很多方法,所以這不實際。在本篇中我們使用包裝設計模式來增強數據庫直接提供的Connection對象,覆寫close()方法,將其功能從銷毀連接變為返回進集合中。關於包裝設計模式請看《包裝設計模式》。
接下來我們將會在一個工程中按上面的步驟來簡單地創建一個自定義的數據庫連接池。
⑴ 創建一個JdbcPool類,同之前使用JDBC工具類(如《JDBC操作數據庫的學習(2)》)一樣,在初始化時就根據配置文件實現注冊驅動,獲取連接等等操作。這里我們創建一個初始連接池數為10個的連接池。
配置文件內容如下:

1 public class JdbcPool implements DataSource { 2 3 private static LinkedList<Connection> connectionList = new LinkedList<>(); //以集合作為連接池 4 private static Properties config = new Properties(); 5 6 static{ 7 InputStream in = JdbcPool.class.getClassLoader().getResourceAsStream("database.properties"); 8 try { 9 config.load(in); 10 Class.forName(config.getProperty("driver")); //注冊驅動 11 String url = config.getProperty("url"); 12 String username = config.getProperty("username"); 13 String password = config.getProperty("password"); 14 for(int i=0;i<10;i++) { //獲取十個連接 15 Connection conn = DriverManager.getConnection(url, username, password); 16 connectionList.addLast(conn); //將每個連接都添加到集合(池)中 17 } 18 19 } catch (Exception e) { 20 throw new ExceptionInInitializerError(e); 21 } 22 } 23 24 @Override 25 public Connection getConnection() throws SQLException { 26 if(connectionList.size()<1) { 27 throw new RuntimeException("數據庫連接忙"); 28 } 29 Connection conn = connectionList.removeFirst(); 30 MyConnection myConn = new MyConnection(conn); //從池中取出連接並使用包裝類增強close方法 31 32 return myConn; 33 } 34 35 class MyConnection implements Connection{ //包裝設計模式的類 36 private Connection conn; 37 public MyConnection(Connection conn) { 38 this.conn = conn; 39 } 40 41 @Override 42 public void close() throws SQLException { 43 connectionList.addFirst(this.conn); //調用close方法時只是將連接重新返回池中,而不會銷毀 44 } 45 46 @Override 47 public Statement createStatement() throws SQLException { 48 this.conn.createStatement(); //對於不增強的方法則調用目標對象的方法即可,在MyConnection包裝類中其他方法都是這樣的 49 return null; 50 } 51 。。。 //以下省略Connection接口中覆寫的其他方法,對於不想增強的方法都如上(createStatement方法)所示 52 } // MyConnection包裝類完成 53 54 @Override 55 public Connection getConnection(String username, String password) 56 throws SQLException { //實現DataSource接口的其他方法,在本案例中對我們來說並沒有什么作用 57 return null; 58 } 59 。。。//以下省略DataSource接口中覆寫的其他方法,這些方法對我們來說暫時沒有作用 60 }
以上就是一個簡單的使用JDBC來實現一個數據庫連接池的代碼。在上面的代碼中,我們在該類(JdbcPool)的初始化時(靜態代碼塊里),先通過驅動管理器獲取了十個連接,並將數據庫提供的連接Connection對象存入集合也就是我們說的池中。
而別的方法需要通過JdbcPool類的getConnection方法來獲取池中的連接,但是我們通過一個包裝類,同時在上面也是內部類,將池中的連接取出,並做了包裝,覆寫了close方法,最后再返回出去,這時候別的方法獲取的並不是數據庫直接提供的連接,而是我們自己包裝過的類,這個包裝類除了close方法不同外,其他還是一樣的和原來數據庫提供的連接具有相同的方法,以為就是在調用原來連接的方法。
使用包裝設計模式雖然能滿足我們的需求,但是如果要包裝的接口方法過多,例如Connection接口,而我們只想覆寫其中一兩個方法,則程序會造成極大的冗余,因此最佳的方式應該是使用動態代理。