欢迎访问悦橙教程(wld5.com),关注java教程。悦橙教程  java问答|  每日更新
页面导航 : > > 文章正文

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

来源: javaer 分享于  点击 37614 次 点评:31

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


目录
  • Cursor是啥
  • 如何使用Cursor
  • Cursor原理
    • 解析Mapper方法返回值
    • 根据Cursor返回值调用selectCursor
    • 构建statement
    • 封装Cursor
    • 为啥能避免内存溢出
      • Cursor查询
      • 批量查询
  • 总结

    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异常的资料请关注3672js教程其它相关文章!

    您可能感兴趣的文章:
    • MyBatis游标Cursor在Oracle数据库上的测试方式
    • Mybatis中的游标查询Cursor(滚动查询)
    • MyBatis游标Cursor的正确使用和百万数据传输的内存测试
    • Mybatis千万级数据查询的解决方式,避免OOM问题
    相关栏目:

    用户点评