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

36次阅读

共计 14028 个字符,预计需要花费 36 分钟才能阅读完成。

对于 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 宝典

    正文完
     0