@(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来实现连接的创建以及分配的,过程不复杂,当有空闲连接时则直接使用,否则再根据是否达到了设置的峰值,来决定是否需要创建新的连接等。

流程图如下:

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关闭数据库连接的时候,根据当前状态来决定放入空闲链表中还是释放掉。

流程图如下:

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.源码分析---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. 脱离spring集成cxf(基于nutz框架)

    什么是webService WebService是一种跨编程语言和跨操作系统平台的远程调用技术. 理论资料: http://blog.csdn.net/wooshn/article/details/8 ...

  2. ORA-00600 3020 ORA-10567案例

    PlateSpin克隆复制出的Oracle数据库服务器,往往启动数据库实例都会遇到一些杂七杂八的问题.今天测试DR环境时又遇到了一个特殊场景,在此之前,我已经遇到了下面两起案例: ORA-00600: ...

  3. Shell中字符串分割的三种方法

    问题:对于’1,2,3,4,5’这样的字符串输出采用,分隔开的1 2 3 4 5 特征:在字符串中没有空格 解决方法1: #!/bin/bash var=’1,2,3,4,5’ var=${var// ...

  4. 树莓派3B更新软件

    因为软件是要不断更新的,所以半个月或者一个月要升级一下软件 升级软件非常简单 在终端或者SSH里输入 sudo apt-get update && apt-get upgrade -y ...

  5. 如何安装appium-linux

    准备工作: JDK/Android sdk (记得把android sdk的环境变量命名成ANDROID_HOME ) 确保ADB命令可用 git python 和 pip 一,安装node.js和n ...

  6. ubuntu 14.04 配置 jdk1.8

    自己在Ubuntu中安装jdk1.8的步骤,记录以方便以后查询. 将下载好的jdk安装包移到/usr/local目录中(我喜欢将自己安装的软件放在/usr/local目录中),解压缩 sudo tar ...

  7. Windows下虚拟机安装Ubuntu15.10 Destop简易操作过程

    一.前提环境: 1.vmware12.1,若您的系统是32位,请使用vmware10以下版本. 2.至少双核处理器,2G以上可用内存. 3.Ubuntu安装包(.iso后缀). 注:请尽量支持正版. ...

  8. Programming Contest Problem Types

        Programming Contest Problem Types Hal Burch conducted an analysis over spring break of 1999 and ...

  9. Favorites of top 10 rules for success

    Dec. 31, 2015 Stayed up to last minute of 2015, 12:00am, watching a few of videos about top 10 rules ...

  10. .数据库连接池技术:DBCP和C3P0

    数据库连接池技术:DBCP和C3P0 1.什么是数据库连接池 已知的方法是需要访问数据库的时候进行一次数据库的连接,对数据库操作完之后再释放这个连接,通常这样业务是缺点很明显的: 用户每次请求都需要向 ...