1. 程式人生 > >一個簡單資料庫連線池的實現

一個簡單資料庫連線池的實現

一、已實現功能

  資料庫連線快取。將資料庫連線與執行緒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包中為此文相關程式碼。