目录

  • 1、前言
  • 2、获取连接
  • 2.1 mybatis连接池设计
  • 2.2 mybatis获取连接流程
  • 3、释放连接
  • 4、题外话


1、前言

之所以研究这个问题,是因为在一次开发中手动开启事务后没有调用close()方法导致数据库连接池耗尽的情况:

springboot3 mybatis连接池 mybatis 连接池原理_spring


此前从来没关注过mybatis和数据库连接池之间的关系,正好借此机会从源码的角度来看看mybatis是怎么处理连接的。(虽然上面报错是来自druid的哈哈哈……)

2、获取连接

我们都知道,连接池的实现,核心的思想在于,将连接缓存起来。
但是对于连接池的一些细节,很多人可能没有去思考过:

  • mybatis的连接池是启动时就初始化指定数量的连接吗?
  • mybatis获取连接的流程是什么?
  • mybatis是怎么归还连接的?

带着这几个疑问,我们先打开源码来看看mybatis对连接池的设计。

2.1 mybatis连接池设计

mybatis内置了两个DataSource的实现:

  • UnpooledDataSource,该数据源对于每次获取请求都简单的打开和关闭连接。
  • PooledDataSource,该数据源在Unpooled的基础上构建了连接池(从名字上也可以看出来)。

在mybatis的org.apache.ibatis.datasource.pooled包中,我们可以看到如下四个类:

springboot3 mybatis连接池 mybatis 连接池原理_mybatis_02


这里简单说一下每个类的作用:

  1. PooledConnection:数据库连接,包括是否有效、创建时间、上次使用时间等
  2. PooledDataSource:实现了DataSource接口的实现类,用于提供获取连接
  3. PooledDataSourceFactory:工厂类,实例化了PooledDataSource
  4. PoolState:连接池,保存了数据库连接

简单来看,可以认为PooledConnection是一个连接,PoolState是连接池,PooledDataSource用于提供连接。

2.2 mybatis获取连接流程

既然是JDBC的扩展,mybatis获取连接自然逃不过getConnection()方法,此外通过观察这个方法的实现类,我们还可以看到druid、hikari等连接池的实现,不过这里我们只看mybatis默认的实现。

springboot3 mybatis连接池 mybatis 连接池原理_java_03

mybatis获取连接是通过PooledDataSourcepopConnection方法,不过这个方法体有点长,这里给出简化之后的代码流程:

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()方法不是关闭一个连接而是归还一个连接的关键!

springboot3 mybatis连接池 mybatis 连接池原理_动态代理_04


具体释放连接的方法在pushConnection中,这里偷个懒就不具体展开分析了,猜也能把流程猜个八九不离十了。

4、题外话

要知道,无论是原生的mybatis还是spring,我们在使用过程中对获取连接、释放连接都是无感的。那么我们在日常开发过程中,连接池是怎么和我们的mapper联系起来的呢?

实际上无论是原生的mybatis还是spring,我们在每次执行SQL语句时才会真正去获取一个连接,而原生的mybatis则需要我们去手动释放,spring则不需要,这里面的原理等下次再分析吧~