关于java:Mybatis数据源结构解析之连接池

对于 ORM 框架而言,数据源的组织是一个十分重要的一部分,这间接影响到框架的性能问题。本文将通过对 MyBatis 框架的数据源构造进行详尽的剖析,找出什么时候创立 Connection ,并且深刻解析 MyBatis 的连接池。


本章的组织构造:

  • 零、什么是连接池和线程池
  • 一、MyBatis 数据源 DataSource 分类
  • 二、数据源 DataSource 的创立过程
  • 三、 DataSource 什么时候创立 Connection 对象
  • 四、不应用连接池的 UnpooledDataSource
  • 五、应用了连接池的 PooledDataSource

连接池和线程池

连接池:(升高物理连贯损耗)

  • 1、连接池是面向数据库连贯的
  • 2、连接池是为了优化数据库连贯资源
  • 3、连接池有点相似在客户端做优化

数据库连贯是一项无限的低廉资源,一个数据库连贯对象均对应一个物理数据库连贯,每次操作都关上一个物理连贯,应用完都敞开连贯,这样造成零碎的性能低下。 数据库连接池的解决方案是在应用程序启动时建设足够的数据库连贯,并将这些连贯组成一个连接池,由应用程序动静地对池中的连贯进行申请、应用和开释。对于多于连接池中连接数的并发申请,应该在申请队列中排队期待。并且应用程序能够依据池中连贯的使用率,动静减少或缩小池中的连接数。


线程池:(升高线程创立销毁损耗)

  • 1、线程池是面向后台程序的
  • 2、线程池是是为了进步内存和CPU效率
  • 3、线程池有点相似于在服务端做优化

线程池是一次性创立肯定数量的线程(应该能够配置初始线程数量的),当用申请过去不必去创立新的线程,间接应用已创立的线程,应用后又放回到线程池中。
防止了频繁创立线程,及销毁线程的零碎开销,进步是内存和CPU效率。

相同点:

都是当时筹备好资源,防止频繁创立和销毁的代价。

数据源的分类

在Mybatis体系中,分为3DataSource

关上Mybatis源码找到datasource包,能够看到3个子package

  • UNPOOLED 不应用连接池的数据源
  • POOLED 应用连接池的数据源
  • JNDI 应用JNDI实现的数据源

MyBatis外部别离定义了实现了java.sql.DataSource接口的UnpooledDataSource,PooledDataSource类来示意UNPOOLED、POOLED类型的数据源。 如下图所示:

  • PooledDataSource和UnpooledDataSrouce都实现了java.sql.DataSource接口.
  • PooledDataSource持有一个UnPooledDataSource的援用,当PooledDataSource要创立Connection实例时,理论还是通过UnPooledDataSource来创立的.(PooledDataSource)只是提供一种缓存连接池机制.

JNDI类型的数据源DataSource,则是通过JNDI上下文中取值。

数据源 DataSource 的创立过程

在mybatis的XML配置文件中,应用<dataSource>元素来配置数据源:

<!-- 配置数据源(连接池) -->
<dataSource type="POOLED"> //这里 type 属性的取值就是为POOLED、UNPOOLED、JNDI
  <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>

MyBatis在初始化时,解析此文件,依据<dataSource>的type属性来创立相应类型的的数据源DataSource,即:

  • type=”POOLED” :创立PooledDataSource实例
  • type=”UNPOOLED” :创立UnpooledDataSource实例
  • type=”JNDI” :从JNDI服务上查找DataSource实例


    Mybatis是通过工厂模式来创立数据源对象的 咱们来看看源码:

    public interface DataSourceFactory {
    
    void setProperties(Properties props);
    
    DataSource getDataSource();//生产DataSource
    
    }

    上述3种类型的数据源,对应有本人的工厂模式,都实现了这个DataSourceFactory

MyBatis创立了DataSource实例后,会将其放到Configuration对象内的Environment对象中, 供当前应用。

留神dataSource 此时只会保留好配置信息.连接池此时并没有创立好连贯.只有当程序在调用操作数据库的办法时,才会初始化连贯.

### DataSource什么时候创立Connection对象
咱们须要创立SqlSession对象并须要执行SQL语句时,这时候MyBatis才会去调用dataSource对象来创立java.sql.Connection对象。也就是说,java.sql.Connection对象的创立始终提早到执行SQL语句的时候。

例子:

  @Test
  public void testMyBatisBuild() throws IOException {
      Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
      SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
      SqlSession sqlSession = factory.openSession();
      TestMapper mapper = sqlSession.getMapper(TestMapper.class);
      Ttest one = mapper.getOne(1L);//直到这一行,才会去创立一个数据库连贯!
      System.out.println(one);
      sqlSession.close();
  }

口说无凭,跟进源码看看他们是在什么时候创立的…

#### 跟进源码,验证Datasource 和Connection对象创立机会
##### 验证Datasource创立机会

  • 下面咱们曾经晓得,pooled数据源实际上也是应用的unpooled的实例,那么咱们在UnpooledDataSourceFactory的

getDataSource办法的源码中做一些批改 并运行测试用例:

@Override
public DataSource getDataSource() {//此办法是UnpooledDataSourceFactory实现DataSourceFactory复写
  System.out.println("创立了数据源");
  System.out.println(dataSource.toString());
  return dataSource;
}

论断:在创立完SqlsessionFactory时,DataSource实例就创立了.

##### 验证Connection创立机会

首先咱们先查出当初数据库的所有连接数,在数据库中执行

SELECT * FROM performance_schema.hosts;

返回数据: 显示以后连接数为1,总连接数70

  show full processlist; //显示所有的工作列表

返回:以后只有一个查问的连贯在运行

重新启动我的项目,在运行到须要执行理论的sql操作时,能够看到他曾经被代理加强了

直到此时,连接数还是没有变,阐明连贯还没有创立,咱们接着往下看.

咱们按F7进入办法,能够看到,他被代理,,这时候会执行到之前的代理办法中调用invoke办法.这里有一个判断,然而并不成立,于是进入cachedInvoker(method).invoke()办法代理执行一下操作

cachedInvoker(method).invoke()办法

  
    @Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return mapperMethod.execute(sqlSession, args);
    }

持续F7进入办法,因为咱们是单条查问select 所以会case进入select块中的selectOne

持续F7

持续F7

通过configuration.getMappedStatement获取MappedStatement

单步步过,F8后,进入executor.query办法

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

持续走到底,F7进入query办法

此时,会去缓存中查问,这里的缓存是二级缓存对象 ,生命周期是mapper级别的(一级缓存是一个session级别的),因为咱们此时是第一次运行程序,所以必定为Null,这时候会间接去查问,调用delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql)办法,F7进入这个办法

二级缓存没有获取到,又去查问了一级缓存,发现一级缓存也没有,这个时候,才去查数据库

queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);//没有缓存则去db查

F7进入queryFromDatabase办法.看到是一些对一级缓存的操作,咱们次要看doQuery办法F7进入它.

能够看到它筹备了一个空的Statement

咱们F7跟进看一下prepareStatement办法 ,发现他调用了getConnection,哎!有点眼生了,持续F7进入getConnection()办法,

又是一个getConnection()….持续F7进入transaction.getConnection()办法

又是一个getConnection()办法.判断connection是否为空.为空openConnection()否则间接返回connection;咱们F7持续跟进openConnection()办法

  protected void openConnection() throws SQLException {
  if (log.isDebugEnabled()) {
    log.debug("Opening JDBC Connection");
  }
  connection = dataSource.getConnection();//最终获取连贯的中央在这句.
  if (level != null) {
    connection.setTransactionIsolation(level.getLevel());//设置隔离等级
  }
  setDesiredAutoCommit(autoCommit);//是否主动提交,默认false,update不会提交到数据库,须要手动commit
}

dataSource.getConnection()执行完,至此一个connection才创立实现.
咱们验证一下 在dataSource.getConnection()时打一下断点.

此时数据库中的连接数仍然没变 还是1

咱们按F8 执行一步

在控制台能够看到connection = com.mysql.jdbc.JDBC4Connection@1500b2f3 实例创立结束 咱们再去数据库中看看连接数

两个连贯别离是:

不应用连接池的 UnpooledDataSource

当 <dataSource>的type属性被配置成了”UNPOOLED”,MyBatis首先会实例化一个UnpooledDataSourceFactory工厂实例,而后通过.getDataSource()办法返回一个UnpooledDataSource实例对象援用,咱们假设为dataSource。
应用UnpooledDataSource的getConnection(),每调用一次就会产生一个新的Connection实例对象。

UnPooledDataSource的getConnection()办法实现如下:

/*
UnpooledDataSource的getConnection()实现
*/
public Connection getConnection() throws SQLException
{
  return doGetConnection(username, password);
}

private Connection doGetConnection(String username, String password) throws SQLException
{
  //封装username和password成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
{
  //1.初始化驱动
  initializeDriver();
  //2.从DriverManager中获取连贯,获取新的Connection对象
  Connection connection = DriverManager.getConnection(url, properties);
  //3.配置connection属性
  configureConnection(connection);
  return connection;
}

UnpooledDataSource会做以下几件事件:

    1. 初始化驱动: 判断driver驱动是否曾经加载到内存中,如果还没有加载,则会动静地加载driver类,并实例化一个Driver对象,应用DriverManager.registerDriver()办法将其注册到内存中,以供后续应用。
    1. 创立Connection对象: 应用DriverManager.getConnection()办法创立连贯。
    1. 配置Connection对象: 设置是否主动提交autoCommit和隔离级别isolationLevel。
    1. 返回Connection对象。

咱们每调用一次getConnection()办法,都会通过DriverManager.getConnection()返回新的java.sql.Connection实例,这样当然对于资源是一种节约,为了避免反复的去创立和销毁连贯,于是引入了连接池的概念.

### 应用了连接池的 PooledDataSource
要了解连接池,首先要理解它对于connection的容器,它应用PoolState容器来治理所有的conncetion


在PoolState中,它将connection分为两种状态,闲暇状态(idle)活动状态(active),他们别离被存储到PoolState容器内的idleConnectionsactiveConnections两个ArrayList中

    • idleConnections:闲暇(idle)状态PooledConnection对象被搁置到此汇合中,示意以后闲置的没有被应用的PooledConnection汇合,调用PooledDataSource的getConnection()办法时,会优先从此汇合中取PooledConnection对象。当用完一个java.sql.Connection对象时,MyBatis会将其包裹成PooledConnection对象放到此汇合中。
    • activeConnections:流动(active)状态的PooledConnection对象被搁置到名为activeConnections的ArrayList中,示意以后正在被应用的PooledConnection汇合,调用PooledDataSource的getConnection()办法时,会优先从idleConnections汇合中取PooledConnection对象,如果没有,则看此汇合是否已满,如果未满,PooledDataSource会创立出一个PooledConnection,增加到此汇合中,并返回。

    从连接池中获取一个连贯对象的过程

    上面让咱们看一下PooledDataSource 的popConnection办法获取Connection对象的实现:

    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) {//给state对象加锁
            if (!state.idleConnections.isEmpty()) {//如果闲暇列表不空,就从闲暇列表中拿connection
              // Pool has available connection
              conn = state.idleConnections.remove(0);//拿出闲暇列表中的第一个,去验证连贯是否还无效
              if (log.isDebugEnabled()) {
                log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
              }
            } else {
              // 闲暇连接池中没有可用的连贯,就来看看沉闷连贯列表中是否有..先判断流动连贯总数 是否小于 最大可用的流动连接数
              if (state.activeConnections.size() < poolMaximumActiveConnections) {
                // 如果连接数小于list.size 间接创立新的连贯.
                conn = new PooledConnection(dataSource.getConnection(), this);
                if (log.isDebugEnabled()) {
                  log.debug("Created connection " + conn.getRealHashCode() + ".");
                }
              } else {
                // 此时连接数也满了,不能创立新的连贯. 找到最老的那个,查看它是否过期
                //计算它的校验工夫,如果校验工夫大于连接池规定的最大校验工夫,则认为它曾经过期了
                // 利用这个PoolConnection外部的realConnection从新生成一个PooledConnection
                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()) {
                    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");
                    }
                  }
                  conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
                  conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
                  conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
                  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) {
              // ping to server and check the connection is valid or not
              if (conn.isValid()) {//去验证连贯是否还无效.
                if (!conn.getRealConnection().getAutoCommit()) {
                  conn.getRealConnection().rollback();
                }
                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 + 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.");
        }
    
        return conn;
      }

    如上所示,对于PooledDataSource的getConnection()办法内,先是调用类PooledDataSource的popConnection()办法返回了一个PooledConnection对象,而后调用了PooledConnection的getProxyConnection()来返回Connection对象。

    复用连贯的过程

    如果咱们应用了连接池,咱们在用完了Connection对象时,须要将它放在连接池中,该怎么做呢? 如果让咱们来想的话,应该是通过代理Connection对象,在调用close时,并不真正敞开,而是丢到治理连贯的容器中去. 要验证这个想法 那么 来看看Mybatis帮咱们怎么实现复用连贯的.

    class PooledConnection implements InvocationHandler {
    
      //......
      //所创立它的datasource援用
      private PooledDataSource dataSource;
      //真正的Connection对象
      private Connection realConnection;
      //代理本人的代理Connection
      private Connection proxyConnection;
    
      //......
    }

    PooledConenction实现了InvocationHandler接口,并且,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);
      }

    实际上,咱们调用PooledDataSource的getConnection()办法返回的就是这个proxyConnection对象。
    当咱们调用此proxyConnection对象上的任何办法时,都会调用PooledConnection对象内invoke()办法。
    让咱们看一下PooledConnection类中的invoke()办法定义:

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        //当调用敞开的时候,回收此Connection到PooledDataSource中
        if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
          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);
          }
        }
      }

    论断:当咱们应用了pooledDataSource.getConnection()返回的Connection对象的close()办法时,不会调用真正Connection的close()办法,而是将此Connection对象放到连接池中。调用dataSource.pushConnection(this)实现

    protected void pushConnection(PooledConnection conn) throws SQLException {
    
        synchronized (state) {
          state.activeConnections.remove(conn);
          if (conn.isValid()) {
            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();
              if (log.isDebugEnabled()) {
                log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
              }
              state.notifyAll();
            } else {
              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++;
          }
        }
      }

    关注公众号:java宝典

    评论

    发表回复

    您的邮箱地址不会被公开。 必填项已用 * 标注

    这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理