文章目录

  • Mybatis 的连接池
  • 1. 连接池介绍
  • 2. Mybatis 的连接池
  • 2.1 Mybatis 连接池的分类:
  • 2.2 Mybatis 中使用 unpooled 配置连接池的原理分析
  • 2.3 Mybatis 中使用 POOLED 配置连接池的原理分析


Mybatis 的连接池

1. 连接池介绍

我们在实际开发中都会使用连接池。

因为连接池可以减少我们获取连接所耗费的时间。

其实连接池就像一个容器,它会把连接都初始化出来放在一个容器里面,这种容器大家可以理解为一个瓶子,想要用就取一个。当一个应用或者线程取出第一个连接并且占用该连接,那么连接池中的连接的顺序就会重新排序(挨个向前进行添补,比如5号连接变为4号连接)。

当一个线程或者应用解除对一个连接的占用,那么,这个连接会重新回到连接池中。同时排在容器序列的最后一个。

容器其实就是一个集合对象,该集合必须是线程安全的,不能两个线程拿到同一个连接。该集合还必须实现队列的特性:先进先出。

2. Mybatis 的连接池

2.1 Mybatis 连接池的分类:

  • Mybatis将自己的数据源分为三类:
  • UNPOOLED - 不使用连接池的数据源
  • POOLED - 使用连接池的数据源
  • JNDI - 使用 JNDI 实现的数据源
  • Mybatis 连接池提供了三种方式的配置:
  • 配置的位置:
  • 主配置文件中的DataSource标签,type属性就是表示采用何种连接池方法。
  • type 属性的取值:
  • POOLED - 采用传统的 javax.sql.DataSource 规范中的连接池,mybatis 中有针对性规范的实现
  • UNPOOLED - 采用传统的获取连接的方法,虽然也实现了 javax.sql.DataSource 接口,但是并没有使用 池 的思想。
  • JNDI - 采用服务器提供的 JDNI 技术实现,来获取 DataSource 对象,不同的服务器所能拿到的 DataSource 是不一样的。
    注意:如果不是 web 或者 maven 的 war 工程,是不能使用的。
    这里使用的 Tomcat 服务器,采用的连接池就是 dbcp 连接池。

2.2 Mybatis 中使用 unpooled 配置连接池的原理分析

  • 首先打开 JDBC 连接
  • 然后关闭 JDBC 中的事务的自动提交
  • 准备 sql 语句
  • 执行 SQL 语句然后返回结果
  • 最后关闭 JDBC 的连接

上面的步骤说明,如果在主配置文件中,将配置连接池信息的 type 类型设置为 unpooled, 那么每次使用都会创建一个新的连接:

<!--配置连接池-->
            <dataSource type="UNPOOLED">
                <property name="driver" value="${jdbc.driver}"></property>
                <property name="url" value="${jdbc.url}"></property>
                <property name="username" value="${jdbc.username}"></property>
                <property name="password" value="${jdbc.password}"></property>
            </dataSource>

在 Mybatis 内部分别定义实现了 javax.sql.DataSource 接口的 UnpooledDataSource, PooledDataSource 类表示 UNPOOLED、POOLED类型的数据源。

进入 UnpooledDataSource 类:

public class UnpooledDataSource implements DataSource {}

它(PooledDataSource类同样)实现了 DataSource 接口,而 DataSource 接口就是 javax.sql 的DataSource 也就是 jdbc 规范中的连接定义。其中也必定有一个方法,叫 getConnection。

然后,返回 UnpooledDataSource 类,寻找 getConnection 对象:

@Override
  public Connection getConnection() throws SQLException {
    return doGetConnection(username, password);
  }

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

getConnection 方法在内部调用了 doGetConnection 方法,查看 doGetConnection 方法:

private Connection doGetConnection(String username, String password) throws SQLException {
    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);
  }

发现,在 doGetConnection 方法的内部,创建了一个Properties 对象,并且将用户名和密码设置进去,然后再次调用 doGetConnection 的重载方法,传递一个 props 属性过去。继续跟踪:

private Connection doGetConnection(Properties properties) throws SQLException {
    initializeDriver();
    Connection connection = DriverManager.getConnection(url, properties);
    configureConnection(connection);
    return connection;
  }

在这个方法中,调用了 initializeDriver 方法,同时设置和返回了连接,而这个 initializeDriver 方法,实际上就是初始化驱动。查看 initializeDriver 方法:

private synchronized void initializeDriver() throws SQLException {
    if (!registeredDrivers.containsKey(driver)) {
      Class<?> driverType;
      try {
        if (driverClassLoader != null) {
          driverType = Class.forName(driver, true, driverClassLoader);
        } else {
          driverType = Resources.classForName(driver);
        }
        // DriverManager requires the driver to be loaded via the system ClassLoader.
        // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html
        Driver driverInstance = (Driver)driverType.newInstance();
        DriverManager.registerDriver(new DriverProxy(driverInstance));
        registeredDrivers.put(driver, driverInstance);
      } catch (Exception e) {
        throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
      }
    }
  }

该方法调用了 Class.forName 方法,这个方法不就是用来注册驱动用的吗?

那么,使用 unpooled 来连接数据库就很明显了:

每次都会注册驱动,获取连接。

2.3 Mybatis 中使用 POOLED 配置连接池的原理分析

进入 PooledDataSource 类,该类在获取连接时,调用的是 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.isEmpty()) {
          // Pool has available connection
          conn = state.idleConnections.remove(0);
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else {
          // Pool does not have available 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 {
            // Cannot create new connection
            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
            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 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;
  }

这段代码描述了这么一段过程:

  • 首先,如果连接为空,那么使用 synchronized 的代码块,该代码块锁定是 state (PoolState)的锁,保证了线程的安全。

Springboot 配置mybatis 连接池 数目 mybatis 连接池原理_框架

  • 如果池中有空闲的连接(if (!state.idleConnections.isEmpty()){…}),那么就拿出第一个连接,供应用使用(conn = state.idleConnections.remove(0))。
  • 如果其中没有连接(else{…}),那么会进行判断:
  • 如果活动连接池中活动的连接数量少于设定的最大值(if (state.activeConnections.size() < poolMaximumActiveConnections) {…}),在这个条件下,会创建一个连接,然后扔进活动连接池,并提供给我们使用。
  • 如果活动连接池中也没有地儿了(不能再创建新的连接了),活动连接池会拿出一个 oldestActiveConnection,获得连接中最老的一个连接出来(PooledConnection oldestActiveConnection = state.activeConnections.get(0);),然后进行一些相关的清理和设置操作(ooledConnection oldestActiveConnection = state.activeConnections.get(0); 后面的一大段代码,有兴趣的可以自行去分析),供我们使用。
  • 通过这里的activeConnections.get(0),可以发现,其实,通过 POOLED 来创建连接时,Mybatis 会创建两个池:一个空闲池,一个活动池(一样也是容器 protected final List activeConnections = new ArrayList<>(); 同样在 PoolState 类中定义)。