# 数据源分类

先看一下之前 xml 配置写的内容:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
      </dataSource>
    </environment>
  </environments>
</configuration>

dataSource 标签,Mybatis 将数据源分为三类:

  • UNPOOLED:不使用连接池
  • POOLED:使用连接池
  • JNDI:使用 JNDI 实现的数据源

这里不讲 JNDI,只讨论前两种。

先看一下相关的类图:

看一下最顶上的接口:

public interface DataSource  extends CommonDataSource, Wrapper {
  Connection getConnection() throws SQLException;
  Connection getConnection(String username, String password)
    throws SQLException;
}

之前使用 JDBC(导入 jar 包那会),是通过 DriverManager 方法获取连接的

Connection conn = DriverManager.getConnection(url,user,password);

很显然,Mybatis 是通过 DataSource 的实现类获取连接的(其实最终还是使用了 DriverManager ),连接的创建和关闭时非常消耗系统资源的,通过 DriverManager 获取的连接,每次操作都请求依次物理连接,使用完后关闭连接,频繁的建立,关闭连接会消耗资源。

创建一个连接往往比执行一个 sql 语句还要费时间

一般比较常用的 DataSource 实现,都是采用池化技术,就是在一开始就创建好 N 个连接,这样之后使用就无需再次进行连接,而是直接使用现成的 Connection 对象进行数据库操作。

POOLED 就是使用池化技术, UNPOOLED 就是非池化技术。

# 连接池

看上图,池化技术中,PooledDataSource 依赖于 UnpooledDataSource,这点待会在源码讲解中也会明显看出来。

# UNPOOLED

每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。

该数据源只需要配置 6 种属性,这在源码中表现如下:

public class UnpooledDataSource implements DataSource {
    private ClassLoader driverClassLoader;
    private Properties driverProperties;
    private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap();
    
    private String driver;
    private String url;
    private String username;
    private String password;
    private Boolean autoCommit; // 这个不算在 6 个配置属性里面
    private Integer defaultTransactionIsolationLevel; // 默认事务隔离级别
    private Integer defaultNetworkTimeout; // 超时连接的默认等待时间
    // ...
}

实现 DataSource ,需要知道,连接最终都需要用户名和密码,所以这个接口的两个方法在 UmpooledDataSource 都调用最终的一个方法 doGetConnection(String, String)

public Connection getConnection() throws SQLException {
    return this.doGetConnection(this.username, this.password);
}
public Connection getConnection(String username, String password) throws SQLException {
    return this.doGetConnection(username, password);
}
// 最终走向的方法
private Connection doGetConnection(String username, String password) throws SQLException {
    Properties props = new Properties();
    if (this.driverProperties != null) {
        props.putAll(this.driverProperties);
    }
    if (username != null) {
        props.setProperty("user", username);
    }
    if (password != null) {
        props.setProperty("password", password);
    }
    return this.doGetConnection(props);
}

它将数据库的连接信息也给添加到 Properties 对象中进行存放,并交给下一个 doGetConnection 来处理,继续向下走:

private Connection doGetConnection(Properties properties) throws SQLException {
  	// 若未初始化驱动,需要先初始化,内部维护了一个 Map 来记录初始化信息
    this.initializeDriver();
  	// 传统的获取连接的方式
    Connection connection = DriverManager.getConnection(this.url, properties);
  	// 对连接进行额外的一些配置
    this.configureConnection(connection);
    return connection;
}

可以看到其实最终还是使用了 DriverManager 获取连接,这就是非池化技术,也是获取一个连接对象的流程。在池化技术种表现不同的就是,连接不会马上被销毁,而是被维护在链表中从而实现复用。

# POOLED

该数据源除了非池化配置的属性,还有很多配置属性,这里就不一一列举了。因为维护了一个连接池(其实就是一个集合,说得高大上),所以需要考虑并发问题,考虑如何合理存放大量连接对象,合理分配等。在原始创建连接时,还是依赖的非池化技术:

private final UnpooledDataSource dataSource; // 属性

先看一下实现 DataSource 接口:

public Connection getConnection() throws SQLException {
    return this.popConnection(this.dataSource.getUsername(), this.dataSource.getPassword()).getProxyConnection();
}
public Connection getConnection(String username, String password) throws SQLException {
    return this.popConnection(username, password).getProxyConnection();
}

看到 popProxy ,可以猜测整个连接池有点像栈集合,获取对象进行了一个代理。

popConnection 方法很长,为了内容完整性,我们还是一起来看一下:

private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
  	// 返回的是 PooledConnection 对象,
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    int localBadConnectionCount = 0;
    while(conn == null) {
        synchronized(this.state) {   // 加锁,因为有可能很多个线程都需要获取连接对象
            PoolState var10000;
          	//PoolState 存了两个 List,一个是空闲列表,一个是活跃列表
            if (!this.state.idleConnections.isEmpty()) {   // 有空闲连接时,可以直接分配 Connection
                conn = (PooledConnection)this.state.idleConnections.remove(0);  //ArrayList 中取第一个元素
                if (log.isDebugEnabled()) {
                    log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
                }
              // 如果已经没有多余的连接可以分配,那么就检查一下活跃连接数是否达到最大的分配上限,如果没有,就 new 一个
            } else if (this.state.activeConnections.size() < this.poolMaximumActiveConnections) {
              	// 注意 new 了之后并没有立即往 List 里面塞,只是存了一些基本信息
              	// 我们发现,这里依靠 UnpooledDataSource 创建了一个 Connection 对象,并将其封装到 PooledConnection 中
                conn = new PooledConnection(this.dataSource.getConnection(), this);
                if (log.isDebugEnabled()) {
                    log.debug("Created connection " + conn.getRealHashCode() + ".");
                }
              // 以上条件都不满足,那么只能从之前的连接中寻找了,看看有没有那种卡住的链接
              // 由于网络问题有可能之前的连接一直被卡住,然而正常情况下早就结束并且可以使用了,所以这里相当于是优化也算是一种捡漏的方式
            } else {
              	// 获取最早创建的连接
                PooledConnection oldestActiveConnection = (PooledConnection)this.state.activeConnections.get(0);
                long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
              	// 判断是否超过最大的使用时间
                if (longestCheckoutTime > (long)this.poolMaximumCheckoutTime) {
                  	// 超时统计信息(不重要)
                    ++this.state.claimedOverdueConnectionCount;
                    var10000 = this.state;
                    var10000.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
                    var10000 = this.state;
                    var10000.accumulatedCheckoutTime += longestCheckoutTime;
                  	// 从活跃列表中移除此链接信息
                    this.state.activeConnections.remove(oldestActiveConnection);
                  	// 如果开启事务,还需要回滚一下
                    if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                        try {
                            oldestActiveConnection.getRealConnection().rollback();
                        } catch (SQLException var15) {
                            log.debug("Bad connection. Could not roll back");
                        }
                    }
										
                  	// 这里就根据之前的连接对象直接 new 一个新的连接(注意使用的还是之前的 Connection 对象,只是被重新封装了)
                    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 {
                  // 确实是没得用了,只能卡住了(阻塞)
                  // 然后记录一下有几个线程在等待当前的任务搞完
                    try {
                        if (!countedWait) {
                            ++this.state.hadToWaitCount;
                            countedWait = true;
                        }
                        if (log.isDebugEnabled()) {
                            log.debug("Waiting as long as " + this.poolTimeToWait + " milliseconds for connection.");
                        }
                        long wt = System.currentTimeMillis();
                        this.state.wait((long)this.poolTimeToWait);   // 要是超过等待时间还是没等到,只能放弃
                      	// 注意这样的话 con 就为 null 了
                        var10000 = this.state;
                        var10000.accumulatedWaitTime += System.currentTimeMillis() - wt;
                    } catch (InterruptedException var16) {
                        break;
                    }
                }
            }
						
          	// 经过之前的操作,已经成功分配到连接对象的情况下
            if (conn != null) {
                if (conn.isValid()) {  // 是否有效
                    if (!conn.getRealConnection().getAutoCommit()) {  // 清理之前遗留的事务操作
                        conn.getRealConnection().rollback();
                    }
                    conn.setConnectionTypeCode(this.assembleConnectionTypeCode(this.dataSource.getUrl(), username, password));
                    conn.setCheckoutTimestamp(System.currentTimeMillis());
                    conn.setLastUsedTimestamp(System.currentTimeMillis());
                  	// 添加到活跃表中
                    this.state.activeConnections.add(conn);
                    // 统计信息(不重要)
                    ++this.state.requestCount;
                    var10000 = this.state;
                    var10000.accumulatedRequestTime += System.currentTimeMillis() - t;
                } else {
                  	// 无效的连接,直接抛异常
                    if (log.isDebugEnabled()) {
                        log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
                    }
                    ++this.state.badConnectionCount;
                    ++localBadConnectionCount;
                    conn = null;
                    if (localBadConnectionCount > this.poolMaximumIdleConnections + this.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.");
    } else {
        return conn;
    }
}

面试绝对不会问代码细节,所以这个方法我们看完知道个流程就行了,首先要知道,PoolState 维护两个链表,一个是空闲链表,一个是活跃链表。当我们需要一个连接时:

  1. 先从空闲链表中获取节点
  2. 如果没有空闲的节点,并且活跃连接数(活跃链表节点数)没有达到上限,就 new 一个
  3. 否则就只能到活跃链表最前面拿到最先创建的节点,检查这个节点是否超过最大使用时间,如果可以拿到这个节点,回滚一下这个连接之前的事务,重新封装(不是重新创建)一下,如果拿不到,就会等待阻塞这个节点,超出等待时间,就会记录一些日志,然后 conn=null 结束。
  4. 最后拿到这个 conn ,如果为 null ,就抛出异常;不为 null ,这个 Connection 然后加入到活跃链表中。

关闭这个连接( connection.close() ,在 try 语句中就会自动调用,关闭连接),需要知道 Mybatis 并没有重定义 Connection,** 我们如何知道系统调用了 connetcion.close() 这个方法?**Mybatis 使用的是代理模式,为真正的 Connection 对象创建一个代理对象,代理对象所有的方法都是调用相应的真正 Connection 对象的方法实现。当代理对象执行 close() 方法时,要特殊处理,不调用真正 Connection 对象的 close() 方法,而是将 Connection 对象添加到连接池中。

class PooledConnection implements InvocationHandler {  
   
    //......  
    private static final String CLOSE = "close";
    // 所创建它的 datasource 引用  
    private PooledDataSource dataSource;  
    // 真正的 Connection 对象  
    private Connection realConnection;  
    // 代理自己的代理 Connection  
    private Connection proxyConnection;  
   
    //......  
}

实际上,我们调用 PooledDataSourcegetConnection() 方法 ** 返回的就是这个 proxyConnection 对象。** 当我们调用此 proxyConnection 对象上的任何方法时,都会调用 PooledConnection 对象内 invoke() 方法。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
    String methodName = method.getName();  
    // 当调用关闭的时候,回收此 Connection 到 PooledDataSource 中  
    if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {  
        dataSource.pushConnection(this);  
        return null;  
    } else {  
        try {  
            if (!Object.class.equals(method.getDeclaringClass())) {  
                checkConnection();  
            }  
            return method.invoke(realConnection, args);  
        } catch (Throwable t) {  
            throw ExceptionUtil.unwrapThrowable(t);  
        }  
    }  
}

我只能感慨,反射学的好,那是真 nb

# 一些问题

  • 何时创建 Connection:我们执行 sql 语句是通过 sqlSession ,这时候 Mybatis 才会调用 DataSource 对象创建连接。
  • UnpooledDataSource 会做的事:先判断 driver 驱动是否加载到内存中,实例化 Driver 对象,使用 DriverManager.registerDriver () 方法将其注册到内存中,以供后续使用。之后的内容就熟悉了,本质会调用 DriverManager.getConnection() 方法获取连接,配置后返回。

# 参考

https://pdai.tech/md/framework/orm-mybatis/mybatis-y-datasource.html

https://www.yuque.com/qingkongxiaguang/spring/rlgcf7#f8ccf35e

部分图片来源:https://blog.csdn.net/luanlouis