MyBatis(技术NeiMu):基础支持层(DataSource)

回顾

上一篇,分析了类型转换、日志适配器、类加载模块和ResolverUtil(用于寻找指定包下符合要求的类),并且还看到了使用的一些设计模式,比如适配器模式、单例模式

下面就到基础支持层的一个核心,DataSource

DataSource

在数据持久层中,数据源是一个非常重要的组件,性能会直接关乎到整个持久层的性能。

大多数时候,我们都会去使用第三方的数据源,比如Apache Common DBCP、C3P0、Proxool等,MyBatis不仅支持可以自定义去集成第三方数据源组件,还提供了自己的数据源实现

首先我们要认识一下DataSource这个接口,这个接口是javax.sql.DataSource下的

MyBatis(技术NeiMu):基础支持层(DataSource)
常见的数据源组件都是实现了这个DataSource接口的,所以MyBatis自身定义的数据源也会去实现这个接口,我们就可以从中知道MyBatis实现了多少种数据源,并且在工厂模式中,生产出来的产品就是一个DataSource
MyBatis(技术NeiMu):基础支持层(DataSource)
可以看到,对于MyBatis,总共有两种实现

  • PooledDataSource
  • UnpooledDataSource

MyBati有多个数据源,那么MyBatis是如何进行管理的呢?

DataSourceFactory

对于多个数据源,MyBatis是使用DataSourceFactory来进行管理的,从这里就可以看到,大概就可以估计使用了工厂模式了

MyBatis(技术NeiMu):基础支持层(DataSource)
从上面可以看到,DataSourceFactory这个接口里面仅仅提供了两个方法

  • setProperties:设置数据源相关的属性
  • getDataSource:获取数据源

MyBatis(技术NeiMu):基础支持层(DataSource)
并且可以看到,MyBatis对于DataSourceFactory接口有三个实现类

  • JndiDataSourceFactory:
  • PooledDataSourceFactory:创建PooledDataSource数据源
  • UnpooledDataSourceFactory:创建UnpooledDataSource数据源

UnpooledDataSourceFactory

下面我们先来看下UnpooledDataSource数据源的创建工厂

MyBatis(技术NeiMu):基础支持层(DataSource)
从构造方法上我们就可以看到,当实例化UnPooledDataSourceFactory时,就会去创建UnpooledDataSource了,并且注入进自己的DataSource成员属性中去

下面再来看看DataSourceFactory中的getDataSource方法是如何实现的

MyBatis(技术NeiMu):基础支持层(DataSource)
可以看到,十分朴实,直接返回构造出来的UnpooledDataSource

接下来看看是如何重写setProperty方法的,源码如下

 public void setProperties(Properties properties) {
     //存储对于DataSource的配置
    Properties driverProperties = new Properties();
     //使用SystemMetaObject去创建DataSource的MetaObject对象
     //前面我们已经提到过这个MetaObject对象了,是MyBatis用来管理实例对象的信息的!!!!
    MetaObject metaDataSource = SystemMetaObject.forObject(dataSource);
     //遍历要设置的属性
    for (Object key : properties.keySet()) {
        //获取当前要设置的属性的名称
      String propertyName = (String) key;
        //如果是driver.
        //代表是对DataSource的配置
      if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) {
          //添加进driverProperties集合中去
        String value = properties.getProperty(propertyName);
        driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value);
      } 
        //下面就是设置其他的相关属性了,必须要有setter方法才能够进行设置
        else if (metaDataSource.hasSetter(propertyName)) {
            //调用set方法进行设置属性
        String value = (String) properties.get(propertyName);
        Object convertedValue = convertValue(metaDataSource, propertyName, value);
        metaDataSource.setValue(propertyName, convertedValue);
      } else {
        throw new DataSourceException("Unknown DataSource property: " + propertyName);
      }
    }
     //最后去设置DataSource的配置
    if (driverProperties.size() > 0) {
      metaDataSource.setValue("driverProperties", driverProperties);
    }
  }

可以看到,UnpooledDataSourceFactory对于setProperty方法是先将DataSource转换为MetaObject,然后再继续进行set注入的,所以这里就可以看到MyBatis对于MetaObject的运用了,将当前的实例DataSource转换为MetaObject(MetaObject可以用于管理实例的信息),然后进行对应的属性修改或注入

PooledDataSourceFactory

PooledDataSourceFactory则更简单,其是用来创建PooledDataSource的

MyBatis(技术NeiMu):基础支持层(DataSource)
可以看到PooledDataSourceFactory是继承了UnpooledDataSourceFactory的,仅仅只是构造方法上去创建数据源不同而已,PooledDataSourceFactory创建的是PooledDataSource,而UnpooledDataSourceFactory创建的是UnPooledDataSource

其他的方法,比如获取数据源,设置数据源属性都是跟UnpooledDataSourceFactory一样的

JndiDataSourceFactory

JndiDataSourceFactory是使用JNDI服务来去设置第三方数据源的,在之前的JVM学习双亲委派模型的时候,针对JNDI服务可以破坏过依次双亲委派模型的

JNDI

JNDI是全名为Java Naming and Directory Interface,是Sum公司提供的一套API规范,其功能是模仿Window的注册表

相当与把用户的配置存放进去了注册表中,然后JNDI可以从该注册表中取信息出来

而JndiDataSourceFactory就i是可以利用Jndi服务从容器中去获取用户配置的DataSource的

看完DataSourceFactory之后,下面就来看看DataSource

UnpooledDataSource

UnpooledDataSource的特点就是如名字一样,没有池子,也就是没有连接池,每次获取数据库连接时都会创建一个新连接

MyBatis(技术NeiMu):基础支持层(DataSource)
下面我们先来看看UnpooledDataSourced的成员属性

  • driverClassLoader:加载Driver类的类加载器

  • driverPorperties:数据库连接驱动道德相关配置

  • registeredDrivers:缓存所有已经注册的数据库连接驱动

  • driver:数据库连接的驱动名称

  • url:数据库URL

  • username:用户名

  • password:密码

  • autoCommit:是否自动提交

  • defaultTransactionIslationLevel:默认的事务隔离等级

  • defaultNetworkTimeout:默认的网络连接超时

回忆一下之前使用原生的JDBC

第一步:注册驱动,DriverManager.registerDriver

注册驱动之后也是保存在DriverManger的一个容器里面,如下所示

MyBatis(技术NeiMu):基础支持层(DataSource)
MyBatis(技术NeiMu):基础支持层(DataSource)
下面再看一下静态代码块,因为当第一次访问UnpooledDataSource会执行静态代码块

MyBatis(技术NeiMu):基础支持层(DataSource)
源码如下

static {
    //获取注册进DriverManager的驱动
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    //遍历将注册的驱动都添加进缓存中
    while (drivers.hasMoreElements()) {
      Driver driver = drivers.nextElement();
      registeredDrivers.put(driver.getClass().getName(), driver);
    }
  }

可以看到,注册驱动并不是在DataSource里面去完成的,并且DataSource里面还支持多种驱动!!!!!!说白了支持多个数据库

下面再看看是如何获取连接的

getConnection

MyBatis(技术NeiMu):基础支持层(DataSource)
getConnection对应的就是去获取Connection连接的,可以看到其调用了doGetConnection方法,怎么框架都喜欢将实质操作给到doxxx方法去做的呀,在Spring框架也是一样,明明已经是createBean了,还要推到doCreateBean去实现

下面来看一下doGetConnection做了什么事情

源码如下

private Connection doGetConnection(String username, String password) throws SQLException {
    //封装获取连接的配置参数
    //Properties本质上是一个HashTable
    //Properties还是java.util下的类,是Java原生的,适合存一些键值对配置信息
    Properties props = new Properties();
    //装入所有的数据源配置
    if (driverProperties != null) {
      props.putAll(driverProperties);
    }
    //装入用户名
    if (username != null) {
      props.setProperty("user", username);
    }
    //装入密码
    if (password != null) {
      props.setProperty("password", password);
    }
    //调用doGetConnection的重载方法
    return doGetConnection(props);
  }

可以看到,在这一层仅仅做了封装参数而已,封装了数据源的配置、连接数据源的账号与密码,接下来就委托到doGetConnection方法去实现了

重载的doGetConnection源码如下

private Connection doGetConnection(Properties properties) throws SQLException {
    //初始化驱动,针对没有驱动
    initializeDriver();
    //使用DriverManager来进行获取连接。。。。。。
    Connection connection = DriverManager.getConnection(url, properties);
    //对获取的连接进行一些信息配置
    configureConnection(connection);
    return connection;
  }

步骤如下

  • 初始化驱动
  • 使用DriverManager来根据数据库URL和配置信息来获取Connection连接
  • 对Connection做一些配置
  • 返回Connection

下面对这几个步骤逐个分析

initializeDriver

源码如下

private synchronized void initializeDriver() throws SQLException {
    //判断驱动是否已经注册了
    if (!registeredDrivers.containsKey(driver)) {
      Class<?> driverType;
      try {
          //如果驱动没有注册
          //使用ClassLoader来创建驱动类
          //根据驱动的名称进行创建,所以这里驱动的名称应该是一个全限定类名
        if (driverClassLoader != null) {
          driverType = Class.forName(driver, true, driverClassLoader);
        } else {
          driverType = Resources.classForName(driver);
        }
          //使用反射来创建驱动的实例
        Driver driverInstance = (Driver) driverType.getDeclaredConstructor().newInstance();
          //DriverManager来注册驱动
        DriverManager.registerDriver(new DriverProxy(driverInstance));
          //并且将注册的驱动添加到缓存中去
        registeredDrivers.put(driver, driverInstance);
      } catch (Exception e) {
        throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
      }
    }
  }

可以看到初始化驱动其实就是判断是否已经将UnpooledDataSource的驱动已经完成注册了(虽然有缓存,但只能用一个驱动!!!),如果没有注册,说白了就是缓存中没有当前驱动的名称,如果有就代表已经注册了,没有就代表没有注册,如果没有注册代表还没有去创建驱动,需要使用ClassLoader+反射来创建出驱动实例,然后交由DriverManager进行注册驱动,完成注册驱动后,将注册的驱动添加到缓存中去

DriverManager.getConnection

使用DriverManager.getConnection来获取连接就没什么好说的

configureConnection

接下来就是要配置一下Connection连接了,是否自动提交、超时时间的设置、默认的事务隔离等级,源码如下

private void configureConnection(Connection conn) throws SQLException {
    //判断定义的默认的超时时间是不是不为空
    if (defaultNetworkTimeout != null) {
        //如果超时时间存在定义,那就去重新定义Connection的超时时间
      conn.setNetworkTimeout(Executors.newSingleThreadExecutor(), defaultNetworkTimeout);
    }
    //判断是否有自定义自动提交属性
    //如果设置了自动提交属性并且自定义的提交属性与Connection原来默认的存在出入
    if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
        //进行设置
        //这里之所以判断复杂,个人认为应该是自动提交是个敏感的属性
      conn.setAutoCommit(autoCommit);
    }
    //如果自定义的默认事务隔离机制不为空
    if (defaultTransactionIsolationLevel != null) {
        //给Connection设置自定义的事务隔离机制
      conn.setTransactionIsolation(defaultTransactionIsolationLevel);
    }
  }

从代码中可以看到,对于创建出的Connection的配置其实就是针对以下三个方面

  • 网络超时时间
  • 自动提交
  • 事务隔离机制

从整个获取连接的过程可以看到,getConnection每次都会调用DriverManager去创建Connection,创建出新连接,这也是unPooled的原因

PooledDataSource

创建数据库连接也就是Connection是一个 非常耗时的操作,而且数据库能够建立的连接数也是有限的,所以在绝大多数系统中,数据库连接是一个非常珍贵的资源,数据库连接池是很有必要性的,使用数据库连接池会带来一系列的好处,例如,可以实现数据库连接的重用、提高响应速度、防止数据库连接过多造成数据库假死、同时避免数据库连接泄漏

数据库连接池的工作流程是怎样的呢?

  • 数据库连接池在初始化时,先会创建一定数量的数据库连接并且添加到连接池中备用

  • 当程序需要用到连接的时候,从池中去获取连接

  • 当程序使用完连接之后,会将连接返回到池中,等待下次使用,而不是直接关闭

  • 数据库连接池会保证一定数量的连接,会控制连接总数的上线以及空闲连接数的上限,如果连接池创建的总连接数达到了上线,并且全都被占用,后续想从连接池中获取连接的线程会进入阻塞队列进行等待,直到线程释放出可用的连接;如果连接池中空闲连接数比较多,达到空闲连接的上线,那么后续使用完的连接返回回来时(一般变为空闲连接),会被直接关闭,因为维护空闲连接也是要需要开销的

下面分析一下,总连接数和空闲连接数的上线设置过大过小会有什么问题

  • 总连接数:代表的是连接池中的Connection实例数量,如果设置的过大,容易导致数据库僵死(太多连接);如果设置的过小,无法发挥数据库的性能,而且影响用户线程(被阻塞)
  • 空闲连接数:代表的是连接池中的空闲Connection的数量,如果设置的过大,会浪费过多的系统资源去维护这些空闲Connection;当空闲连接数设置的过小时,当出现峰值请求时,系统的响应能力会很弱

下面就来看下PooledDataSource是如何实现的

MyBatis(技术NeiMu):基础支持层(DataSource)
可以看到,PooledDataSource同样实现了DataSource接口,并且其还组装了UnpooledDataSource与PoolState

PooledConnection

PooledDataSource并不会直接管理Connection对象,而是管理经过了一层封装的PooledConnection对象,在PooledConnection中封装了真正的数据库连接对象,也就是Connection,还有其代理对象,该代理对象是由JDK动态代理产生的

MyBatis(技术NeiMu):基础支持层(DataSource)
可以看到,PooledConnection实现了InvocationHandler接口,说明了PooledConnection本身就是一个代理对象。。。。。。

下面来看一下PooledConnection的核心字段

  • dataSource:PooledConnection的连接池
  • realConnection:真正的数据库连接(注意这个是Connection类型)
  • proxyConnection:数据库连接的代理对象(这个也是Connection类型)
  • checkoutTimestamp:从连接池中被取出的时间戳
  • createdTimestamp:该连接被创建的时间戳
  • lastUsedTimestamp:最近一次使用该连接的时间戳
  • connectionTypeCode:用数据库URL、用户名和密码计算出来的一个哈希值,可以用来标识该连接所属于的连接池
  • valid:标志当前的PooledConnection是否有效,该字段的作用就是预防程序已经通过close方法关闭了该连接,但后续该连接还被继续使用

前面提到过,PooledConnection本身实现了InvocationHandler接口,说明其本身就是一个代理对象,那么为什么在成员属性上会有一个代理对象呢?

我们来看一下该构造方法

源码如下

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;
    //可以看到,真实产生的代理对象是属性中的proxyConnection
    //属性中的代理对象是通过当前PooledConnection来创建出来的,个人感觉怎么像是变成了一个静态代理
    //相当于只要去new了代理对象,代理对象内部就会去实例出了一个被当前代理对象代理的Connection出来
    //编码能力确实牛逼。。。。。。。
    this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
  }

从代码上可以看到,MyBatis的确有点东西。。。。。。。

个人感觉这种方式跟静态代理差不多了,通过创建代理对象就完成了代理

对于JDK动态代理对象,重点就要在于invoke方法,下面就来看看invoke方法做了什么逻辑

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //获取执行的方法名
    String methodName = method.getName();
    //如果方法名是close
    if (CLOSE.equals(methodName)) {
        //让dataSource去回收缓存,而不是真正的关闭!!!
      dataSource.pushConnection(this);
        //不去执行真正的close逻辑
      return null;
    }
    try {
        //判断是不是执行Object里面的方法
        
      if (!Object.class.equals(method.getDeclaringClass())) {
          //如果执行不是Object里面的方法
          //要检查当前连接是不是失效了
          //里面的逻辑仅仅只是判断valid字段而已,要是为false就会抛错
        checkConnection();
      }
        //这里很关键,可以看到,这里是交由了realConnection去执行的
        //而不是proxyConnection,也就是被代理的Connection去执行逻辑
      return method.invoke(realConnection, args);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }

  }

可以看到,invoke方法的逻辑如下

  • 先拦截close方法,如果执行了close方法并不是真正去执行Connectiond的close方法,而是让PooledDataSource去回收进连接池
  • 然后拦截不是Object里面的方法,如果不是Object里面的方法,都必须要经过校验才能去执行,也就是valid字段的校验,避免了将连接已经放回连接池中了,还要继续操控这个连接,通过了校验才会使用反射去执行,并且反射执行的是realConnection对象的方法,而不是被代理的Connection的方法

PoolState

认识玩了PooledConnection之后,下面我们认识PoolState,PoolState就是PooledDataSource里面的连接池

MyBatis(技术NeiMu):基础支持层(DataSource)
MyBatis(技术NeiMu):基础支持层(DataSource)
下面就来分析一下PoolState的成员属性,除了连接池之外,也有着很多的统计数据

  • PooledDataSource:连接池所属的数据源
  • idleConnection:空闲的PooledConnection连接集合,可以看到是一个ArrayList集合
  • activeConnection:正在使用的PooledConnection连接集合,可以看到是一个ArrayList集合
  • requestCount:请求数据库连接次数
  • accumulateRequestTime:获取所有连接的累积时间
  • accumulateCheckoutTime:获取所有连接的CheckoutTime
  • claimedOverdueConnection:记录超时的连接个数
  • accumulatedCheckoutTimeofOverdueConnections:所有连接累计的超时时间
  • accumulatedWaitTime:所有连接累计的等待时间
  • hadToWaitCount:线程获取连接失败,等待的次数
  • badConnectionCount:无效的连接数

可以看到,PoolState维护了一系列的统计数据,而且PoolState仅仅只是拥有这些资源的一个封装,剩下的方法都是get方法而已

这个连接池是所有用户线程都会使用的,因此会产生一系列的并发问题,在PoolState里面,主要是使用Synchroniced来保证并发安全的

MyBatis(技术NeiMu):基础支持层(DataSource)

PooledDataSource的其他可选信息

MyBatis(技术NeiMu):基础支持层(DataSource)
可以看到,PooledDataSource还有着一系列的可选信息,比如之前我们提到的连接池的总连接数、最大的空闲连接数等。。。。。

  • poolMaximumActiveConnections:最大的活跃连接数
  • poolMaximumIdleConnections:最大的空闲连接数
  • poolMaximumCheckoutTime:最大的Checkout时长
  • poolTimeToWait:最大的等待获取连接的时长,既当线程获取不了连接时,最大的等大时长
  • poolMaximumLocalBadConnectionToLerance:最大的可容忍失败连接次数,也就是Ping测试SQL失败
  • PoolPingQuery:发送的测试SQL语句,用来测试数据库连接是否可用的
  • PoolPingEnable:是否开启测试数据库连接是否可用功能
  • poolPingConnecionsNotUsedFor:超过多少毫秒时,发生一次测试SQL,检查连接是否还可用
  • exceptedConnectionTypeCode:根据数据库的URL、用户名和密码生成的一个hash值,该哈希值用于标志着当前的连接池

UnpooledDataSource

从PooledDataSource的成员属性中,我们可以看到其还组装了UnpooledDataSource,我个人理解为这里采用了装饰器模式,给UnpooledDataSource加上了连接池的功能,从而形成了新的UnpooledDataSource

下面就来看看PooledDataSource的一个关键点,如何从连接池中获取连接

MyBatis(技术NeiMu):基础支持层(DataSource)

获取Connection连接

对应的方法是getConnection方法,而getConnection方法通过popConnection方法获取连接池中的PooledConnection,然后再获取代理的Connection的代理对象(ProxtConnection)去进行操作,所以关键点在于popConnection是如何从连接池中获取的

源码如下

private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
    //conn用来承载获取到的PooledConnection
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    int localBadConnectionCount = 0;
	//循环去进行获取PooledConnection
    //循环去获取PooledConnection,直到获取成功,获取获取失败超过一定次数抛错
    while (conn == null) {
        //对state进行上锁
        //保证了只有一个线程可以从state中获取连接
      synchronized (state) {
          //判断是否有空闲连接池中是否有空闲连接
        if (!state.idleConnections.isEmpty()) {
            //如果有空闲连接,弹出第一个空闲连接
            //注意这里!
            //使用的是remove方法,弹出了之后是要进行维护的
            //既将后面的空闲连接给移动上来
            //因此过大的空闲连接是否耗费性能的
          conn = state.idleConnections.remove(0);
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else {
          // 如果没有空闲的连接
          //判断当前正在激活的Connection有没有超过允许的最大激活Connecion数量
            //说白了就是判断当前正在进行的连接有没有超过设置的最大连接数阈值
          if (state.activeConnections.size() < poolMaximumActiveConnections) {
            // 如果没超过,使用UnPooledDataSource去创建新的Connection
              //然后为该新的Connection创建代理对象
              //封装进PooledConnection里面
            conn = new PooledConnection(dataSource.getConnection(), this);
            if (log.isDebugEnabled()) {
              log.debug("Created connection " + conn.getRealHashCode() + ".");
            }
          } else {
            // 如果超过了设置的最大连接阈值,就不能去新建了
              //获取最老的一个正在使用的连接
            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
              //判断最老的连接是不是已经执行超时了
              //这里的超时是指超过了定义的CheckoutTime
            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
            if (longestCheckoutTime > poolMaximumCheckoutTime) {
                //如果已经超时了就要进行移除
              // 维护poolState的统计信息
              state.claimedOverdueConnectionCount++;
              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
              state.accumulatedCheckoutTime += longestCheckoutTime;
                //超时了,从连接池中进行移除
              state.activeConnections.remove(oldestActiveConnection);
                //判断该Connection有没有开启自动提交
              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                try {
                    //如果没有开启自动提交,执行回滚
                  oldestActiveConnection.getRealConnection().rollback();
                } catch (SQLException e) {
                  log.debug("Bad connection. Could not roll back");
                }
              }
                //重新创建PooledConnection
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
                //设置PooledConnection的一些时间属性
              conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
              conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
                //将最老的连接改为失效
              oldestActiveConnection.invalidate();
              if (log.isDebugEnabled()) {
                log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
              }
            } 
              //如果最老的连接都没有超过定义的Checkout时间
              //那就必须要进行等待了
              else {
              // 进行等待
              try {
                  //等待一次
                if (!countedWait) {
                    //维护PoolState的一些属性
                  state.hadToWaitCount++;
                    //不等了
                  countedWait = true;
                }
                if (log.isDebugEnabled()) {
                  log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
                }
                  //
                long wt = System.currentTimeMillis();
                  //PoolState执行wait方法进行等待,并且等待的时间为设置的等待时间
                  //这里关键点在于进入wait方法是会释放锁的!
                  //默认为20000毫秒,等待20S?????
                state.wait(poolTimeToWait);
                state.accumulatedWaitTime += System.currentTimeMillis() - wt;
              } catch (InterruptedException e) {
                break;
              }
            }
          }
        }
          //如果经过上面的步骤可以拿到连接
        if (conn != null) {
          // 对连接进行校验,isValid方法里面会进行ping
          if (conn.isValid()) {
              //在使用连接之前,将里面的事务回滚掉!
              //回滚掉会比较稳健。。。。
            if (!conn.getRealConnection().getAutoCommit()) {
              conn.getRealConnection().rollback();
            }
              //对PooledConnection进行一些属性设置
            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;
          } 
            //如果校验失败,也就是ping不同,执行测试SQL失败
            else {
            if (log.isDebugEnabled()) {
              log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
            }
                //维护信息
            state.badConnectionCount++;
            localBadConnectionCount++;
                //让con = null,让外部循环重新去获取!
            conn = null;
                //判断ping测试失败次数,超过了指定的失败次数要进行抛错
            if (localBadConnectionCount > (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) {
      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.");
    }
	//返回得到得到Connection
    return conn;
  }

大概的流程如下

  • 循环去获取Connection,直到获取成功,获取获取失败次数达到上限抛错处理(获取失败并不是获取Null,而是获取到了连接,但Ping失败了,既执行测试SQL失败)
  • 对PoolState进行上锁
  • 判断空闲的连接池是否为空,如果为空直接取空闲连接池里面的第一个连接池,并且维护空闲连接池(将前面的空闲连接移动过来)
  • 如果空闲的连接池不为空,那就要考虑是否要新建连接或者释放已经活跃的连接
    • 如果已经活跃的连接数量未超过设定阈值,新建连接,并且获取的连接就是新建的连接
    • 如果已经活跃的连接数量超过了设定阈值,则要考虑需不需要释放正在活跃的连接
      • 判断最老的一个活跃连接有没有超时,也就是有没有超过设定的最大CheckoutTime,如果超过了,则释放最老的活跃连接,先从活跃的连接池中释放该最老连接,如果该连接没有设置自动提交,则将其里面的事务进行回滚,最后新建连接
      • 如果没有超时,则要进行等待,并且只等待一次!!!!!!等待操作交由state.wait操作去执行,默认的等待时间为20000毫秒,而wait操作实际是Object里面的方法,执行wait方法是会释放锁资源的,此时用完连接将要准备释放连接的线程才能继续操作
  • 经过上面的步骤之后,判断是否获得了连接,如果获得了连接,将该连接进行校验,既执行测试SQL,也就是Ping操作
    • 如果校验失败,返回False,让获取的连接变为Null,并且统计失败次数,如果超过上限值(空闲连接数目+最大容忍失败的次数),就会抛错,如果没失败则会下一轮循环
    • 如果校验成功,如果不是自动提交的,回滚里面的事务,将该连接添加进活跃的连接池上
  • 最后返回连接

获取连接的大概过程看懂了,现在来看一下细节

校验连接

对应的方法就是isValid方法

MyBatis(技术NeiMu):基础支持层(DataSource)
可以看到,要valid为true、realConnection不为null,并且交由dataSource.pingConnection成功才能代表校验成功,关键在于pingConnection操作

源码如下

protected boolean pingConnection(PooledConnection conn) {
    boolean result = true;

    try {
        //判断真实的连接关闭了没有
        //代理对象都是使用realConnection去执行操作的
        //关闭了result就会为false
        //只要没关闭才会为true
      result = !conn.getRealConnection().isClosed();
    } catch (SQLException e) {
      if (log.isDebugEnabled()) {
        log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
      }
      result = false;
    }
	//如果连接未关闭,并且开启了测试校验,并且超过了设定的进行校验的相隔时间
    //就需要进行SQL测试校验
    if (result && poolPingEnabled && poolPingConnectionsNotUsedFor >= 0
        && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
      try {
        if (log.isDebugEnabled()) {
          log.debug("Testing connection " + conn.getRealHashCode() + " ...");
        }
          //使用当前连接去执行poolPingQuery,也就是测试SQL
        Connection realConn = conn.getRealConnection();
        try (Statement statement = realConn.createStatement()) {
          statement.executeQuery(poolPingQuery).close();
        }
          //如果是自动提交的,进行回滚
          //保证了测试SQL不会影响数据
        if (!realConn.getAutoCommit()) {
          realConn.rollback();
        }
          //测试SQL通过,让result为true
        result = true;
        if (log.isDebugEnabled()) {
          log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");
        }
      } catch (Exception e) {
        log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage());
        try {
          conn.getRealConnection().close();
        } catch (Exception e2) {
          // ignore
        }
        result = false;
        if (log.isDebugEnabled()) {
          log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
        }
      }
    }
    return result;
  }

可以看到,Ping校验并不是一定会开启的,必须要连接未关闭(如果关闭了还测试个鬼咩),并且开启了测试SQL功能,还要达到时间条件(也就是达到一定时间间隔才执行测试)

等待获取连接

还有一个关键点在于PoolState是如何进行等待操作的

MyBatis(技术NeiMu):基础支持层(DataSource)
MyBatis(技术NeiMu):基础支持层(DataSource)
可以看到,PoolState是直接调用了Object的wait方法,让当前线程进入了等待状态。。。。。。

dataSource.pushConnection

那问题来了,线程一定会等待到指定时间吗?此时如果空闲连接池有空闲连接了不能直接拿吗?

还记得代理Connection对象的PooledConnection吗?在它的invoke方法会拦截Close方法,并且会执行dataSouece.pushConnection方法,如下表示

MyBatis(技术NeiMu):基础支持层(DataSource)
下面就来看看这个pushConnection的逻辑,因为空闲连接是从这里产生的,因此很有可能在这里唤醒线程取进行争抢,源码如下

 protected void pushConnection(PooledConnection conn) throws SQLException {
	//对连接池进行上锁,因此释放连接也是只能一个线程一个线程去进行释放
    synchronized (state) {
        //首先从活跃连接池中进行释放该连接
      state.activeConnections.remove(conn);
        //判断其是否仍然可以通过校验了
        //前面以及看过isValid方法了,是有可能会进行Ping操作的
        //因此在释放前也会判断该连接是否可用
      if (conn.isValid()) {
          //如果此时的空闲连接数小于定义的最大空闲连接数
          //并且expectedConnectionTypeCode可以对应上
        if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
            //同意进行释放
            //更新CheckoutTime
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
            //如果不是自动提交,将里面的事务回滚
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback();
          }
            //可以看到,原先的PooledConnection代理对象被抛弃掉了,转而去创建新的代理对象
            //但是!被代理的RealConnection却还是同一个
          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 {
            //如果超过了允许的空闲连接最大数量,就需要抛弃掉这个连接了
            //更新连接池信息
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
            //不是自动提交就进行回滚
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback();
          }
            //关闭旧的RealConnection
          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.");
        }
          //如果ping失败了,既归还的连接不能使用
          //然后什么都不做???单纯移出活跃连接池?
        state.badConnectionCount++;
      }
    }
  }

下面总结一下整个归还步骤

  • 获取对连接池的锁
  • 将该连接从活跃连接池中释放
  • 校验一下该连接是否还可以继续使用
    • 如果可以继续使用,判断当前空闲连接池是否达到了定义的最大空闲连接数
      • 如果没有达到,创建新的代理对象(不过被代理的Connection还是同一个),将新的代理对象添加进空闲连接池中,并且将没有自动提交的事务进行回滚,让原先旧的代理对象失效,最后去通知所有的线程(唤醒那些在获取连接中进入了等待状态的线程)
      • 如果达到了,将没有自动提交的事务进行回滚,然后将此时的代理对象中的被代理Connection真正去关闭掉,然后将代理对象设置为失效,不会被添加进空闲连接池,并且也不会去通知线程,因为并没有归还嘛,相当于直接丢掉了
    • 如果不可以使用,更新badConnectionCount属性,什么都不做了。。。。。。

强制关闭所有连接

在PooledDataSource里面还有一个重要的操作,就是强制关闭所有连接,什么时候进行强制关闭所有连接呢?

当修改了PooledDatSource的字段时,比如数据库URL、用户名、密码、autoCommit等配置(执行其Set方法),都会调用forceCloseAll方法将所有的数据库连接关闭,同时也会将所有相应的PooledConnection对象都设置为无效,并且清空连接池,包括活跃连接池与空闲连接池

MyBatis(技术NeiMu):基础支持层(DataSource)
可以看到,set方法之后都会执行forceCloseAll方法

forceCloseAll

该方法源码如下

public void forceCloseAll() {
    //对PooledState进行上锁
    synchronized (state) {
        //生成新的expectedConnectionTypeCode
      expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
        //遍历活跃连接池
      for (int i = state.activeConnections.size(); i > 0; i--) {
        try {
            //将当前连接移出活跃连接池
          PooledConnection conn = state.activeConnections.remove(i - 1);
            //改为失效
          conn.invalidate();
			//进行回滚
          Connection realConn = conn.getRealConnection();
          if (!realConn.getAutoCommit()) {
            realConn.rollback();
          }
            //关闭
          realConn.close();
        } catch (Exception e) {
          // ignore
        }
      }
        //遍历空闲连接池
      for (int i = state.idleConnections.size(); i > 0; i--) {
        try {
            //将当前连接移出空闲连接池
          PooledConnection conn = state.idleConnections.remove(i - 1);
            //改为失效
          conn.invalidate();
		//进行回滚
          Connection realConn = conn.getRealConnection();
          if (!realConn.getAutoCommit()) {
            realConn.rollback();
          }
          realConn.close();
        } catch (Exception e) {
          // ignore
        }
      }
    }
    if (log.isDebugEnabled()) {
      log.debug("PooledDataSource forcefully closed/removed all connections.");
    }
  }

可以看到,强制关闭所有连接就是对活跃连接池和空闲连接池进行处理

  • 遍历连接池
  • 将连接从当前连接池移出来
  • 让连接变为失效(修改valid字段)
  • 判断是不是自动提交,如果不是自动提交就进行回滚
  • 让realConnection执行close

至此,PooledDataSource也认识完了

总结一下

  • MyBatis使用工厂模式去管理数据源的建立,并且采用抽象工厂模式,一个数据源的建立对应就是一个工厂,工厂抽象成DataSourceFactory,而创建的数据源是抽象成DataSource

  • MyBatis默认支持两种数据源

    • UnpooledDataSource:没有连接池,线程获取连接就去新创建一个连接
    • PooledDataSource:在UnpooledDataSource的基础上添加了连接池,并且采用组装的方式(修饰了UnpooledDataSource),添加了PoolState连接池
      • 管理连接不是直接管理Connection,而是管理其代理对象PooledConnection
      • 连接池分为两个部分,一个是活跃连接池,另外一个是空闲连接池,都是一个ArrayList集合
      • 连接并不是事先加载好的,更像是一种懒加载的形式一种!
        • 只有使用到连接的时候,才会判断是否需要创建,当连接用完了之后,执行close方法的时候会被代理对象PooledConnection拦截到,转而不去真正地进行close,而是存放进空闲连接中
        • 当没有空闲连接的时候,才会去考虑创建活跃连接,当活跃连接超过指定阈值时,就会尝试进行释放最老的活跃连接,如果释放失败,就会让线程进入等待状态(默认等待时间为20000毫秒,20S),等待20S或者直到有线程归还连接,并通知所有线程接触等待状态
        • 按道理来说,如果一直获取不到就会一直在循环里面,20S只是避免空转,针对测试SQL失败还有对应的机制进行限制,假如Ping失败超过了指定次数,将会抛错
  • 对于PooledDataSource:实际支持的最大连接数目就是自定义的最大活跃连接数,因为空闲连接池里面的连接都是来自活跃连接池里面释放出来的

上一篇:Druid阅读(七)设计并实现Pikaqiu连接池


下一篇:python3 ldap 查询用户,使用管理重置用户密码