数据源对象是比较复杂的对象,其创建过程相对比较复杂,对于 MyBatis 创建数据源,具体来讲有如下难点:

  • MyBatis 不但要能集成第三方的数据源组件,自身也提供了数据源的实现;
  • 数据源的初始化参数较多,比较复杂;

在MyBatis中使用了工厂模式来实现数据源的创建,使用代理模式来帮助实现自己的数据源。

一 . MyBatis数据源模块类结构

MyBatis数据源模块的代码全部位于org.apache.ibatis.datasource包下:

数据源模块类主要结构如下(图片来自于:https://blog.csdn.net/Zzzzz_xh/article/details/100531968):

DataSourceFactory是工厂的抽象接口:

/**
* 数据源工厂类的抽象接口,它有两个实现类:UnpooledDataSourceFactory、PooledDataSourceFactory
* @author Clinton Begin
*/
public interface DataSourceFactory { /**
* 设置数据源属性
* @param props
*/
void setProperties(Properties props); /**
* 获取数据源
* @return
*/
DataSource getDataSource(); }

DataSourceFactory接口拥有UnpooledDataSourceFactoryPooledDataSourceFactory两个实现类,UnpooledDataSourceFactory是未使用池化技术的数据源工厂类,PooledDataSourceFactory是使用了池化技术的数据源工厂类。它们两分别用于创建UnpooledDataSourcePooledDataSource,它们都实现了javax.sql.DataSource JDBC提供的数据源标准,其中PooledDataSource就是MyBatis自己实现的数据库连接池。

二. UnpooledDataSource源码分析

在JDK中官方定义了一个数据源接口,市面上所有第三方连接池(数据源)都应该实现这个接口:

public interface DataSource  extends CommonDataSource, Wrapper {

  Connection getConnection() throws SQLException;

  Connection getConnection(String username, String password)
throws SQLException;
}

UnpooledDataSource实际上是一个未使用池化技术的数据源,它实现了javax.sql.DataSource数据源标准(JDBC规定),但是在内部调用getConnection()方法时是通过创建Connection对象来实现的,由于每一次获取都创建新的连接对象,连接对象并没有进行复用,效率较低。其中UnpooledDataSource源码如下(只保留关键代码):

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;
private Integer defaultTransactionIsolationLevel;
private Integer defaultNetworkTimeout; @Override
public Connection getConnection() throws SQLException {
return doGetConnection(username, password);
} @Override
public Connection getConnection(String username, String password) throws SQLException {
return doGetConnection(username, password);
} private Connection doGetConnection(Properties properties) throws SQLException {
//初始化驱动
initializeDriver();
//重点在这里,连接对象使用过 DriverManager.getConnection创建的新的连接对象
Connection connection = DriverManager.getConnection(url, properties);
//配置连接
configureConnection(connection);
return connection;
}
...
}

三. PooledDataSource源码分析

PooledDataSource是Mybatis自己实现的数据库连接池,在分析它的源码之前我们首先要清楚作为一个连接池需要实现哪些功能。

作为一个数据库连接池,其最核心的功能是要做到Connection的复用,当用户调用连接池的getConnection获取连接时会在池中去拿,当用户调用Connectionclose()方法时就会将该连接归还至连接池。而PooledDataSource实现上述功能需要借助另外两个类来实现:

  • PoolState:用于保存线程池的相关状态。
  • PooledConnectionConnection的加强类,用于加强原生close等方法,从而实现数据库连接的复用。

3.1 PoolState

在PoolState最核心的的是idleConnectionsactiveConnections,他们分别是存储空闲连接和非空闲连接的集合(在后文中出于习惯考虑,将其描述为空闲队列和活动队列)。

public class PoolState {

  protected PooledDataSource dataSource;

  //空闲连接队列
protected final List<PooledConnection> idleConnections = new ArrayList<>();
//活动队列
protected final List<PooledConnection> activeConnections = new ArrayList<>();
//请求的次数
protected long requestCount = 0;
//累计获得连接的时长
protected long accumulatedRequestTime = 0;
//累计使用连接的时间。从连接取出到归还,算一次使用时间
protected long accumulatedCheckoutTime = 0;
//使用连接超时的次数
protected long claimedOverdueConnectionCount = 0;
//累计超时时间
protected long accumulatedCheckoutTimeOfOverdueConnections = 0;
//累计等待时间
protected long accumulatedWaitTime = 0;
//等待次数
protected long hadToWaitCount = 0;
//无效的连接次数
protected long badConnectionCount = 0; public PoolState(PooledDataSource dataSource) {
this.dataSource = dataSource;
} ... }

3.2 PooledDataSource

PooledDataSource最主要是要理解getConnection方法获取连接对象的逻辑,这里给出该方法的执行流程图:

PooledDataSource类获取连接时的核心源码如下:

public class PooledDataSource implements DataSource {
//Log是是适配器模式的抽象接口
private static final Log log = LogFactory.getLog(PooledDataSource.class); //线程池的相关状态
private final PoolState state = new PoolState(this); //没有池化的数据源
private final UnpooledDataSource dataSource; // OPTIONAL CONFIGURATION FIELDS
//在任意时间可存在的活动(正在使用)连接数量,默认值:10
protected int poolMaximumActiveConnections = 10;
//任意时间可能存在的空闲连接数,默认是5
protected int poolMaximumIdleConnections = 5;
//在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000 毫秒(即 20 秒)
protected int poolMaximumCheckoutTime = 20000;
//这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)。
protected int poolTimeToWait = 20000;
//这是一个关于坏连接容忍度的底层设置, 作用于每一个尝试从缓存池获取连接的线程。 如果这个线程获取到的是一个坏的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过
// poolMaximumIdleConnections 与 poolMaximumLocalBadConnectionTolerance 之和。 默认值:3(新增于 3.4.5)
protected int poolMaximumLocalBadConnectionTolerance = 3;
// 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求。默认是“NO PING QUERY SET”,这会导致多数数据库驱动出错时返回恰当的错误消息。
protected String poolPingQuery = "NO PING QUERY SET";
//是否启用侦测查询。若开启,需要设置 poolPingQuery 属性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句),默认值:false。
protected boolean poolPingEnabled;
// 配置 poolPingQuery 的频率。可以被设置为和数据库连接超时时间一样,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)。
protected int poolPingConnectionsNotUsedFor; //根据数据库URL、用户名、密码生成一个Hash值,唯一标识一个连接池,由这个连接池产生的连接对象都会带上这个值
private int expectedConnectionTypeCode; @Override
public Connection getConnection() throws SQLException {
return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
} @Override
public Connection getConnection(String username, String password) throws SQLException {
return popConnection(username, password).getProxyConnection();
} private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();
//此次获取任务中,获取到失效连接的次数
int localBadConnectionCount = 0; //最外面是while死循环,如果一直拿不到connection,则不断尝试
while (conn == null) {
//使用state加锁,也就是说下面代码对state的操作都是线程安全的
synchronized (state) {
if (!state.idleConnections.isEmpty()) {
//连接池中拥有空闲连接
//拿出空闲队列中的第一个连接
conn = state.idleConnections.remove(0);
//如果是Debu级别,则输出日志
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {
/**
* 连接池中没有空闲连接
*/
if (state.activeConnections.size() < poolMaximumActiveConnections) {
//如果当前活动的线程小于所约定的最大活动线程数,则创建一个连接
//创建代理对象
conn = new PooledConnection(dataSource.getConnection(), this);
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else {
// 如果连接池中活动连接数到达极限,则不能创建连接
// 拿去activeConnections队列中最老的连接对象
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
// 获取此连接已取出的时间
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// 如果此连接从连接池中获取出来的时间超过限制的最大时间
//将过期的连接数+1
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 happened.
Wrap the bad connection with a new PooledConnection, this will help
to not interrupt current executing thread and give current thread a
chance to join the next competition 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");
}
}
//重新封装一个新的代理连接对象(这里有点疑问?新代理对象与老代理对象(PooledConnection)共用目标对象(Connection)不会带来线程安全问题吗?)
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
//设置创建时间
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
//设置最后使用的时间
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
//将超时的代理连接对象(PooledConnection)作废
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
// 如果此连接从连接池中获取出来的时间没有超过限制的最大时间,则必须等待
try {
if (!countedWait) {
//等待数量+1
state.hadToWaitCount++;
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
//当前线程释放掉state锁,等待poolTimeToWait
state.wait(poolTimeToWait);
//计算累计等待时间
state.accumulatedWaitTime += System.currentTimeMillis() - wt;
} catch (InterruptedException e) {
//如果捕获到中断异常则跳出循环
break;
}
}
}
}
if (conn != null) {
//如果已经拿到连接对象了
if (conn.isValid()) {
//connection是有效的
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
//设置当前connection对象的TypeCode,TypeCode是URL+用户名+密码的HashCode,目的是在归还的时候判断,当前连接的参数是否与数据源相同
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 {
//connection是无效的
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
//整个数据源累计获取到无效连接的次数+1
state.badConnectionCount++;
//此次获取任务中 获取失效连接的次数+1
localBadConnectionCount++;
conn = null;
if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
//此次连接获取任务中 获取失效连接的次数 大于 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) {
//如果经过上面一系列的操作还没有获取到对象,则抛出SQLException
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;
} }

3.3 PooledConnection

PooledDataSource中我们理解了Connection获取流程,当用户调用close方法时需要将该对象归还至数据库,而这一功能需要通过PooledConnection类来实现:

class PooledConnection implements InvocationHandler {

  private static final String CLOSE = "close";
private static final Class<?>[] IFACES = new Class<?>[] { Connection.class }; private final int hashCode;
//当前连接所属的数据源,最后会归还至该数据源
private final PooledDataSource dataSource;
//真正的连接对象
private final Connection realConnection;
//代理的连接对象
private final Connection proxyConnection;
//从数据源中取出来的时间戳
private long checkoutTimestamp;
//连接创建的时间戳
private long createdTimestamp;
//连接最后一次使用的时间戳
private long lastUsedTimestamp;
//根据数据库URL、用户名、密码生成一个Hash值,唯一标识一个连接池
private int connectionTypeCode;
//连接是否有效
private boolean valid; ...
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (CLOSE.equals(methodName)) {
//如果用户调用的是close方法,则将调用PooledDataSource中的pushConnection方法
dataSource.pushConnection(this);
return null;
}
try {
if (!Object.class.equals(method.getDeclaringClass())) {
/**
* 如果调用的不是Object的方法,则检查该连接是否有效,如果无效则抛出异常,
* 这也是PooledDataSource.getConnection当没有空闲连接,将超时连接作废机制的关键
*/
// issue #579 toString() should never fail
// throw an SQLException instead of a Runtime
checkConnection();
}
//调用被代理的Connection中的方法
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
} } private void checkConnection() throws SQLException {
if (!valid) {
throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
}
} }

可以看到通过invoke方法的加强,当用户调用close()方法时会通过PooledDataSource中的pushConnection()方法归还连接,连接归还的流程图如下:

pushConnection方法源码如下:

/**
* 将连接对象归还给连接池(实际上是将连接从active队列中移到idle队列中)
*
* @param conn
* @throws SQLException
*/
protected void pushConnection(PooledConnection conn) throws SQLException { synchronized (state) {
//将当前连接从active队列中移除
state.activeConnections.remove(conn);
if (conn.isValid()) {
//连接是有效的
if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
//空闲的数量小于最大空闲值 且 连接对象的typeCode与数据源期望的TypeCode相同
//记录当前连接使用的时间
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
//如果连接对象的事务是非自动提交的,则回滚事务
conn.getRealConnection().rollback();
}
//重新封装一个新的代理连接对象
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
//将新建的代理连接对象放入空闲队列
state.idleConnections.add(newConn);
//设置创建的时间戳
newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
//设置最后使用的时间戳
newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
//老的代理对象作废
conn.invalidate();
if (log.isDebugEnabled()) {
log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
}
//换新在state锁上的等待的线程
state.notifyAll();
} else {
//空闲的数量大于等于最大空闲值 或者 连接对象的typeCode与数据源期望的TypeCode不相同
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
//关闭这个连接
conn.getRealConnection().close();
if (log.isDebugEnabled()) {
log.debug("Closed connection " + conn.getRealHashCode() + ".");
}
//将代理连接作废
conn.invalidate();
}
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
}
state.badConnectionCount++;
}
}
}

详细代码注释请移步至:https://github.com/tianjindong/mybatis-source-annotation

MyBatis数据源模块源码分析的更多相关文章

  1. nginx健康检查模块源码分析

    nginx健康检查模块 本文所说的nginx健康检查模块是指nginx_upstream_check_module模块.nginx_upstream_check_module模块是Taobao定制的用 ...

  2. Spark Scheduler模块源码分析之TaskScheduler和SchedulerBackend

    本文是Scheduler模块源码分析的第二篇,第一篇Spark Scheduler模块源码分析之DAGScheduler主要分析了DAGScheduler.本文接下来结合Spark-1.6.0的源码继 ...

  3. Spark Scheduler模块源码分析之DAGScheduler

    本文主要结合Spark-1.6.0的源码,对Spark中任务调度模块的执行过程进行分析.Spark Application在遇到Action操作时才会真正的提交任务并进行计算.这时Spark会根据Ac ...

  4. Zepto事件模块源码分析

    Zepto事件模块源码分析 一.保存事件数据的handlers 我们知道js原生api中要移除事件,需要传入绑定时的回调函数.而Zepto则可以不传入回调函数,直接移除对应类型的所有事件.原因就在于Z ...

  5. Django(51)drf渲染模块源码分析

    前言 渲染模块的原理和解析模块是一样,drf默认的渲染有2种方式,一种是json格式,另一种是模板方式. 渲染模块源码入口 入口:APIView类中dispatch方法中的:self.response ...

  6. Springboot中mybatis执行逻辑源码分析

    Springboot中mybatis执行逻辑源码分析 在上一篇springboot整合mybatis源码分析已经讲了我们的Mapper接口,userMapper是通过MapperProxy实现的一个动 ...

  7. MyBatis 之 SqlSessionManager 源码分析

    MyBatis 的 4 个基本构成: SqlSessionFactoryBuilder(构造器): 根据配置信息或者代码来生成 SqlSessionFactory(工厂接口) SqlSessionFa ...

  8. Django(48)drf请求模块源码分析

    前言 APIView中的dispatch是整个请求生命过程的核心方法,包含了请求模块,权限验证,异常模块和响应模块,我们先来介绍请求模块 请求模块:request对象 源码入口 APIView类中di ...

  9. Django(49)drf解析模块源码分析

    前言 上一篇分析了请求模块的源码,如下: def initialize_request(self, request, *args, **kwargs): """ Retu ...

  10. Django(50)drf异常模块源码分析

    异常模块源码入口 APIView类中dispatch方法中的:response = self.handle_exception(exc) 源码分析 我们点击handle_exception跳转,查看该 ...

随机推荐

  1. 使用GUI--tkinter 制作一个批量修改文件名的桌面软件

    ''' title:批量修改文件名称 author:huyang createtime:2021-01-29 14:50:00 ''' from tkinter import * from tkint ...

  2. Push failed idea将项目发布到gitHub失败

    此时需要点击VCS --> inport into version..-->create git ...重新生成仓库

  3. 应用可靠性与性能不给力?HarmonyOS HiViewDFX了解一下

    原文链接:https://mp.weixin.qq.com/s/Y44jUEB3ttlijbMDPrBcNg,点击链接查看更多技术内容:   作为基础软件服务子系统的HarmonyOS HiViewD ...

  4. vue项目node-scss装不上问题( vue执行npm install报错: Can‘t find Python executable “python“, you can set the PYTHON env variable

    一.描述从网上下载的一个Vue模板项目,导入VsCode,执行npm install命令后,报错了,报错的信息是node-sass安装失败,同时提示需要python环境的错误信息,这是因为安装node ...

  5. MySQL 分析查询与来源机器

    当前分析针对版本:MariaDB 10.5 线上出现报错:can't create more than max_prepared_stmt_count statements.造成这个错误的直接原因就是 ...

  6. log4j2 lookup漏洞修复方法

    2021.12.10凌晨,Apache Log4j远程代码执行漏洞细节被公开,参考链接:https://unit42.paloaltonetworks.com/apache-log4j-vulnera ...

  7. ERP财务管理有哪些功能?如何选择合适的ERP软件开发商定制开发适合自己的ERP财务管理?

    企业日常运营中,分工明确.结构清晰的财务管理非常重要,因此在完整的ERP解决方案中,财务管理是不可或缺的部分,甚至财务管理是整个ERP解决方案的核心,其它功能模块都围绕着财务管理构建价值链创造流程,最 ...

  8. Oracle ORA-12725 unmatched parentheses in regular expression

    Oracle ORA-12725 unmatched parentheses in regular expression 简单来说就是正则表达式中的括号问题 这种一般就可以锁定使用正则的函数,例如 r ...

  9. 02_Vue模板语法

    Vue模板语法有2大类:         1.插值语法:           功能:用于解析标签体内容.           写法:{{xxx}},xxx是js的表达式,且可以直接读取到data中的所 ...

  10. 基于 EventBridge 构建 SaaS 应用集成方案

    ​简介:事件源是事件驱动的基石,如何获取更多事件源也是 EventBridge 一直在探索和尝试的方向.针对市场上其他云厂商和垂直领域的 Saas 服务,EventBridge 发布了 HTTP So ...