MyBatis数据源模块源码分析
数据源对象是比较复杂的对象,其创建过程相对比较复杂,对于 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
接口拥有UnpooledDataSourceFactory
和PooledDataSourceFactory
两个实现类,UnpooledDataSourceFactory
是未使用池化技术的数据源工厂类,PooledDataSourceFactory
是使用了池化技术的数据源工厂类。它们两分别用于创建UnpooledDataSource
和PooledDataSource
,它们都实现了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
获取连接时会在池中去拿,当用户调用Connection
的close()
方法时就会将该连接归还至连接池。而PooledDataSource
实现上述功能需要借助另外两个类来实现:
PoolState
:用于保存线程池的相关状态。PooledConnection
:Connection
的加强类,用于加强原生close
等方法,从而实现数据库连接的复用。
3.1 PoolState
在PoolState最核心的的是idleConnections
和activeConnections
,他们分别是存储空闲连接和非空闲连接的集合(在后文中出于习惯考虑,将其描述为空闲队列和活动队列)。
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数据源模块源码分析的更多相关文章
- nginx健康检查模块源码分析
nginx健康检查模块 本文所说的nginx健康检查模块是指nginx_upstream_check_module模块.nginx_upstream_check_module模块是Taobao定制的用 ...
- Spark Scheduler模块源码分析之TaskScheduler和SchedulerBackend
本文是Scheduler模块源码分析的第二篇,第一篇Spark Scheduler模块源码分析之DAGScheduler主要分析了DAGScheduler.本文接下来结合Spark-1.6.0的源码继 ...
- Spark Scheduler模块源码分析之DAGScheduler
本文主要结合Spark-1.6.0的源码,对Spark中任务调度模块的执行过程进行分析.Spark Application在遇到Action操作时才会真正的提交任务并进行计算.这时Spark会根据Ac ...
- Zepto事件模块源码分析
Zepto事件模块源码分析 一.保存事件数据的handlers 我们知道js原生api中要移除事件,需要传入绑定时的回调函数.而Zepto则可以不传入回调函数,直接移除对应类型的所有事件.原因就在于Z ...
- Django(51)drf渲染模块源码分析
前言 渲染模块的原理和解析模块是一样,drf默认的渲染有2种方式,一种是json格式,另一种是模板方式. 渲染模块源码入口 入口:APIView类中dispatch方法中的:self.response ...
- Springboot中mybatis执行逻辑源码分析
Springboot中mybatis执行逻辑源码分析 在上一篇springboot整合mybatis源码分析已经讲了我们的Mapper接口,userMapper是通过MapperProxy实现的一个动 ...
- MyBatis 之 SqlSessionManager 源码分析
MyBatis 的 4 个基本构成: SqlSessionFactoryBuilder(构造器): 根据配置信息或者代码来生成 SqlSessionFactory(工厂接口) SqlSessionFa ...
- Django(48)drf请求模块源码分析
前言 APIView中的dispatch是整个请求生命过程的核心方法,包含了请求模块,权限验证,异常模块和响应模块,我们先来介绍请求模块 请求模块:request对象 源码入口 APIView类中di ...
- Django(49)drf解析模块源码分析
前言 上一篇分析了请求模块的源码,如下: def initialize_request(self, request, *args, **kwargs): """ Retu ...
- Django(50)drf异常模块源码分析
异常模块源码入口 APIView类中dispatch方法中的:response = self.handle_exception(exc) 源码分析 我们点击handle_exception跳转,查看该 ...
随机推荐
- Zookeeper+dubbo学习笔记
举个例子 你需要打车 那么会使用滴滴软件,而滴滴软件就类似于Zookeeper, 你和汽车司机都需要在滴滴公司注册(留下联系方式方便联系), 当你需要打车时Zookeeper会帮你找到你需要的汽车司 ...
- Prometheus之自定义标签
前言: 我们一般通过grafana导入Dashboard模板用来展示数据,但是有时候需要自己定义展示项目,这时需要自己在Prometheus重新自定义标签,并在grafana进行应用. 1.Prome ...
- Linux-主机之间创建免密
一.四台主机进行免密 192.168.10.6 192.168.10.11 192.168.10.12 192.168.10.13 二.192.168.10.6主机生成密钥对,并将公钥传输到其它所有主 ...
- k8s 深入篇———— k8s 的本质[四]
前言 简单整理一下k8s的本质. 正文 首先,Kubernetes 项目要解决的问题是什么? 编排?调度?容器云?还是集群管理? 实际上,这个问题到目前为止都没有固定的答案.因为在不同的发展阶段,Ku ...
- css block,inline和inline-block概念和区别
总体概念 block和inline这两个概念是简略的说法,完整确切的说应该是 block-level elements (块级元素) 和 inline elements (内联元素).block元素通 ...
- Matlab绘图(2)通过代码进行局部放大绘图、多文件绘图
Matlab进阶绘图 在这次的绘图练习中,我们需要考虑一次性将所有数据文件逐一读入,然后对每幅图图片进行放大处理. 参数设置 这里包括每幅图的标题,图例,读入文件的名称,等等 title_d = {' ...
- 我们为什么需要操作系统(Operating System)?
我们为什么需要操作系统(Operating System)? a) 从计算机体系的角度,OS向下统筹了所有硬件资源(1),向上为所有软件提供API调用(2),使得软件程序员不必知晓硬件的具体细节,实现 ...
- 【Oracle】在PL/SQL中使用sql实现插入排序
[Oracle]在PL/SQL中使用sql实现插入排序 一般来说,SQL要排序的话直接使用order by即可 不一般来说,就是瞎搞,正好也可以巩固自己的数据结构基础,主要也发现没有人用SQL去实现这 ...
- 极致体验!基于阿里云 Serverless 快速部署 Function
简介: 云计算的不断发展,涌现出很多改变传统 IT 架构和运维方式的新技术,而以虚拟机.容器.微服务为代表的技术更是在各个层面不断提升云服务的技术能力,它们将应用和环境中很多通用能力变成了一种服务.但 ...
- 【ESSD技术解读-01】 云原生时代,阿里云 ESSD 快照服务 助力企业级数据保护
简介:本文以云原生为时代背景,介绍了阿里云块存储快照服务如何基于高性能 ESSD 云盘提升快照服务性能,提供轻量.实时的用户体验及揭秘背后的技术原理.依据行业发展及云上数据保护场景,为企业用户及备份 ...