MyBatis源码分析(5)——内置DataSource实现
@(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再补充
参考
- MyBatis官方文档
 
MyBatis源码分析(5)——内置DataSource实现的更多相关文章
- 10.源码分析---SOFARPC内置链路追踪SOFATRACER是怎么做的?
		
SOFARPC源码解析系列: 1. 源码分析---SOFARPC可扩展的机制SPI 2. 源码分析---SOFARPC客户端服务引用 3. 源码分析---SOFARPC客户端服务调用 4. 源码分析- ...
 - MyBatis 源码分析 - 内置数据源
		
1.简介 本篇文章将向大家介绍 MyBatis 内置数据源的实现逻辑.搞懂这些数据源的实现,可使大家对数据源有更深入的认识.同时在配置这些数据源时,也会更清楚每种属性的意义和用途.因此,如果大家想知其 ...
 - MyBatis 源码分析 - 配置文件解析过程
		
* 本文速览 由于本篇文章篇幅比较大,所以这里拿出一节对本文进行快速概括.本篇文章对 MyBatis 配置文件中常用配置的解析过程进行了较为详细的介绍和分析,包括但不限于settings,typeAl ...
 - MyBatis 源码分析系列文章导读
		
1.本文速览 本篇文章是我为接下来的 MyBatis 源码分析系列文章写的一个导读文章.本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说 ...
 - MyBatis 源码分析系列文章合集
		
1.简介 我从七月份开始阅读MyBatis源码,并在随后的40天内陆续更新了7篇文章.起初,我只是打算通过博客的形式进行分享.但在写作的过程中,发现要分析的代码太多,以至于文章篇幅特别大.在这7篇文章 ...
 - MyBatis 源码分析 - 插件机制
		
1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 My ...
 - MyBatis 源码分析 - 缓存原理
		
1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 Redis 或 memcached 等缓存中间件,拦截大量奔向数据库的请求,减轻数据库压力.作为一个重要的组件,MyBatis 自然 ...
 - MyBatis 源码分析 - 映射文件解析过程
		
1.简介 在上一篇文章中,我详细分析了 MyBatis 配置文件的解析过程.由于上一篇文章的篇幅比较大,加之映射文件解析过程也比较复杂的原因.所以我将映射文件解析过程的分析内容从上一篇文章中抽取出来, ...
 - 精尽MyBatis源码分析 - MyBatis-Spring 源码分析
		
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...
 
随机推荐
- 原创 C++应用程序在Windows下的编译、链接(四)动态链接
			
4动态链接 4.1概述 在静态链接阶段,链接器为PE文件生成了导入表,导出表,符号表,并调整了Call指令后面的操作数,在程序调用的时候,能够直接地或者间接地定位到IAT中的某个位置,在PE文件中,该 ...
 - tftp-nfs开发环境搭建
			
嵌入式开发通常使用主机-开发板的开发模式,在裸板开发中,我们通常使用串口调试工具传递文件,比如windows平台的超级终端,SecuCRT以及Linux平台的ckermit(题外话:ckermit比w ...
 - 对C#泛型实例化对像
			
public class A { } public class B<T> { public static T Get() { //在这一块如何实例化T这个对象呢?如果用default(T) ...
 - HDOJ 2317. Nasty Hacks 模拟水题
			
Nasty Hacks Time Limit: 3000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Tota ...
 - 例解 Linux 下 Make 命令
			
Linux 下 make 命令是系统管理员和程序员用的最频繁的命令之一.管理员用它通过命令行来编译和安装很多开源的工具,程序员用它来管理他们大型复杂的项目编译问题.本文我们将用一些实例来讨论 make ...
 - jenkins / ant / jmeter 持续集成接口自动化
			
1. 将 jmeter 脚本放在/var/lib/jenkins/workspace/Jmeter_auto/jmxpath路径下 2. 点击http://jk.facebank.net.cn/job ...
 - PAT 1046. 划拳(15)
			
划拳是古老中国酒文化的一个有趣的组成部分.酒桌上两人划拳的方法为:每人口中喊出一个数字,同时用手比划出一个数字.如果谁比划出的数字正好等于两人喊出的数字之和,谁就赢了,输家罚一杯酒.两人同赢或两人同输 ...
 - [LeetCode] Split Array Largest Sum 分割数组的最大值
			
Given an array which consists of non-negative integers and an integer m, you can split the array int ...
 - [LeetCode] Evaluate Division 求除法表达式的值
			
Equations are given in the format A / B = k, where A and B are variables represented as strings, and ...
 - [LeetCode] Trips and Users 旅行和用户
			
The Trips table holds all taxi trips. Each trip has a unique Id, while Client_Id and Driver_Id are b ...