java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Mybatis Cursor避免OOM异常

Mybatis的Cursor避免OOM异常的方法详解

作者:Azir12138

在Mybatis中,有一个特殊的对象Cursor,这个对象的注释上清晰的说明了,这个类的用途,在Mybatis中使用Cursor非常简单,只要在Mapper文件中将方法的返回值设置成Cursor<T>即可,本文给大家介绍了Mybatis的Cursor避免OOM异常的方法,需要的朋友可以参考下

Cursor是啥

研究Cursor如何避免OOM异常之前,先了解一下Cursor是啥。
在Mybatis中,有一个特殊的对象Cursor,这个对象的注释上清晰的说明了,这个类的用途。

/**
 * Cursor contract to handle fetching items lazily using an Iterator.
 * Cursors are a perfect fit to handle millions of items queries that would not normally fits in memory.
 * If you use collections in resultMaps then cursor SQL queries must be ordered (resultOrdered="true")
 * using the id columns of the resultMap.
 *
 * @author Guillaume Darmont / guillaume@dropinocean.com
 */

Cursors are a perfect fit to handle millions of items queries that would not normally fits in memory. Cursor非常适合处理通常不适合内存的数百万项查询

甚至在说明中还着重的说明了是非常适合的。
这个类的作用其实就是为了避免在数据库批量查询到大数据时导致程序OOM错误。

如何使用Cursor

Mybatis中使用Cursor非常简单,只要在Mapper文件中将方法的返回值设置成Cursor<T>即可。

@Select("SELECT * FROM log")
Cursor<Log> selectAll();

注意:要是想在SpringBoot中使用Cursor的话,需要下面方式二选一,不然的话使用Cursor会报错。

之所以需要额外配置是因为在SpringBoot中,Mybatis的SqlSession生命周期只在Mapper方法中,并且在关闭SqlSession时,还会将SqlSession**绑定的Cursor关闭,**所以就需要延长SqlSession的存活时间了。

Cursor原理

解析Mapper方法返回值

在Mybatis中,调用Mapper方法时,会由MapperProxy进行方法的代理。此时就会根据具体的方法进行不同的解析

public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
    // 解析方法返回值
    Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
    if (resolvedReturnType instanceof Class<?>) {
        this.returnType = (Class<?>) resolvedReturnType;
    } else if (resolvedReturnType instanceof ParameterizedType) {
        this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
    } else {
        this.returnType = method.getReturnType();
    }
    this.returnsVoid = void.class.equals(this.returnType);
    this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
    // 方法是否返回Cursor类型
    this.returnsCursor = Cursor.class.equals(this.returnType);
    this.returnsOptional = Optional.class.equals(this.returnType);
    this.mapKey = getMapKey(method);
    this.returnsMap = this.mapKey != null;
    this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
    this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
    this.paramNameResolver = new ParamNameResolver(configuration, method);
}

根据Cursor返回值调用selectCursor

解析Mapper方法得到返回值后,就会根据返回值的类型来决定具体调用的查询方法。

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
    // ---------- 其他查询----------------
        case SELECT:
            if (method.returnsVoid() && method.hasResultHandler()) {
                executeWithResultHandler(sqlSession, args);
                result = null;
            } else if (method.returnsMany()) {
                result = executeForMany(sqlSession, args);
            } else if (method.returnsMap()) {
                result = executeForMap(sqlSession, args);
            } else if (method.returnsCursor()) {
                // Cursor返回类型
                result = executeForCursor(sqlSession, args);
            } else {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(command.getName(), param);
                if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
                    result = Optional.ofNullable(result);
                }
            }
            break;
    // ---------- 其他查询----------------
    return result;
}

构建statement

使用上面解析Mapper方法后得到的Sql,从数据库链接中创建一个PreparedStatement并填充对应的参数值。

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
}

封装Cursor

在调用的最后,会将从数据库得到的ResultSet以及Mybatis内部ResultSetHandler封装成Cursor对象供用户使用。

public <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling cursor results").object(mappedStatement.getId());
    
    ResultSetWrapper rsw = getFirstResultSet(stmt);
    
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    if (resultMapCount != 1) {
        throw new ExecutorException("Cursor results cannot be mapped to multiple resultMaps");
    }
    
    ResultMap resultMap = resultMaps.get(0);
    return new DefaultCursor<>(this, resultMap, rsw, rowBounds);
}

为啥能避免内存溢出

在讨论这个问题前,我们可以看一下在Mybatis中,Cursor返回值的查询以及批量查询的实际调用逻辑。

Cursor查询

  @Override
  protected <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
      throws SQLException {
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql);
    Statement stmt = prepareStatement(handler, ms.getStatementLog());
    Cursor<E> cursor = handler.queryCursor(stmt);
    stmt.closeOnCompletion();
    return cursor;
  }

批量查询

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
      BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler,
          boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

可以对比一下两个实际执行的方法,比较明显的区别就是在批量搜索中,显式关闭了打开的Statement,而在Cursor查询中,并没有关闭与数据库的连接。归根结底就是因为Cursor在使用上就是在操作原生的Statement,故不能在查询后关闭。
另外,在批量查询的handler.query(stmt, resultHandler)方法中,是获取本次查询所有数据后返回的,而这就会导致在大批量数据时塞爆内存导致OOM了。
然而在Cursor查询中,并不会获取全部数据后返回,而是根据用户操作来获取对于数据,自然而然也就不会塞爆内存了。

总结

类型如何获取数据返回值是否关闭Statement、ResultSet
Cursor用户自行根据Cursor迭代器获取Cursor游标类型不关闭,需要一直持有
普通搜索Mybatis内部操控JDBC指针,获取所有查询数据后返回。具体实体类型获取完数据后关闭

以上就是Mybatis的Cursor避免OOM异常的方法详解的详细内容,更多关于Mybatis Cursor避免OOM异常的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文