數據庫訪問
訪問數據庫主要有以下幾個步驟:
- 加載數據庫驅動
- 創建數據庫連接
- 執行訪問操作並處理執行結果
- 關閉連接,釋放資源
在每一次請求數據庫都要經歷上述過程,創建連接和釋放資源也都是些重復性的動作,當請求量比較大時,資源是個很大的浪費。如何優化呢,可以使用連接池。
連接池
數據庫連接池負責分配、管理和釋放數據庫連接,它允許應用程序重復使用一個現有的數據庫連接,而不是再重新建立一個;釋放空閑時間超過最大空閑時間的數據庫連接來避免因為沒有釋放數據庫連接而引起的數據庫連接遺漏。
原理分析
數據庫連接是訪問數據庫必須的,可以在系統初始化時提前創建一定數量的連接,保存起來,當有創建連接的請求過來時,就直接拿出來,標記為使用中(避免與其他請求拿到同一個),使用完后,再放回連接池中。過程如下
- 系統在啟動時初始化連接池;
- 向連接池請求可用的數據庫連接;
- 如果沒有獲取到可用的數據庫連接,並且連接池中連接的數量小於最大連接數,則按照規定的步長給連接池中添加連接,然后再獲取,如果連接池中的數量已經到了最大連接數還沒有獲取到可用的連接,則等待其他請求釋放了連接后再獲取;
- 使用獲取到的數據庫連接請求數據庫;
- 將數據庫連接放回連接池,供其他連接使用;
簡單模擬實現
/**
* 連接池對象
*/
public class Pool {
private String driver = null;//數據庫驅動
private String url = null;//連接地址
private String username = null;//用戶
private String password = null;//密碼
//初始化連接數
private static int initSize = 2;
//池中最大連接數
private static int maxSize = 5;
//每次創建的連接數
private static int stepSize = 2;
//超時時間
private static int timeout = 2000;
//用來保存創建的數據庫連接
private List<PooledConnection> connectionPool = new ArrayList<PooledConnection>();
private Lock lock = new ReentrantLock();
public Pool(String driver, String url, String username, String password) throws Exception {
this.driver = driver;
this.url = url;
this.username = username;
this.password = password;
//創建連接池時初始化initSize個數據庫連接放入池中
resizePool(initSize);
}
/**
* 初始化連接池
* @param num 初始時按照initSize給池中添加連接,其他時候按照stepSize給池中加
*/
private void resizePool(int num) throws Exception {
//池中現有的連接數
int currentNum = connectionPool.size();
//池中的連接數不能超過設置的最大連接數
if (maxSize < currentNum + num) {
num = maxSize - currentNum;
}
//創建連接放入池中
for(int i=0; i<num; i++){
PooledConnection conn = newPooledConnection();
connectionPool.add(conn);
}
}
/**
* 創建連接池中的連接對象,包含了狀態(忙、閑)
* @return
*/
private PooledConnection newPooledConnection() throws Exception {
Connection conn = createConnection();//數據庫連接
PooledConnection pconn = new PooledConnection(conn);//連接池中的連接
return pconn;
}
/**
* 創建數據庫連接
* @return
* @throws SQLException
*/
private Connection createConnection() throws Exception {
//加載驅動
this.getClass().getClassLoader().loadClass(driver);
//創建連接
Connection conn = null;
conn = DriverManager.getConnection(url, username, password);
return conn;
}
/**
* 獲取數據庫連接
* @return
*/
public synchronized Connection getConnection() throws Exception {
Connection conn = null;
//從連接池中獲取連接
if(connectionPool.size() > 0){
//獲取一個空閑的數據庫連接
conn = getFreeConnFromPool();
//沒有獲取到連接
while(conn == null){
//隔2秒 重新獲取
System.out.println(Thread.currentThread().getName() + " 等待獲取連接");
Thread.sleep(2000);
conn = getFreeConnFromPool();
}
}
return conn;
}
/**
* 從連接池中獲取空閑的連接
* @return
*/
private Connection getFreeConnFromPool() throws Exception {
Connection conn = null;
//獲取可用的連接
conn = findAvailableConn();
//沒有獲取到可用的連接
if(conn == null){
//重新添加數據庫連接到連接池中
resizePool(stepSize);
//獲取可用的連接
conn = findAvailableConn();
}
return conn;
}
/**
* 獲取一個可用的連接
* @return
* @throws Exception
*/
private Connection findAvailableConn() throws Exception {
Connection conn = null;
if(connectionPool.size() > 0){
for(PooledConnection cip : connectionPool){
if(!cip.isBusy()){
conn = cip.getConn();
cip.setBusy(true);//獲取后將當前連接狀態標記為 執行
//判斷當前連接是否可用
if(!conn.isValid(timeout)){
//conn.isValid如果連接未關閉且有效,則返回true
//當前連接池連接的數據庫連接有問題,創建一個新的數據庫連接代替它
conn = createConnection();
cip.setConn(conn);
}
break;
}
}
}
return conn;
}
/**
* 把連接返回連接池
* 把連接返回給連接池就是把狀態標記為‘閑’,可以讓其他請求使用
*/
public void returnConnToPool(Connection conn){
for (PooledConnection cip : connectionPool) {
if (cip.getConn() == conn) {
cip.setBusy(false);//設置為空閑
System.out.println(Thread.currentThread().getName() + " 釋放了連接");
break;
}
}
}
}
/**
* 連接池中的連接對象
*/
public class PooledConnection {
//數據庫連接
private Connection conn;
//用於標識當前數據庫連接的狀態 true:執行 false:空閑
private boolean busy;
public PooledConnection(Connection conn) {
this.conn = conn;
}
// 此處省略get set方法
}
測試
public class App {
public static void main(String[] args) throws Exception {
//創建一個連接池
Pool pool = new Pool("com.mysql.jdbc.Driver", "jdbc:mysql://localhost:3306/test",
"root", "123456");
//創建7個線程,模擬並發
Thread[] threads = new Thread[7];
for(int i=0;i<threads.length;i++){
int t = i * 1000;
threads[i] = new Thread(()->{
Connection conn = null;
try {
conn = pool.getConnection();
if(conn != null){
System.out.println(Thread.currentThread().getName()+"獲取到連接 "+conn);
Thread.sleep(3000 + t);//模擬每個連接使用時間不等
pool.returnConnToPool(conn);
}else{
System.out.println(Thread.currentThread().getName()+" 沒有獲取到連接");
}
} catch (Exception e) {
e.printStackTrace();
}
}, "Thread-"+i);
}
for(Thread thread : threads){
thread.start();
}
}
}
測試結果
可以看出,請求數超過池中的最大數時,多余的請求會進入等待狀態,等到其他的連接被釋放后才會獲取到連接,線程0和線程5用的同一個連接,線程1和6用的同一個連接,實現了資源的重復利用,沒有在去重新創建和關閉連接,節省了完成這些工作需要的時間,提高了效率。