一個簡單資料庫連線池的實現
一、已實現功能
資料庫連線快取。將資料庫連線與執行緒ID繫結並提供執行資料庫操作時檢測。資料庫連線超時檢測。初始化資料庫環境,包括初始化資料庫,資料庫使用者,資料庫表。
二、程式碼列表:
1、MySqlDBManager:
用於管理資料庫配置、初始化資料庫環境及建立資料庫連線等操作。
2、ConnectionAdapter:
資料庫連線適配,封裝了具體資料庫連線,在現有功能上新增與執行緒ID繫結、連線超時檢測等功能。
3、ConnectionException:
資料庫異常,簡單繼承自SQLException,目前沒有具體實現。
4、ConnectionPool:
資料庫連線池具體實現,資料庫連接出入棧及釋放所有連線操作。
5、ITable:
一個表的超類,只有兩個函式:判斷表存在(tableIsExist)、建立表(createTable)。
6、DBConnectionFactory:
資料庫連線工廠,唯一對外介面:獲取連線(getConnection)、初始化資料庫上下文(initDataBaseContext)、關閉所有連線(closeAllConnection)。
三、程式碼設計
1、MySqlDBManager:此類只被DBConnectionFactory呼叫,初始化主要包含:
- 檢測資料庫及賬戶是否存在
- 檢測資料庫中表是否存在
主要實現的函式:
- getConnection: 從資料庫連線池中獲取一個連線並返回。
- closeAllConnection: 釋放所有連線。
- createNewConnection:函式為預設作用域,在DBConnectFactory中呼叫,建立一個新的資料庫連線。
class MySqlDBManager{ private final static String TAG = "MysqlDBConnectionManager"; private final static String driverClassName = "com.mysql.jdbc.Driver"; private final String database = "blog"; private final String URL = "jdbc:mysql://localhost:3306/" + database + "?useSSL=false"; private final String USERNAME = "****"; private final String PASSWORD = "****"; private final String createDatabase = "create DATABASE "+ database +" CHARACTER SET utf8;"; private final String createUser = "create USER '" + USERNAME + "'@'localhost' IDENTIFIED BY '" + PASSWORD + "';"; private final String grantUser = "GRANT ALL PRIVILEGES ON " + database + ".* TO '" + USERNAME + "'@'localhost';"; private final String flush = "FLUSH PRIVILEGES;"; private final String ROOT_URL = "jdbc:mysql://localhost:3306/mysql?useSSL=false"; private final String ROOT_USERNAME = "root"; private final String ROOT_PASSWORD = "*******"; //private final static int intMaxConnectionNum = 50; //private int intConnectionNum = 0; private static ConnectionPool connPool = new ConnectionPool(); MySqlDBManager(){ try{ Class.forName(driverClassName); } catch(ClassNotFoundException ce){ ce.printStackTrace(); } initDatabase(); } public Connection getConnection() { // TODO Auto-generated method stub return connPool.pop(); } public void closeAllConnection() { connPool.releaseAllConnection(); } private void initDatabase(){ if(!DatabaseIsExist()){ switchRootInitDatabase(); } } private boolean DatabaseIsExist(){ boolean result = false; Connection conn = null; try { conn = (Connection) DriverManager.getConnection(URL, USERNAME, PASSWORD); result = true; } catch (SQLException e) { // TODO Auto-generated catch block //e.printStackTrace(); } finally{ try { if(conn != null){ conn.close(); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return result; } private void switchRootInitDatabase(){ try { Connection conn = DriverManager.getConnection(ROOT_URL, ROOT_USERNAME, ROOT_PASSWORD); Statement smt = conn.createStatement(); smt.addBatch(createDatabase); smt.addBatch(createUser); smt.addBatch(grantUser); smt.addBatch(flush); smt.executeBatch(); smt.close(); conn.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void initTable(ITable tab){ if(!tab.tableIsExist(getConnection())){ tab.createTable(getConnection()); } } public boolean tableIsExist(ITable tab){ return tab.tableIsExist(getConnection()); } Connection createNewConnection(){ Connection conn = null; try { conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(TAG + ",createNewConnection()!"); return conn; } }
2、ConnectionPool
資料庫連線池,包含一個idleList和usedList。
主要函式:
push:當應用使用完資料庫連線,呼叫close方法時,將資料庫連線放回連線池。
pop:當顯示呼叫DBConnectionFactor.geConnection方法時,返回一個數據庫連線給應用。
releaseAllConnection:釋放所有資料庫連線,因為此實現設計中存在一個雙向關聯(connectionPool持有ConnectionAdapter引用,ConnectionAdapter持有connectionPool引用),必須在釋放操作最後呼叫idleList.Clear和usedList.Clear。
class ConnectionPool {
private final static String TAG = "ConnectionPool";
private static ArrayList<ConnectionAdapter> idleConnection = new ArrayList<>();
private static ArrayList<ConnectionAdapter> usedConnection = new ArrayList<>();
public ConnectionPool(){}
void push(ConnectionAdapter connAdapter){
// TODO Auto-generated method stub
synchronized(ConnectionPool.class){
if(connAdapter != null){
usedConnection.remove(connAdapter);
}
if(connAdapter != null && !idleConnection.contains(connAdapter)){
idleConnection.add(connAdapter);
}
}
System.out.println(TAG + ",idle connection number: " + idleConnection.size());
System.out.println(TAG + ",used connection number: " + usedConnection.size());
}
public Connection pop(){
synchronized(ConnectionPool.class){
ConnectionAdapter connAdapter = null;
if(idleConnection.isEmpty()){
connAdapter = createNewConnectionAdapter();
}else{
connAdapter = idleConnection.get(0);
idleConnection.remove(connAdapter);
if(connAdapter == null || connAdapter.isInvalid()){
connAdapter = createNewConnectionAdapter();
}
}
//System.out.println(TAG + ",pop()");
if(connAdapter != null && !usedConnection.contains(connAdapter)){
usedConnection.add(connAdapter);
}
return connAdapter.getProxyConnection();
}
}
private ConnectionAdapter createNewConnectionAdapter(){
return DBConnectionFactory.createNewConnectionAdapter(ConnectionPool.this);
}
public void releaseAllConnection() {
// TODO Auto-generated method stub
Iterator<ConnectionAdapter> it = idleConnection.iterator();
while(it.hasNext()){
it.next().Close();
}
it = usedConnection.iterator();
while(it.hasNext()){
it.next().Close();
}
idleConnection.clear();
usedConnection.clear();
}
}
3、ConnectionAdapter
資料庫介面卡,新增兩個功能:將資料庫連線與執行緒ID繫結,MYSQL超時檢測。
新增功能解釋:
1、與執行緒ID繫結
在實際使用過程中發現存在如下一個情況,假設兩個執行緒同時進行資料庫操作時,執行緒A獲取一個數據庫連線conn_1,當執行完操作以後,呼叫conn_1.colse,將資料庫連線返回資料庫連線池後,其實執行緒A依然是持有Conn_1的引用的,如果此時執行緒A繼續使用conn_1進行資料庫操作,函式將正常執行。如果此時執行緒B從資料庫連線池獲取一個空閒連線等到conn_1,那麼這時候將兩個執行緒將同時持有同一個資料庫連線。解決方案如下:在ConnectionAdapter中儲存一個當前持有該連線的執行緒ID,在操作執行之前比對執行緒ID,如果非持有執行緒執行的資料庫操作,提示該連線已經關閉。
實現此功能也為以後實現連線池超時回收連線考慮,超時回收基於功能可以簡單實現。
2、MYSQL超時檢測
MYSQL資料庫預設配置存在一個8小時自動關閉超時連線,當一個連線超過8小時沒有使用,會被MYSQL關閉,如果在此連線上執行資料庫操作,會出現異常。
主要函式:
- getProxyConnection:獲取一個代理連線。
- Close:關閉資料庫連線。此函式只被ConnectionPool連線呼叫,用於釋放連線。
- isInvalid:連線有效性判斷,包括超時判斷。
- markUsed,markIdle:連線標記為空閒或者正在使用。
- checkStatus;在此連線上執行任何操作之前進行檢測,目前主要檢測執行緒ID。
- _Connection:一個簡單的代理。
class ConnectionAdapter {
private final long connectionIimeout = 8 * 60 * 60 * 1000 - 10 * 60 * 1000;//減去十分鐘左右誤差時間
private long lastIimeoutTest = 0L;
private boolean isIdle = true;
private long ownerThreadId = -1L;
private Connection conn;
private Connection proxyConn;
public ConnectionAdapter(Connection conn,ConnectionPool pool){
this.conn = conn;
this.proxyConn = (Connection) Proxy.newProxyInstance(conn.getClass().getClassLoader(), new Class[]{Connection.class}, new _Connection(conn,pool));
this.lastIimeoutTest = System.currentTimeMillis();
}
public Connection getProxyConnection(){
if(markUsed()){
return proxyConn;
}else{
return null;
}
}
public void Close(){
try {
if( conn != null && !conn.isClosed()){
conn.close();
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public boolean isInvalid(){
if(conn == null){return true;}
if(proxyConn == null){return true;}
if(connectionIsWaitTimeout()){return true;}
try {
if(!conn.isClosed()){
return false;
}
} catch (SQLException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
return true;
}
private Boolean connectionIsWaitTimeout(){
boolean result = true;
if((System.currentTimeMillis() - lastIimeoutTest) > connectionIimeout){
result = testConnectionIsOk();
}else{
result = false;
}
lastIimeoutTest = System.currentTimeMillis();
return result;
}
private boolean testConnectionIsOk(){
try{
PreparedStatement stat = conn.prepareStatement("select 1 from users where 1=2");
stat.execute();
stat.close();
return true;
} catch (CommunicationsException e){
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return false;
}
private boolean markUsed(){
if(!isIdle){
return false;
}
isIdle = false;
ownerThreadId = Thread.currentThread().getId();
return true;
}
private boolean markIdle() throws Exception{
if(isIdle){return false;}
if(ownerThreadId != Thread.currentThread().getId()){
throw new ConnectionException("Current ThreadId is " + Thread.currentThread().getId() + ",but the connection is used by " + ownerThreadId + " Thread!");
}
isIdle = true;
ownerThreadId = -1;
return true;
}
private void checkStatus() throws Exception{
if(isIdle){
throw new ConnectionException("this connection is closed!");
}
if(ownerThreadId != Thread.currentThread().getId()){
throw new ConnectionException("Current ThreadId is " + Thread.currentThread().getId() + ",but the connection is used by " + ownerThreadId + " Thread!");
}
}
private boolean isClosed(){
if(isIdle){return true;}
if(ownerThreadId != Thread.currentThread().getId()){return true;}
return false;
}
private class _Connection implements InvocationHandler{
private final Connection conn;
private final ConnectionPool pool;
_Connection(Connection conn,ConnectionPool pool){
this.conn = conn;
this.pool = pool;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// TODO Auto-generated method stub
Object obj = null;
if("close".equals(method.getName())){
if(markIdle()){
pool.push(ConnectionAdapter.this);
}
}else if("isClosed".equals(method.getName())){
obj = isClosed();
}else{
checkStatus();
obj = method.invoke(conn, args);
}
return obj;
}
}
}
4、ITable
資料庫表超類,使用者初始化資料庫表及基礎資料,因為單獨建立資料庫表示一件很繁瑣的事情,如果可以在應用啟動的時候檢測資料庫表是否存在,並完成一些基礎資料的初始化,會減輕很多工作。
public abstract class ITable {
public boolean tableIsExist(Connection conn){
boolean result = true;
try{
PreparedStatement stat = conn.prepareStatement(getTestExistStatement());
stat.execute();
stat.close();
} catch (CommunicationsException e){
result = false;
} catch (SQLException e) {
// TODO Auto-generated catch block
result = false;
} finally{
try {
conn.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return result;
};
public void createTable(Connection conn){
Statement smt;
try {
smt = conn.createStatement();
String[] str = getCreateStatement();
for(int k = 0; k < str.length; k++){
smt.addBatch(str[k]);
}
smt.executeBatch();
smt.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally{
try {
conn.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
protected abstract String getTestExistStatement();
protected abstract String[] getCreateStatement();
}
四、總結
作為一個完整的實現,對外唯一介面為DBConnectionFactory,所以外部應只調用DBConnectionFactory中的函式,不應直接呼叫其他類的函式。
程式碼貼在這裡好像顯得比較亂,所以只貼了幾個主要實現的程式碼,想檢視完整程式碼可以到下面幾個連結檢視。
五、專案程式碼
完整程式碼及實際使用:https://github.com/hu-xuemin/xBlog.git。其中com.huxuemin.xblog.database包中為此文相關程式碼。