MyBatis源码分析(5)——内置DataSource实现

时间:2023-02-20 18:16:44

@(MyBatis)[DataSource]

MyBatis源码分析(5)——内置DataSource实现

MyBatis内置了两个DataSource的实现:UnpooledDataSource,该数据源对于每次获取请求都简单的打开和关闭连接。PooledDataSource,该数据源在Unpooled的基础上构建了连接池。

UnpooledDataSource

配置

UNPOOLED数据源只有5个属性需要配置:

driver:JDBC具体数据库驱动
url:JDBC连接
username:用户名
password:密码
defaultTransactionIsolationLevel:默认事务隔离级别

除了以上属性外,还可以配置驱动的连接信息,但是需要加前缀driver.,如:driver.encoding=UTF8,会将该配置作为参数传入driverManager.getConnection。

如下:

<dataSource type="UNPOOLED">
<property name="driver" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>

实现

UnpooledDataSource,内部通过调用DriverManager来实现,DriverManager是用于管理低层驱动以及创建连接的。

public class UnpooledDataSource implements DataSource {

  private ClassLoader driverClassLoader;
// 驱动连接属性
private Properties driverProperties;
// 所有已注册的驱动,仅仅用于识别驱动在DriverManager中是否已经被加载进来了
private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<String, Driver>(); // 当前使用的驱动
private String driver;
private String url;
private String username;
private String password; private Boolean autoCommit;
// 默认事务隔离级别
private Integer defaultTransactionIsolationLevel; static {
// 静态代码块,当类加载的时候,就从DriverManager中获取所有的驱动信息,放到当前维护的Map中
Enumeration<Driver> drivers = DriverManager.getDrivers();
while (drivers.hasMoreElements()) {
Driver driver = drivers.nextElement();
registeredDrivers.put(driver.getClass().getName(), driver);
}
} // 省略了部分代码... public Connection getConnection() throws SQLException {
// 获取数据库连接
return doGetConnection(username, password);
} public Connection getConnection(String username, String password) throws SQLException {
return doGetConnection(username, password);
} private Connection doGetConnection(String username, String password) throws SQLException {
// 这里通过url加Properties来获取连接,是因为可以在配置文件中配置数据库连接的信息,比如编码之类的
Properties props = new Properties();
if (driverProperties != null) {
props.putAll(driverProperties);
}
if (username != null) {
props.setProperty("user", username);
}
if (password != null) {
props.setProperty("password", password);
}
return doGetConnection(props);
} private Connection doGetConnection(Properties properties) throws SQLException {
// 初始化驱动信息
initializeDriver();
// 从DriverManager中获取数据库连接
Connection connection = DriverManager.getConnection(url, properties);
// 配置连接信息,自动提交以及事务隔离级别
configureConnection(connection);
return connection;
} private synchronized void initializeDriver() throws SQLException {
// 如果没有包含在已注册Map中,则需要将该驱动加载进来
if (!registeredDrivers.containsKey(driver)) {
Class<?> driverType;
try {
// 加载数据库连接驱动
if (driverClassLoader != null) {
driverType = Class.forName(driver, true, driverClassLoader);
} else {
// Resources为MyBatis内置的资源工具类,该方法依次尝试从多个ClassLoader中获取Class类,顺序为:配置的classLoader,默认的defaultClassLoader,当前线程的getContextClassLoader,当前类的getClass().getClassLoader(),系统的systemClassLoader
driverType = Resources.classForName(driver);
}
// 创建驱动实例
Driver driverInstance = (Driver)driverType.newInstance();
// 注册到DriverManager中,用于创建数据库连接
DriverManager.registerDriver(new DriverProxy(driverInstance));
// 放到已注册Map中
registeredDrivers.put(driver, driverInstance);
} catch (Exception e) {
throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
}
}
} private void configureConnection(Connection conn) throws SQLException {
if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
// 如果已开启了事务,则可以将自动提交关闭
conn.setAutoCommit(autoCommit);
}
if (defaultTransactionIsolationLevel != null) {
//设置事务隔离级别
conn.setTransactionIsolation(defaultTransactionIsolationLevel);
}
}
}

PooledDataSource

配置

除了UNPOOLED的配置外,还可以配置其它的一些属性,如下:

poolMaximumActiveConnections:最大活动连接数(默认为10)
poolMaximumIdleConnections:最大空闲连接数(默认为5)
poolMaximumCheckoutTime:最大可回收时间,即当达到最大活动链接数时,此时如果有程序获取连接,则检查最先使用的连接,看其是否超出了该时间,如果超出了该时间,则可以回收该连接。(默认20s)
poolTimeToWait:没有连接时,重尝试获取连接以及打印日志的时间间隔(默认20s)
poolPingQuery:检查连接正确的语句,默认为"NO PING QUERY SET",即没有,使用会导致抛异常
poolPingEnabled:是否开启ping检测,(默认:false)
poolPingConnectionsNotUsedFor:设置ping检测时间间隔,通常用于检测超时连接(默认为0,即当开启检测后每次从连接词中获取连接以及放回连接池都需要检测)

示例配置如下:

<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="poolMaximumActiveConnections" value="20" />
<property name="poolMaximumIdleConnections" value="10" />
<property name="poolMaximumCheckoutTime" value="15" />
<property name="poolTimeToWait" value="10" />
<property name="poolPingQuery" value="select 1 from dual" />
<property name="poolPingEnabled" value="true" />
<property name="poolPingConnectionsNotUsedFor" value="0" />
</dataSource>

当日志开启DEBUG输出,使用Mapper操作时,可以看到:

2016-07-31 21:30:45 [DEBUG]-[Thread: main]-[org.apache.ibatis.datasource.pooled.PooledDataSource.popConnection()]:
Created connection 395084181. 2016-07-31 21:30:45 [DEBUG]-[Thread: main]-[org.apache.ibatis.datasource.pooled.PooledDataSource.pingConnection()]:
Testing connection 395084181 ... 2016-07-31 21:30:45 [DEBUG]-[Thread: main]-[org.apache.ibatis.datasource.pooled.PooledDataSource.pingConnection()]:
Connection 395084181 is GOOD!

实现

连接池的实现,核心的思想在于,将连接缓存起来,即可以通过两个链表(/队列)来实现,一个用于维持活动连接,另一个维持空闲连接。还有另外一点就是关闭连接后返回给连接池或者释放掉,这里可以通过代理来实现,当客户端调用close时,并不是直接关闭连接,而是将其缓存起来,放到空闲链表中。这里使用代理还有个优点,每次释放的时候,重新创建新的代理连接来封装,并且将原有的代理设置为无效,可以使得程序即使持有原有的代理,也不会影响到回收的连接。

在MyBatis中,主要是通过PoolState来维护连接状态,以及通过PooledConnection代理来实现归还连接操作。


PooledConnection

这里主要是通过Java Proxy代理来实现的。

// 实现代理接口
class PooledConnection implements InvocationHandler { private static final String CLOSE = "close";
private static final Class<?>[] IFACES = new Class<?>[] { Connection.class }; // 省略了部分代码... private PooledDataSource dataSource;
private Connection realConnection;
private Connection proxyConnection; public PooledConnection(Connection connection, PooledDataSource dataSource) {
this.hashCode = connection.hashCode();
this.realConnection = connection;
this.dataSource = dataSource;
this.createdTimestamp = System.currentTimeMillis();
this.lastUsedTimestamp = System.currentTimeMillis();
this.valid = true;
// 创建代理
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
} // 代理实现Connection接口的所有方法,只对CLOSE方法特别处理
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
// 如果为CLOSE方法,那么就将其放回连接池中
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);
}
}
}
}

PoolState

连接池的状态,用于维护活动连接,空闲连接,以及统计一些连接信息。

public class PoolState {

  protected PooledDataSource dataSource;

  // 空闲连接
protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>();
// 当前活动连接
protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>();
// 请求次数
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;
} // 省略了部分Getter方法...
}

PooledDataSource获取连接

连接池主要是通过popConnection来实现连接的创建以及分配的,过程不复杂,当有空闲连接时则直接使用,否则再根据是否达到了设置的峰值,来决定是否需要创建新的连接等。

流程图如下:

MyBatis源码分析(5)——内置DataSource实现

popConnection实现:
  private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();
int localBadConnectionCount = 0; while (conn == null) {
// 加锁访问
synchronized (state) {
if (state.idleConnections.size() > 0) {
// 有空闲连接,直接获取
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {
// 空闲连接不足
if (state.activeConnections.size() < poolMaximumActiveConnections) {
// 小于最大活动连接数,直接建立新的连接,并封装代理
conn = new PooledConnection(dataSource.getConnection(), this);
Connection realConn = conn.getRealConnection();
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else {
// 超出最大活动连接数,不能创建连接
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
// 获取使用时间最长的活动连接,并计算使用的时间
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// 超出了最大可回收时间,直接回收该连接,回收过期次数增加
state.claimedOverdueConnectionCount++;
// 统计过期回收时间增加
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
// 统计使用时间增加
state.accumulatedCheckoutTime += longestCheckoutTime;
// 将连接从活动队列中移除
state.activeConnections.remove(oldestActiveConnection);
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
// 如果不是自动提交事务,则将其回滚,因为可能存在一些操作
oldestActiveConnection.getRealConnection().rollback();
}
// 使用新的代理封装,可以使得不会被原有的影响
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
// 将之前的代理设置为无效
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
// Must wait
try {
if (!countedWait) {
// 增加获取连接需要等待的次数
state.hadToWaitCount++;
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
// 等待
state.wait(poolTimeToWait);
// 增加获取连接的等待时间
state.accumulatedWaitTime += System.currentTimeMillis() - wt;
} catch (InterruptedException e) {
// 被中断,退出尝试以及等待
break;
}
}
}
}
if (conn != null) {
if (conn.isValid()) { if (!conn.getRealConnection().getAutoCommit()) {
// 连接为非自动提交事务,则将其回滚,可能存在一些未提交操作,并且防止影响下一次使用
conn.getRealConnection().rollback();
}
// 根据URL,用户名以及密码计算出一个Hash,用于标识此次连接
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 {
// 无效连接
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
// 统计无效连接个数
state.badConnectionCount++;
localBadConnectionCount++;
conn = null;
if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {
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.");
}
}
}
} } // 从上面的循环退出,如果为null,则一定出现异常情况了
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.");
} return conn;
}

PooledDataSource回收连接

通过使用代理,就可以在用户调用Close关闭数据库连接的时候,根据当前状态来决定放入空闲链表中还是释放掉。

流程图如下:

MyBatis源码分析(5)——内置DataSource实现

pushConnection实现:
  // 省略了部分日志代码
protected void pushConnection(PooledConnection conn) throws SQLException {
synchronized (state) {
// 从活动链表中移除当前连接
state.activeConnections.remove(conn);
if (conn.isValid()) {
// 当前连接有效的话判断是否达到了最大空闲连接数,以及当前的连接是否变更过(即用户名,密码,Url等变更)
if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
// 统计使用连接时长
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();
// 通知等待获取连接的线程(不去判断是否真的有线程在等待)
state.notifyAll();
} else {
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
// 超出空闲连接限制,则直接释放当前连接
conn.getRealConnection().close();
// 将原有的代理连接设置为无效
conn.invalidate();
}
} else {
// 连接无效,则统计无效连接个数
state.badConnectionCount++;
}
}
}

连接池调优

// 经验不足,后续补充

MyBatis DataSource集成

// 后续看完Spring-MyBatis再补充

参考

  1. MyBatis官方文档

MyBatis源码分析(5)——内置DataSource实现的更多相关文章

  1. 10&period;源码分析---SOFARPC内置链路追踪SOFATRACER是怎么做的?

    SOFARPC源码解析系列: 1. 源码分析---SOFARPC可扩展的机制SPI 2. 源码分析---SOFARPC客户端服务引用 3. 源码分析---SOFARPC客户端服务调用 4. 源码分析- ...

  2. MyBatis 源码分析 - 内置数据源

    1.简介 本篇文章将向大家介绍 MyBatis 内置数据源的实现逻辑.搞懂这些数据源的实现,可使大家对数据源有更深入的认识.同时在配置这些数据源时,也会更清楚每种属性的意义和用途.因此,如果大家想知其 ...

  3. MyBatis 源码分析 - 配置文件解析过程

    * 本文速览 由于本篇文章篇幅比较大,所以这里拿出一节对本文进行快速概括.本篇文章对 MyBatis 配置文件中常用配置的解析过程进行了较为详细的介绍和分析,包括但不限于settings,typeAl ...

  4. MyBatis 源码分析系列文章导读

    1.本文速览 本篇文章是我为接下来的 MyBatis 源码分析系列文章写的一个导读文章.本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说 ...

  5. MyBatis 源码分析系列文章合集

    1.简介 我从七月份开始阅读MyBatis源码,并在随后的40天内陆续更新了7篇文章.起初,我只是打算通过博客的形式进行分享.但在写作的过程中,发现要分析的代码太多,以至于文章篇幅特别大.在这7篇文章 ...

  6. MyBatis 源码分析 - 插件机制

    1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 My ...

  7. MyBatis 源码分析 - 缓存原理

    1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 Redis 或 memcached 等缓存中间件,拦截大量奔向数据库的请求,减轻数据库压力.作为一个重要的组件,MyBatis 自然 ...

  8. MyBatis 源码分析 - 映射文件解析过程

    1.简介 在上一篇文章中,我详细分析了 MyBatis 配置文件的解析过程.由于上一篇文章的篇幅比较大,加之映射文件解析过程也比较复杂的原因.所以我将映射文件解析过程的分析内容从上一篇文章中抽取出来, ...

  9. 精尽MyBatis源码分析 - MyBatis-Spring 源码分析

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

随机推荐

  1. &lbrack;资料分享&rsqb;组件方式开发 Web App全站

  2. NPOI 读写Excel

    实例功能概述: 1.支持Excel2003以及2007 2.支持Excel读取到DataTable(TableToExcel) 3.支持DataTable导出到Excel(TableToExcel) ...

  3. MAC下Android的Eclipse开发环境的搭建

    一.Eclipse的下载 到网站:http://www.eclipse.org/downloads/ 上,由于我们是用Java开发的所以步骤如下: 1.  找到"Eclipse IDE fo ...

  4. 使用AlarmManager设置闹钟----之一

    import java.util.Calendar; import android.os.Bundle;import android.app.Activity;import android.app.A ...

  5. Minimum Inversion Number~hdu 1394

    The inversion number of a given number sequence a1, a2, ..., an is the number of pairs (ai, aj) that ...

  6. 微信内点击链接或扫描二维码可直接用外部浏览器打开H5链接的解决方案

    很多朋友问我怎么解决微信内点击链接或扫描二维码可以直接跳出微信在外部浏览器打开网页链接,其实这并不难,只要我们使用微信跳转浏览器接口实现跳转功能即可. 简单的处理方案 1. 用浏览器打开我们需要用到的 ...

  7. 添加 vip

    两台机器:172.16.91.101 172.16.91.107 在91.101上增加虚拟ip,92网段的 ifconfig eth0:1 172.16.92.2 netmask 255.255.25 ...

  8. lumisoft&period;net 邮件管理系列文章 - 如何判断附件为内嵌式还是附加式

    如果要区分邮件里面的附件是内嵌图片附件还是真正的附件,那么可以通过下面代码进行判断,如果是MIME_DispositionTypes.Attachment的就是普通附件,MIME_Dispositio ...

  9. mongodb在windows下的安装

    Windows下安装MongoDB 1.下载MongoDB数据库http://fastdl.mongodb.org/win32/mongodb-win32-i386-1.6.5.zip: 2.将安装文 ...

  10. ssh中文手册

    ssh-keygen 中文手册 sshd_config 中文手册 sshd 中文手册