JDBC | 第七章: JDBC資料庫連線池使用
概述
資料庫連線池是負責分配、管理和釋放資料庫連線,它允許應用程式重複使用一個現有的資料庫連線,而不是再重新建立一個。那麼其中的執行機制又是怎樣的呢?今天主要介紹一下資料庫連線池原理和常用的連線池。
為什麼要使用連線池
資料庫連線是一種關鍵的有限的昂貴的資源,這一點在多使用者的網頁應用程式中體現得尤為突出。 一個數據庫連線物件均對應一個物理資料庫連線,每次操作都開啟一個物理連線,使用完都關閉連線,這樣造成系統的效能低下。
資料庫連線池的解決方案是在應用程式啟動時建立足夠的資料庫連線,並講這些連線組成一個連線池(簡單說:在一個“池”裡放了好多半成品的資料庫連線物件),由應用程式動態地對池中的連線進行申請、使用和釋放。對於多於連線池中連線數的併發請求,應該在請求佇列中排隊等待。並且應用程式可以根據池中連線的使用率,動態增加或減少池中的連線數。
連線池技術儘可能多地重用了消耗記憶體地資源,大大節省了記憶體,提高了伺服器地服務效率,能夠支援更多的客戶服務。通過使用連線池,將大大提高程式執行效率,同時,我們可以通過其自身的管理機制來監視資料庫連線的數量、使用情況等。
不使用連線池流程
下面以訪問MySQL為例,執行一個SQL命令,如果不使用連線池,需要經過哪些流程。
不使用資料庫連線池的步驟:
- TCP建立連線的三次握手
- MySQL認證的三次握手
- 真正的SQL執行
- MySQL的關閉
- TCP的四次握手關閉
可以看到,為了執行一條SQL,卻多了非常多網路互動
優點:
實現簡單
缺點:
- 網路IO較多
- 資料庫的負載較高
- 響應時間較長及QPS較低
- 應用頻繁的建立連線和關閉連線,導致臨時物件較多,GC頻繁
- 在關閉連線後,會出現大量TIME_WAIT 的TCP狀態(在2個MSL之後關閉)
使用連線池流程
第一次訪問的時候,需要建立連線。 但是之後的訪問,均會複用之前建立的連線,直接執行SQL語句。
優點:
- 較少了網路開銷
- 系統的效能會有一個實質的提升
- 沒了麻煩的TIME_WAIT狀態
資料庫連線池的工作原理
連線池的工作原理主要由三部分組成,分別為
- 連線池的建立
- 連線池中連線的使用管理
- 連線池的關閉
第一、連線池的建立。
一般在系統初始化時,連線池會根據系統配置建立,並在池中建立了幾個連線物件,以便使用時能從連線池中獲取。連線池中的連線不能隨意建立和關閉,這樣避免了連線隨意建立和關閉造成的系統開銷。
Java中提供了很多容器類可以方便的構建連線池,例如Vector、Stack等。
第二、連線池的管理。
連線池管理策略是連線池機制的核心,連線池內連線的分配和釋放對系統的效能有很大的影響。其管理策略是:
當客戶請求資料庫連線時,首先檢視連線池中是否有空閒連線,如果存在空閒連線,則將連線分配給客戶使用;如果沒有空閒連線,則檢視當前所開的連線數是否已經達到最大連線數,如果沒達到就重新建立一個連線給請求的客戶;如果達到就按設定的最大等待時間進行等待,如果超出最大等待時間,則丟擲異常給客戶。
當客戶釋放資料庫連線時,先判斷該連線的引用次數是否超過了規定值,如果超過就從連線池中刪除該連線,否則保留為其他客戶服務。
該策略保證了資料庫連線的有效複用,避免頻繁的建立、釋放連線所帶來的系統資源開銷。
第三、連線池的關閉。
當應用程式退出時,關閉連線池中所有的連線,釋放連線池相關的資源,該過程正好與建立相反。
連線池需要注意的點
1、併發問題
為了使連線管理服務具有最大的通用性,必須考慮多執行緒環境,即併發問題。
這個問題相對比較好解決,因為各個語言自身提供了對併發管理的支援像java,c#等等,使用synchronized(java)lock(C#)關鍵字即可確保執行緒是同步的。
2、事務處理
我們知道,事務具有原子性,此時要求對資料庫的操作符合“ALL-OR-NOTHING”原則,即對於一組SQL語句要麼全做,要麼全不做。
我們知道當2個執行緒共用一個連線Connection物件,而且各自都有自己的事務要處理時候,對於連線池是一個很頭疼的問題,因為即使Connection類提供了相應的事務支援,可是我們仍然不能確定那個資料庫操作是對應那個事務的,這是由於我們有2個執行緒都在進行事務操作而引起的。
為此我們可以使用每一個事務獨佔一個連線來實現,雖然這種方法有點浪費連線池資源但是可以大大降低事務管理的複雜性。
3、連線池的分配與釋放
連線池的分配與釋放,對系統的效能有很大的影響。合理的分配與釋放,可以提高連線的複用度,從而降低建立新連線的開銷,同時還可以加快使用者的訪問速度。
對於連線的管理可使用一個List。即把已經建立的連線都放入List中去統一管理。每當使用者請求一個連線時,系統檢查這個List中有沒有可以分配的連線。如果有就把那個最合適的連線分配給他,如果沒有就丟擲一個異常給使用者。
4、連線池的配置與維護
連線池中到底應該放置多少連線,才能使系統的效能最佳?
系統可採取設定最小連線數(minConnection)和最大連線數(maxConnection)等引數來控制連線池中的連線。比方說,最小連線數是系統啟動時連線池所建立的連線數。如果建立過多,則系統啟動就慢,但建立後系統的響應速度會很快;如果建立過少,則系統啟動的很快,響應起來卻慢。這樣,可以在開發時,設定較小的最小連線數,開發起來會快,而在系統實際使用時設定較大的,因為這樣對訪問客戶來說速度會快些。最大連線數是連線池中允許連線的最大數目,具體設定多少,要看系統的訪問量,可通過軟體需求上得到。
如何確保連線池中的最小連線數呢?有動態和靜態兩種策略。動態即每隔一定時間就對連線池進行檢測,如果發現連線數量小於最小連線數,則補充相應數量的新連線,以保證連線池的正常運轉。靜態是發現空閒連線不夠時再去檢查。
資料庫連線池的使用
c3p0
package cn.soboys.kenx.datasource;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.io.FileInputStream;
import java.sql.Connection;
import java.util.Properties;
/*
* jdbc 通過資料連線池 c3p0 獲取資料庫連線
*/
public class C3p0DataSource {
//通過xml配置檔案
public Connection getConnectionByC3p0PoolXml() {
//建立資料庫連線池時,不傳引數預設指default-config中引數,傳入引數,則根據<name-config name="myc3p0">中的名字去找
try {
ComboPooledDataSource cpds = new ComboPooledDataSource();
//或者不使用預設的
ComboPooledDataSource cpds2 = new ComboPooledDataSource("myc3p0");
Connection conn = cpds.getConnection();
System.out.println(cpds.getProperties());//這裡可以輸出mysql的使用者名稱和密碼;
System.out.println(cpds.hashCode());//這裡是輸出連線的地址。
System.out.println("資料庫連線成功");
return null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//通過程式碼配置
public Connection getConnectionByC3p0PoolCode() {
//建立資料庫連線池時,不傳引數預設指default-config中引數,傳入引數,則根據<name-config name="myc3p0">中的名字去找
try {
ComboPooledDataSource cpds = new ComboPooledDataSource();
cpds.setDriverClass("com.mysql.cj.jdbc.Driver");
cpds.setJdbcUrl("jdbc:mysql://localhost:3306/kenx_test?characterEncoding=utf-8");
cpds.setUser("root");
cpds.setPassword("root");
cpds.setInitialPoolSize(5);
cpds.setMaxPoolSize(10);
cpds.setCheckoutTimeout(3000);
Connection conn = cpds.getConnection();
System.out.println("資料庫連線成功");
return conn;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//通過屬性檔案配置,預設c3p0不支援屬性檔案自動裝配 需要我們自己手動設定屬性
public Connection getConnectionByC3p0PoolProperties() {
//建立資料庫連線池時,不傳引數預設指default-config中引數,傳入引數,則根據<name-config name="myc3p0">中的名字去找
try {
ComboPooledDataSource cpds = new ComboPooledDataSource();
//載入配置檔案
Properties properties = new Properties();
properties.load(new FileInputStream("src/main/resources/c3p0.properties"));
String driver = properties.getProperty("driverClassName");
String url = properties.getProperty("jdbcUrl");
String username = properties.getProperty("username");
String password = properties.getProperty("password");
cpds.setDriverClass(driver);
cpds.setJdbcUrl(url);
cpds.setUser(username);
cpds.setPassword(password);
cpds.setInitialPoolSize(5);
cpds.setMaxPoolSize(10);
cpds.setCheckoutTimeout(3000);
Connection conn = cpds.getConnection();
System.out.println("資料庫連線成功");
return conn;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
dbcp
package cn.soboys.kenx.datasource;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.util.Properties;
/*
* jdbc 通過資料連線池 DBCP 獲取資料庫連線
*/
public class DbcpDataSource {
//通過程式碼配置
public Connection getConnectionByDBCPPoolCode() {
try {
BasicDataSource bds = new BasicDataSource();
bds.setUrl("jdbc:mysql://localhost:3306/kenx_test?characterEncoding=utf-8");
bds.setDriverClassName("com.mysql.jdbc.Driver");
bds.setUsername("root");
bds.setPassword("root");
bds.setInitialSize(5);
Connection conn = bds.getConnection();
System.out.println("資料庫連線成功");
return conn;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//通過屬性檔案配置,資料來源工廠不需要手動配置單個屬性
public Connection getConnectionByDBCPPoolProperties() {
Properties prop = new Properties();
try {
prop.load(new FileInputStream("src/main/resources/dbcp.properties"));
BasicDataSource bds = BasicDataSourceFactory.createDataSource(prop);
System.out.println("資料庫連線成功");
return bds.getConnection();
} catch (IOException e) {
e.printStackTrace();
return null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
durid
package cn.soboys.kenx.datasource;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.util.Properties;
/*
* jdbc 通過資料連線池 Druid 獲取資料庫連線
*/
public class DruidDatasource {
//通過屬性檔案配置,資料來源工廠不需要手動配置單個屬性
public Connection getConnectionByDuridPoolProperties() {
Properties prop = new Properties();
try {
prop.load(new FileInputStream("src/main/resources/durid.properties"));
// 獲取連線池物件
DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);
System.out.println("資料庫連線成功");
return dataSource.getConnection();
} catch (IOException e) {
e.printStackTrace();
return null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//通過程式碼配置
public Connection getConnectionByDBCPPoolCode() {
try {
DruidDataSource bds=new DruidDataSource();
bds.setUrl("jdbc:mysql://localhost:3306/kenx_test?characterEncoding=utf-8");
bds.setDriverClassName("com.mysql.cj.jdbc.Driver");
bds.setUsername("root");
bds.setPassword("root");
bds.setInitialSize(5);
Connection conn = bds.getConnection();
System.out.println("資料庫連線成功");
return conn;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
完整專案案例
點選這裡 github