mybatis小记——数据库连接池实现

时间:2021-02-24 15:01:24

简单实现
说到数据库连接池的实现,可能大家并不陌生,应该都或多或少的实现过,那么先来说说一个简单的数据库连接池的实现。

既然是连接池,首先得有连接,然后有池子(废话),连接使用jdk的Connection,池子使用一个List<Connection>即可,需要连接的时候从list中获取,如果list中没有那么就新new一个并加入到list中就可以了。使用完成之后,将连接放回list中即可,这样一个最简单的连接池就实现了。(PS:相关接口一定要是线程安全的)

上面实现了一个很简单的线程池,满足了大部分的使用场景,但是从程序的性能、并发性、可扩展性、可维护性、易用性等方面还有一定的欠缺,下面我们将列举出上面简单的连接池的一些不足之处,针对这些不足之处一步一步进行改进。

  1. 设置最大连接数量
    不能无节制的创建新的连接,应该对最大的连接数量进行限制,如果超过了限制的连接数量,或者是无法从老的连接中恢复一个新的连接,那么就抛出一定一场给调用者,调用者可以根据异常来决定将请求缓存起来等到有新的可用的连接的时候再处理。
  2. 释放空闲连接
    如果某一个时刻并发度比较高,那么连接池中的连接的数量将等于程序启动以来最大的并发数,但是在并发比较小的时候,[1]连接池中的连接没有被使用,但是却一直占用着内存空间,是一种资源的浪费[2]即使目前并发度非常的小,但是也需要保留核心数量的连接,应对突然爆发的并发,总结起来就是需要设置poolMaximumIdleConnections(最大空闲的连接数量),
  3. 设置获取连接的超时
    设置连接获取超时等值,这样可以防止无止境的等待。
    ……..
    为了获取更好的性能,需要优化的东西还很多,幸好目前在业界也有很多相应的开源的实现,可以通过分析优秀的开源实现来深入理解如果设计好一个优秀的连接池。

mybatis的连接池的实现
上面阐述了一个简单的连接池的实现,下面将对mybatis的数据库连接池的实现做一个源码级的分析,只要深入到了源码才能正确时的调用API

其实mybatis中的连接池的实现和上面的简单连接池的实现时一样的,只是把上面讨论的那些需要优化的点实现了,在mybatis中数据库连接池的实现主要在类PooledDataSource中,直接找到该类的getConnection方法可以看到,该方法调用了一个popConnection的方法,该方法真正处理了从连接池中获取连接的过程,下面对该方法进行仔细的分析:

private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();
int localBadConnectionCount = 0;

while (conn == null) {
// 多线程同步,保证连接池线程安全
synchronized (state) {
// 1、首先从空闲连接池获取连接
if (!state.idleConnections.isEmpty()) {
// Pool has available connection
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {
// Pool does not have available connection
// 2、然后判断连接池的连接数量是否达到上限
if (state.activeConnections.size() < poolMaximumActiveConnections) {
// Can create new connection
// 没有达到连接池数量的上限,创建新的连接池
conn = new PooledConnection(dataSource.getConnection(), this);
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else {
// Cannot create new connection
// 已经达到了连接池的上限了,无法创建新的连接
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
// 判断最早被使用的连接是否已经使用超时,如果使用超时那么将回滚正在
// 该连接上的请求,然后分配给此次的连接请求
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// Can claim overdue connection
state.claimedOverdueConnectionCount++;
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
state.accumulatedCheckoutTime += longestCheckoutTime;
state.activeConnections.remove(oldestActiveConnection);
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
try {
// 执行数据库任务的回滚
oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException e) {
/*
Just log a message for debug and continue to execute the following
statement like nothing happend.
Wrap the bad connection with a new PooledConnection, this will help
to not intterupt current executing thread and give current thread a
chance to join the next competion for another valid/good database
connection. At the end of this loop, bad {@link @conn} will be set as null.
*/

log.debug("Bad connection. Could not roll back");
}
}
// 回滚成功,将该连接分配给此次连接请求
// 下面使用了连接代理的方式创建,这种方式可以拦截Connection的close方法
// 实现连接的回收(后续会详细讲解具体的实现)
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
// Must wait
// 通过上面的方式无法获取到新的连接,阻塞等待获取新的连接,如果在等待过程中
// 出现了中断异常,那么就会退出该次循环,那么就可能导致conn=null
try {
if (!countedWait) {
state.hadToWaitCount++;
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
state.wait(poolTimeToWait);
state.accumulatedWaitTime += System.currentTimeMillis() - wt;
} catch (InterruptedException e) {
break;
}
}
}
}
if (conn != null) {
// ping to server and check the connection is valid or not
// 上面的操作不一定能保证此次请求完全能获取一个可用的连接
// 所以需要再次判断conn是否可用,如果该连接上仍然有任务,那么回滚在连接上的任务;
// 如果该连接上没有任务,那么直接返回。
if (conn.isValid()) {
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
state.activeConnections.add(conn);
state.requestCount++;
state.accumulatedRequestTime += System.currentTimeMillis() - t;
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
state.badConnectionCount++;
localBadConnectionCount++;
conn = null;
if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Could not get a good connection to the database.");
}
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
}

}

// 如果连接仍然为空,那么就抛出异常给上层应用
if (conn == null) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}

return conn;
}

从上面的分析,我们知道了该方法API要么返回正常的数据库连接,要么抛出异常信息,调用者需要捕获一场信息,并及时的缓存此次数据库操作请求。

上面说到了返回Connection的时候进行了代理的封装,该代理是PooledConnection,该类是jdk的Connection一个代理,下面主要通过查看该代理的invoke方法来说一说该代理类的主要作用

private static final String CLOSE = "close";
private final Connection realConnection;
......
/*
* Required for InvocationHandler implementation.
*
* @param proxy - not used
* @param method - the method to be executed
* @param args - the parameters to be passed to the method
* @see java.lang.reflect.InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])
*/

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取方法名称
String methodName = method.getName();
// 判断是否是close方法
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
// 如果是close方法,那么只是将该连接重新放回连接池中,并不真正的关闭连接
dataSource.pushConnection(this);
return null;
} else {
// 如果不是close方法,那么通过反射的方式调用真正的Connection执行
// (需要注意的是PooledConnection只是一个代理,正在执行的还是jdk的Connection)
try {
if (!Object.class.equals(method.getDeclaringClass())) {
// issue #579 toString() should never fail
// throw an SQLException instead of a Runtime
checkConnection();
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}

这种设计方式可以使用任何连接建立消耗大的场景,如在客户端的socket连接池的使用,如jsch的ssh的会话连接池。

学习框架源码也是有套路的!