目录
1、前言
之所以研究这个问题,是因为在一次开发中手动开启事务后没有调用close()方法导致数据库连接池耗尽的情况:
此前从来没关注过mybatis和数据库连接池之间的关系,正好借此机会从源码的角度来看看mybatis是怎么处理连接的。(虽然上面报错是来自druid的哈哈哈……)
2、获取连接
我们都知道,连接池的实现,核心的思想在于,将连接缓存起来。
但是对于连接池的一些细节,很多人可能没有去思考过:
- mybatis的连接池是启动时就初始化指定数量的连接吗?
- mybatis获取连接的流程是什么?
- mybatis是怎么归还连接的?
带着这几个疑问,我们先打开源码来看看mybatis对连接池的设计。
2.1 mybatis连接池设计
mybatis内置了两个DataSource的实现:
- UnpooledDataSource,该数据源对于每次获取请求都简单的打开和关闭连接。
- PooledDataSource,该数据源在Unpooled的基础上构建了连接池(从名字上也可以看出来)。
在mybatis的org.apache.ibatis.datasource.pooled包中,我们可以看到如下四个类:
这里简单说一下每个类的作用:
- PooledConnection:数据库连接,包括是否有效、创建时间、上次使用时间等
- PooledDataSource:实现了DataSource接口的实现类,用于提供获取连接
- PooledDataSourceFactory:工厂类,实例化了PooledDataSource
- PoolState:连接池,保存了数据库连接
简单来看,可以认为PooledConnection是一个连接,PoolState是连接池,PooledDataSource用于提供连接。
2.2 mybatis获取连接流程
既然是JDBC的扩展,mybatis获取连接自然逃不过getConnection()方法,此外通过观察这个方法的实现类,我们还可以看到druid、hikari等连接池的实现,不过这里我们只看mybatis默认的实现。
mybatis获取连接是通过PooledDataSource的popConnection方法,不过这个方法体有点长,这里给出简化之后的代码流程:
private PooledConnection popConnection(String username,
String password) throws SQLException {
//……省略部分代码
while (conn == null) {
synchronized (state) {
// 如果连接池中有连接,直接获取第一个
if (!state.idleConnections.isEmpty()) {
// Pool has available connection
conn = state.idleConnections.remove(0);
}else {
// 连接池中没有连接,先判断有效的连接数量是否超过最大
if (state.activeConnections.size() < poolMaximumActiveConnections) {
// 如果没有,则创建一个新的连接
// Can create new connection
conn = new PooledConnection(dataSource.getConnection(), this);
} else {
// 此时无法创建新连接,从活跃队列中取出第一个判断是否已经过期
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// 已经过期,做一些记录,然后从队列移除这个连接
// 回收过期次数增加
// 统计过期回收时间增加
// 统计使用时间增加
// 将连接从活动队列中移除
state.activeConnections.remove(oldestActiveConnection);
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
// 如果不是自动提交事务,则将其回滚,因为可能存在一些操作
oldestActiveConnection.getRealConnection().rollback();
}
// 使用新的代理封装,可以使得不会被原有的影响
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
// 将之前的代理设置为无效
oldestActiveConnection.invalidate();
}else{
// 否则只能进行等待
}
}
}
}
}
if (conn != null) {
// 判断是否为有效连接,再做一些额外处理
}
// 从上面的循环退出,如果为null,则代表异常情况
if (conn == null) {
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
return conn;
}
建议读者可以自己去看看源码,代码虽然比较长,还是很容易读懂的。
3、释放连接
不知道各位有没有注意到,上述流程中有出现代理这个关键字,如果有阅读了源码的同学,也一定可以发现,PooledConnection其实是InvocationHandler的实现类。
如果直接说InvocationHandler接口你可能没什么印象,但我要是说JDK动态代理那你肯定就支棱起来了:这玩意我熟啊,八股文常见的静态代理动态代理cglib巴拉巴拉……
PooledConnection对Connection的close方法做了动态代理,这也是为什么我们调用close()方法不是关闭一个连接而是归还一个连接的关键!
具体释放连接的方法在pushConnection中,这里偷个懒就不具体展开分析了,猜也能把流程猜个八九不离十了。
4、题外话
要知道,无论是原生的mybatis还是spring,我们在使用过程中对获取连接、释放连接都是无感的。那么我们在日常开发过程中,连接池是怎么和我们的mapper联系起来的呢?
实际上无论是原生的mybatis还是spring,我们在每次执行SQL语句时才会真正去获取一个连接,而原生的mybatis则需要我们去手动释放,spring则不需要,这里面的原理等下次再分析吧~