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

 更新时间:2024年06月27日 08:53:07   作者: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会报错。

  • 手动创建SqlSession
  • 在调用Mapper方法的方法上标注@Transactional事务注解。

之所以需要额外配置是因为在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异常的资料请关注脚本之家其它相关文章!

相关文章

  • java 学习笔记(入门篇)_java的安装与配置

    java 学习笔记(入门篇)_java的安装与配置

    学习Java已经很长时间了,由于基础不好遇到问题就无从下手,所以,打算写Java的随手笔记来巩固基础,加强学习,接下来讲解java的安装,配置等,感兴趣的朋友可以参考下
    2013-01-01
  • Eclipse 安装 SVN 在线插件教程

    Eclipse 安装 SVN 在线插件教程

    这篇文章主要介绍了Eclipse 安装 SVN 在线插件教程的相关资料,这里对安装步骤进行了详细介绍,需要的朋友可以参考下
    2016-11-11
  • Spring的@CrossOrigin注解使用与CrossFilter对象自定义详解

    Spring的@CrossOrigin注解使用与CrossFilter对象自定义详解

    这篇文章主要介绍了Spring的@CrossOrigin注解使用与CrossFilter对象自定义详解,跨域,指的是浏览器不能执行其他网站的脚本,它是由浏览器的同源策略造成的,是浏览器施加的安全限制,所谓同源是指,域名,协议,端口均相同,需要的朋友可以参考下
    2023-12-12
  • java 递归查询所有子节点id的方法实现

    java 递归查询所有子节点id的方法实现

    在多层次的数据结构中,经常需要查询一个节点下的所有子节点,本文主要介绍了java 递归查询所有子节点id的方法实现,具有一定的参考价值,感兴趣的可以了解一下
    2024-03-03
  • Flink实现特定统计的归约聚合reduce操作

    Flink实现特定统计的归约聚合reduce操作

    这篇文章主要介绍了Flink实现特定统计的归约聚合reduce操作,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2023-02-02
  • 小米Java程序员第二轮面试10个问题 你是否会被刷掉?

    小米Java程序员第二轮面试10个问题 你是否会被刷掉?

    小米Java程序员第二轮面试10个问题,你是否会被刷掉?掌握好基础知识,祝大家面试顺利
    2017-11-11
  • Java多线程并发synchronized 关键字

    Java多线程并发synchronized 关键字

    这篇文章主要介绍了Java多线程并发synchronized 关键字,Java 在虚拟机层面提供了 synchronized 关键字供开发者快速实现互斥同步的重量级锁来保障线程安全。
    2022-06-06
  • 详解SpringBoot构建的Web项目如何在服务端校验表单输入

    详解SpringBoot构建的Web项目如何在服务端校验表单输入

    这篇文章主要介绍了详解SpringBoot构建的Web项目如何在服务端校验表单输入,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-10-10
  • Java使用J4L识别验证码的操作方法

    Java使用J4L识别验证码的操作方法

    这篇文章主要介绍了Java使用J4L识别验证码的操作方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-02-02
  • Spring依赖注入与第三方Bean管理基础详解

    Spring依赖注入与第三方Bean管理基础详解

    依赖注入(Dependency Injection)和控制反转(Inversion of Control)是同一个概念。具体含义是:当某个角色(可能是一个Java实例,调用者)需要另一个角色(另一个Java实例,被调用者)的协助时,在 传统的程序设计过程中,通常由调用者来创建被调用者的实例
    2022-12-12

最新评论