最近在工作中遇到一个sqlSession的使用问题。老项目不知道为什么单独搞了一个配置文件使用Mybatis。

在测试中发现一个性能问题:执行单独使用Mybatis查询使。每查询一定的次数就会出现卡顿。通过跟踪发现没次卡顿还都是20s左右。

但是为什么会这样呢百思不得其姐。最后写了个dome测试也是只使用了Mybatis:

贴代码:

public class SqlSessionTest {
    public static void main(String[] args) throws IOException {
        //加载mybatis配置文件(也会加载关联的映射文件)
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        //构建sqlSession工厂
        //静态代理
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        for (int i = 0; i <21 ; i++) {
            long l = System.currentTimeMillis();
            SqlSession sqlSession1 = sqlSessionFactory.openSession();
            MyCat myCat1  = sqlSession1.selectOne("com.wly.dao.MyCatDao.getById", 1L);
            System.out.println(System.currentTimeMillis()-l);
            System.out.println(myCat1.getName()+i+"============================"+sqlSession1.getConnection()+"=======================");
        }
    }
}

输出:


305
小电驴0============================com.mysql.jdbc.JDBC4Connection@100fc185=======================
23
小电驴1============================com.mysql.jdbc.JDBC4Connection@548a9f61=======================
9
小电驴2============================com.mysql.jdbc.JDBC4Connection@20e2cbe0=======================
65
小电驴3============================com.mysql.jdbc.JDBC4Connection@482f8f11=======================
9
小电驴4============================com.mysql.jdbc.JDBC4Connection@724af044=======================
10
小电驴5============================com.mysql.jdbc.JDBC4Connection@55f3ddb1=======================
9
小电驴6============================com.mysql.jdbc.JDBC4Connection@2b98378d=======================
8
小电驴7============================com.mysql.jdbc.JDBC4Connection@14acaea5=======================
9
小电驴8============================com.mysql.jdbc.JDBC4Connection@4501b7af=======================
24
小电驴9============================com.mysql.jdbc.JDBC4Connection@3ffc5af1=======================
20001
小电驴10============================com.mysql.jdbc.JDBC4Connection@100fc185=======================
1
小电驴11============================com.mysql.jdbc.JDBC4Connection@548a9f61=======================
8
小电驴12============================com.mysql.jdbc.JDBC4Connection@20e2cbe0=======================
1
小电驴13============================com.mysql.jdbc.JDBC4Connection@482f8f11=======================
1
小电驴14============================com.mysql.jdbc.JDBC4Connection@724af044=======================
2
小电驴15============================com.mysql.jdbc.JDBC4Connection@55f3ddb1=======================
2
小电驴16============================com.mysql.jdbc.JDBC4Connection@2b98378d=======================
0
小电驴17============================com.mysql.jdbc.JDBC4Connection@14acaea5=======================
1
小电驴18============================com.mysql.jdbc.JDBC4Connection@4501b7af=======================
1
小电驴19============================com.mysql.jdbc.JDBC4Connection@3ffc5af1=======================
20002
小电驴20============================com.mysql.jdbc.JDBC4Connection@100fc185=======================


发现问题,每第十一条就会卡。

之前了解到sqlSession在执行数据库操作的时候才会去获取连接。就是JDBC的Connection。然后跟踪代码sqlSession1.selectOne("com.wly.dao.MyCatDao.getById", 1L);此代码会调用到Mybatis的PooledDataSource类的popConnection(String username, String password)方法获取数据库连接,中间的调用过程不累赘了,。这个方法比较长这里贴上获取Connection的代码


    


while (conn == null) {
      synchronized (state) {
        // 检测空闲连接集合(idleConnections)是否为空
        if (!state.idleConnections.isEmpty()) {
          // Pool has available connection
          // idleConnections 不为空,表示有空闲连接可以使用
          conn = state.idleConnections.remove(0);
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else {
          // Pool does not have available connection
          // 暂无空闲连接可用,但如果活跃连接数还未超出限制
          //(poolMaximumActiveConnections),则可创建新的连接
          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");
                }
              }
              // 创建一个新的 PooledConnection,注意,此处复用
              // oldestActiveConnection 的 realConnection 变量
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
              // 复用 oldestActiveConnection 的一些信息,注意
              // PooledConnection 中的 createdTimestamp 用于记录
              // Connection 的创建时间,而非 PooledConnection
              // 的创建时间。所以这里要复用原连接的时间信息。
              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;
              }
            }
          }
        }

从连接池中获取连接首先会遇到两种情况:

1. 连接池中有空闲连接

2. 连接池中无空闲连接对于第一种情况,处理措施就很简单了,把连接取出返回即可。

对于第二种情况,则要进行细分,会有如下的情况:

1. 活跃连接数没有超出最大活跃连接数

2. 活跃连接数超出最大活跃连接数对于上面两种情况,

第一种情况比较好处理,直接创建新的连接即可。至于第二种情况,需要再次进行细分。

1. 活跃连接的运行时间超出限制,即超时了

2. 活跃连接未超时

对于第一种情况,我们直接将超时连接强行中断,并进行回滚,然后复用部分字段重新创建 PooledConnection 即可。对于第二种情况,目前没有更好的处理方式了,只能等待了。

我的代码每到第十条就会选择第 二、二、二种情况。所以就只能等待其它的连接超时了。恰好默认的超时时间是2000ms。

如果我们使用Spring来管理Mybatis的话这些操作是不会发生的,Spring会自动事务提交,并且会自动关闭sqlSession


解决方法:

①单独使用Mybatis时需要自己手动提交,sqlSession.commit(),如果只是查询的话可以不提交。但是不管什么操作最后记得需要关闭sqlSession资源,sqlSession.close();

②控制sqlSession的数量,但需要注意sqlSession是线程不安全的