mybatis 连接池探究

起因是目前再看 《Java并发编程的艺术》 并发基础那章实现了一个简单的连接池,虽然只是为了展示等待/通知,但是我对平时常用的mybatis的 连接池是怎么实现的感到很好奇,所以就有了这次的记录。框架平时用的多,但是源码还真的没怎么看,这一篇也算是为阅读源码起一个头。

mybatis版本是:3.4.0
连接池核心类:org.apache.ibatis.datasource.pooled.PooledDataSource
测试代码是:
简单粗暴的抛出Exception 是为了移除异常处理,关注主流程

    public static void main(String[] args) throws Exception {
        //构建一个连接池 有很多重载的构造函数
        PooledDataSource pool = new PooledDataSource(
                "com.mysql.jdbc.Driver",
                "jdbc:mysql://127.0.0.1:3308/db_name",
                "user_name",
                "password");
         
         //从连接池取出一个连接,这个是重头戏
        Connection connection = pool.getConnection();
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery("SELECT * FROM checking_in_log LIMIT 1");
        if (resultSet.next()) {
            String id = resultSet.getString("id");
            System.out.println(id);
        }
        resultSet.close();
        statement.close();
        connection.close();
    }

首先跟着测试代码走一遍:

public PooledDataSource(String driver, String url, String username, String password) {
    dataSource = new UnpooledDataSource(driver, url, username, password);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }

第一行代码就是初始化这个PooledDataSource对象了,里面做了2件事,初始化一个UnpooledDataSource对象,还有把expectedConnectionTypeCode设置为("" + url + username + password).hashCode();

UnpooledDataSource和PoolState这2个对象是PooledDataSource最关键的2个对象了,可以说PooledDataSource是对UnpooledDataSource的使用上的封装,数据库连接参数,设置参数大多都是通过调用UnpooledDataSource进行的。

这是UnpooledDataSource的方法
getConnection
getConnection
setLoginTimeout
getLoginTimeout
setLogWriter
getLogWriter
getDriverClassLoader
setDriverClassLoader
getDriverProperties
setDriverProperties
getDriver
setDriver
getUrl
setUrl
getUsername
setUsername
getPassword
setPassword
isAutoCommit
setAutoCommit
getDefaultTransactionIsolationLevel
setDefaultTransactionIsolationLevel

而PoolState作为一个数据对象,连接池就保存在里面 这个对象里面的方法都标记了synchronized,而对象里面的统计字段基本都是被多线程竞争的,我觉得可以改为使用JUC的工具类atomicLong。毕竟竞争压力大,synchronized的偏向锁 轻量锁无法起到一个优化的作用。可能这个类在jdk1.5以前就创建了。

这是里面的字段
    //PooledDataSource是通过new PoolState(this)得出的PollState 
  protected PooledDataSource dataSource;
  //空闲连接池
  protected final List idleConnections = new ArrayList();
  //活动中的连接池
  protected final List activeConnections = new ArrayList();
  //请求总数
  protected long requestCount = 0;
 //累计请求时间
  protected long accumulatedRequestTime = 0;
  //累计获取时间(连接池被别的线程拿走执行的时间)
  protected long accumulatedCheckoutTime = 0;
  //超时执行连接数计数,当连接执行超过了poolMaximumCheckoutTime(20秒)而这时候有请求竞争这个连接的时候这个值加一,同时连接会rollback,然后给新的竞争线程去使用
  protected long claimedOverdueConnectionCount = 0;
  //这个是accumulatedCheckoutTime是一样的
  protected long accumulatedCheckoutTimeOfOverdueConnections = 0;
  //累计等待时间,就是连接都被使用了,而又没办法抢别人超时执行的连接的时候就只能乖乖等待了
  protected long accumulatedWaitTime = 0;
  //每次等待连接就加一
  protected long hadToWaitCount = 0;
  //当获取到一个已经失效的连接的时候就加一
  protected long badConnectionCount = 0;
//取出连接所调用的方法
 @Override
  public Connection getConnection() throws SQLException {
    return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
  }

方法的返回类型是:java.sql.Connection
通过调用popConnection返回org.apache.ibatis.datasource.pooled.PooledConnection这个mybatis的封装类,然后调用它的getProxyConnection获得java.sql.Connection
PooledConnection实现了InvocationHandler接口,然后内部有2个Connection对象

private Connection realConnection;
private Connection proxyConnection;

realConnection是真实的连接,在构造器传入
proxyConnection是在构造器里面通过反射代理创建的一个对象
private static final Class[] IFACES = new Class[] { Connection.class };
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
通过源码发现,之所以有proxyConnection是为了拦截调用Connection.close()
在PooledConnection的invoke实现方法(InvocationHandler接口)可以看到

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    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);
      }
    }
  }

如果是close方法,调用后并不会真正去执行Connection.close(),而是把连接放回连接池,至于其他方法都是通过realConnection去执行的。

接下来看看2个核心方法,拿和还
pushConnection
popConnection

//取出一个连接
 private PooledConnection popConnection(String username, String password) throws SQLException {
    //是否需要等待连接标记
    boolean countedWait = false;
    //返回对象
    PooledConnection conn = null;
    //方法开始的时间,用来计算请求连接耗费时间的
    long t = System.currentTimeMillis();
    //获取到坏连接的次数
    int localBadConnectionCount = 0;
    
    //如果没获取到连接就一直重试,有3种情况会退出循环,成功获取到连接
    //调用wait(timeout)的时候抛出//InterruptedException异常
   //localBadConnectionCount > (poolMaximumIdleConnections + 3)的时候抛出异常,这个意思是获取到失效连接次数已经大于最大空闲连接数+3次时,不再尝试面对现实
    while (conn == null) {
      //先对 PoolState这个对象加锁,前面也有说,这个对象在连接池是唯一的。
      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 {
          //下面那行源码英文注释其实已经说明了问题,当活动连接还没达到最大活动连接限制的时候直接new一个新连接
          // 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 {
            //在不能创建新连接的时候就要进行下面的操作了,如果第1个连接的执行已经超时就干掉它取而代之,否则就乖乖等待
            // Cannot create new connection
            //获取最初创建的连接
            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
            //获取连接的执行时间,就是连接被线程持有的时间
            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
            //判断是否已经执行超时,默认是20秒
            if (longestCheckoutTime > poolMaximumCheckoutTime) {
              // Can claim overdue connection
              //这4个在前面已经解释了
              state.claimedOverdueConnectionCount++;
              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
              state.accumulatedCheckoutTime += longestCheckoutTime;
              //就旧的连接从活动连接列表移除
              state.activeConnections.remove(oldestActiveConnection);
              //下面这个就很残暴了,如果连接不是自动提交的,调用它的回滚。如果人家正在执行就懵逼了,所以长时间执行的大sql poolMaximumCheckoutTime还是有必要设置一下
              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                try {
                  oldestActiveConnection.getRealConnection().rollback();
                } catch (SQLException e) {
                  log.debug("Bad connection. Could not roll back");
                }  
              }
              //这里相当于把旧衣服脱掉,穿上新衣服。 真正的Connection对象还是没变的
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
              //把旧对象标记为失效
              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.");
                }
                //下面几行就是并发编程的内容了,poolTimeToWait默认值是20秒
                long wt = System.currentTimeMillis();
                //wait 放弃掉自己的锁,然后开始等待被notify唤醒或者达到超时时间,这里要注意的是,如果已经到了超时时间,而别的线程还没有释放锁,照样是醒不来的
                state.wait(poolTimeToWait);
                state.accumulatedWaitTime += System.currentTimeMillis() - wt;
              } catch (InterruptedException e) {
                //中断异常,跳出
                break;
              }
            }
          }
        }

        //经历了前面的磨难,终于快到收官了 如果连接对象不为null且有效
        //conn.isValid() 方法做了:valid && realConnection != null && dataSource.pingConnection(this);
        if (conn != null) {
          if (conn.isValid()) {
            //又是这一步操作,简单粗暴/(ㄒoㄒ)/~~  
            if (!conn.getRealConnection().getAutoCommit()) {
              conn.getRealConnection().rollback();
            }
            //这一步在构造器那里有类似的,连接的hashCode
            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 + 3)) {
              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.");
            }
          }
        }
      }

    }

    //这个循环体外的判断,看上去主要还是防止那个wait抛出中断异常的break
    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;
  }

长长的popConnection后面还有一个短短的pushConnection 添加链接到

 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对象执行wait的线程可以继续了(让可能的在等待获取连接的wait对象动一下)
          state.notifyAll();
        } else {
          //如果空闲列表满了,或者这个连接对象不属于这个池创建的 真正的关闭这个连接 然后标记为失效
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback();
          }
         //执行真实连接对象的close,返回给用户的连接都是ProxyConnection那个动态代理的对象,close只是返回连接到池。
          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++;
      }
    }
  }

大概就是这些了,有一些比较蛋疼的地方,比如PoolState的
protected final List idleConnections = new ArrayList();
protected final List activeConnections = new ArrayList();
空闲列表和活动列表,这2个列表是一直在变化的,所以用ArrayList性能就有点问题了。而源码里面更多只是对头和尾进行操作,这里用LinkedList可能会更好?

你可能感兴趣的:(mybatis 连接池探究)