《深入理解Mybatis原理》 02-Mybatis数据源与连接池

    对于ORM框架而言,数据源的组织是一个非常重要的一部分,这直接影响到框架的性能问题。本文将通过对MyBatis框架的数据源结构进行详尽的分析,并且深入解析MyBatis的连接池。

    本文首先会讲述MyBatis的数据源的分类,然后会介绍数据源是如何加载和使用的。紧接着将分类介绍UNPOOLED、POOLED和JNDI类型的数据源组织;期间我们会重点讲解POOLED类型的数据源和其实现的连接池原理。
 

本文结构如下:

  •         一、MyBatis数据源DataSource分类
  •         二、数据源DataSource的创建过程
  •         三、 DataSource什么时候创建Connection对象
  •         四、不使用连接池的UnpooledDataSource
  •         五、为什么要使用连接池?
  •         六、使用了连接池的PooledDataSource

 

一、Mybatis数据源分类

   mybatis数据源实现类在mybatis的dataSource包中:

   Mybatis将数据源分为三种:

   JNDI 数据源 : 使用JNDI方式数据源

   POOLED数据眼:  使用连接池数据源

   UNPOOLED 数据源 : 不使用连接池数据源

即:

 

相应的Mybatis内部分别以实现  javax.sql.DataSource 接口的 PooledDataSource和 UnPooledDataSource 实现 POOLED和UNPOOLED数据源。(关于数据源创建细节请看下面章节)

JNDI数据源则通过 javax.naming.Context 上下文生成数据源。

 

二、数据源DataSource创建过程

 数据源配置如下:

 <dataSource type="UNPOOLED">
     <property name="driver" value="com.mysql.jdbc.Driver"/> 
     <property name="url" value="jdbc:mysql://localhost:3306/pmdb"/>
     <property name="username" value="root"/>
     <property name="password" value="123456"/>
 </dataSource>  

  Mybatis数据源的创建过程:

  Mybatis初始化阶段 就会创建好数据源,具体创建数据源的时机发生在解析mybatis XML配置文件<environments></environments>  节点下的<dataSource></dataSouorce>节点:

 //context :dataSource节点数据
 private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
      //获取dataSource配置的类型 (POOLED、UNPOOLED、JNDI)
      String type = context.getStringAttribute("type");
      //将dataSource下的username、password等信息解析为Properties
      Properties props = context.getChildrenAsProperties();
      //根据dataSource的type类型(别名机制)获取到对应的DateSource实现类,并实例化该类
      DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");
  }

  创建DataSource最关键的一步在:  DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();

  打开该方法resolveClass方法实现,看它到底做了什么:

public <T> Class<T> resolveAlias(String string) {
    try {
      if (string == null) {
        return null;
      }
      String key = string.toLowerCase(Locale.ENGLISH);
      Class<T> value;
      if (TYPE_ALIASES.containsKey(key)) {
        value = (Class<T>) TYPE_ALIASES.get(key);
      } else {
        value = (Class<T>) Resources.classForName(string);
      }
      return value;
    } catch (ClassNotFoundException e) {
      throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
    }
  }

     resolveClass方法核心功能就是根据XML dataSource节点配置的type属性找到对应的实现类:

    如上图所示:

     根据配置的type别名找到Factory,然后创建出对应的DataSource

  • JNDI : JndiDataSourceFactory
  • POOLED: PooledDataSourceFactory
  • UNPOOLED: UnpooledDataSourceFactory

 

Mybatis创建DataSource之后会将其放在Configuration的Environment中,供以后使用。

 

三、DataSource什么时候创建Connection对象

  InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
  SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
  SqlSession sqlSession = sqlSessionFactory.openSession();
  User user = (User) sqlSession.selectOne("selectByPrimaryKey", 1);

    如上图所示,前三行代码都不会去创建javax.sql.Connection,当执行到 selectOne("selectByPrimaryKey", 1);时,才会去真正创建Connection对象:

 @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException {
    Statement stmt = null;
    try {
      flushStatements();
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

 那对于UNPOOLED类型DataSource的实现UnpooledDataSource是怎么样实现getConnection方法呢?请看一下节。

 

四、不使用连接池的UnpooledDataSource

  //UnpoolDataSource 创建Connection对象
private Connection doGetConnection(Properties properties) throws SQLException {
    //1. 初始化驱动
    initializeDriver();
    //2. 创建Connection对象
    Connection connection = DriverManager.getConnection(url, properties);
    //3. 配置Connection
    configureConnection(connection);
    return connection;
  }

  如上代码所示,流程如下:

  •   1. 初始化驱动 : 判断驱动是否加载到内存中,若有则直接取出,否则创建驱动
  •   2. 创建Connecion : 调用DriverManager创建相应的Connection对象
  •   3. 配置Connection: 配置Connection对象一些默认配置项
  •   4. 返回DataSource对象: 返回创建好的Connection对象供以使用

  总结:从上述的代码中可以看到,我们每调用一次getConnection()方法,都会通过DriverManager.getConnection()返回新的java.sql.Connection实例。

 

五、为什么要使用连接池?

   

public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {

        long start = System.currentTimeMillis();

        Class.forName("com.mysql.jdbc.Driver");
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mall", "root", "123456");
        System.out.println("创建Connection对象耗时 : " + String.valueOf(System.currentTimeMillis() - start));

        String sql = "select *  from mmall_user where id = 1";

        start = System.currentTimeMillis();

        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery(sql);

        System.out.println("查询语句耗时:" + String.valueOf(System.currentTimeMillis() - start));

        while(resultSet.next()){
            System.out.println("id = " + resultSet.getInt(1) + "  username = " + resultSet.getString(2));
        }

        resultSet.close();
        statement.close();
        connection.close();


    }

    

创建Connection对象耗时 789ms, 而查询语句才耗时7ms.(不排除数据库数据少的原因,但是查询耗时一般不会超过789ms)

一次查询请求创建Connection对象耗时789ms。要知道100ms对于Java来说都是很奢侈的。(一个Connection对象耗时 700ms,10000 * 700 =  116分钟,10000次请求只创建对象就耗时116分钟,这是根本不能接受的)

所以使用连接池是非常有必要的。

 

六、使用了连接池的PooledDataSource

   了解连接池之前,先了解两个参数概念:

   idleConnections : 空闲Connection对象,当其他请求需要创建Connection时,直接到ideaConnection取出一个连接,可以减少资源、耗时。

   activeConnections : 活动Connection对象,记录当前正在被请求所使用的Connection对象,当一次请求使用完一个Connection时,不将其立即销毁,而是放到idleConnection缓存池里面。

 

  

  对于UnpooledDataSource每次请求都会创建一个新的Connection对象,当请求结束后会执行Connection.cloes()方法关闭该Connection.

     PooledDataSource是如何创建的Connection的呢?

@Override
  public Connection getConnection() throws SQLException {
    return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
  }

  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return popConnection(username, password).getProxyConnection();
  }

  数据源type设置为 POOLED,当实例化DataSource时会根据别名实例化出 PooledDataSource对象。

  当调用getConnection方法创建Connection时,最终会调用 popConnecion方法并返回一个代理对象。

 

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) {
        //判断连接池中是否还有空闲Connection对象,若有则直接返回一个Connection
        if (!state.idleConnections.isEmpty()) {
          // Pool has available connection
          conn = state.idleConnections.remove(0);
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else {//没有空闲Connection对象
          
          //当前活动对象个数是小于最大活动数量 则会生成一个新的Connection对象
          if (state.activeConnections.size() < poolMaximumActiveConnections) {
            // Can create new connection
            conn = new PooledConnection(dataSource.getConnection(), this);
            if (log.isDebugEnabled()) {
              log.debug("Created connection " + conn.getRealHashCode() + ".");
            }
          } else {//判断老的活动对象是否超过poolMaximumCheckoutTime时间
            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
            //超过poolMaximumCheckoutTime时间,则尝试结束该Connection对象线程,并返回重用Connection
            if (longestCheckoutTime > poolMaximumCheckoutTime) {
              // Can claim overdue connection
              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 happend.
                     Wrap the bad connection with a new PooledConnection, this will help
                     to not intterupt current executing thread and give current thread a
                     chance to join the next competion 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 {//没有超时,则等待该Connection线程结束
              // 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;
  }

综上所述,大致流程如下:

  • 1. 若IdleConnection中是否有空闲的连接对象,则直接返回Connection
  • 2. 判断当前activeConnection数量是否小于poolMaximumActiveConnections(活动连接最大数量),若小于,则和Unpooled方式一样创建新的Connection对象并返回。若大于,则会判断当前所有活动连接的占用时间是否超时,若超时则停止该Connection,并直接返回供其他请求使用。若没有超时,则等待Connection使用完毕后再返回。

 

  连接池对Connection.close()的处理:

PooledDataSource中除了popConnection方法,还有一个pushConnection方法

pushConnection方法会将使用完毕的Connection放入idleConnections缓存池中,供其他请求继续使用。

传统的jdbc连接使用完Connection之后,会手动执行Connection.cloes()方法关闭连接。

Pooled连接池为了重复利用Connection减少不必要的开销,对Connection.cloes做了动态代理。

也就是说,在Pooled模式下,若我们手动执行connecion.cloes(),实际上并不会执行原生Connection.close方法。而是通过PooledConnection对原生Connection做动态代理,把close方法映射到 pushConnection方法上:

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    //若执行close方法,实际上会代理执行pushConnection方法
    if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
      dataSource.pushConnection(this);
      return null;
    } else {
      try {
        if (!Object.class.equals(method.getDeclaringClass())) {
          // issue #579 toString() should never fail
          // throw an SQLException instead of a Runtime
          checkConnection();
        }
        return method.invoke(realConnection, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }

 

以上就是本文 《深入理解Mybatis原理》 02-Mybatis数据源与连接池 的全部内容,

上述内容如有不妥之处,还请读者指出,共同探讨,共同进步!

@author : jackcheng1117@163.com

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值