一个简单数据库连接池的实现
一、已实现功能
数据库连接缓存。将数据库连接与线程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包中为此文相关代码。