Mybatis流式查询避免OOM

5,750 阅读3分钟

前言

当指定查询数据过大时,我们一般使用分页查询的方式,一页一页的将数据放到内存处理。但有些情况不需要分页的方式查询数据,如果一下子将数据全部加载出来到内存中,很可能会发生OOM。这时我们可以使用流式查询解决问题。

非流式查询

为了更好的复现问题,将jvm参数,最大堆设置成212M。使用mysql数据库,表大小为730MB。

img

非流式查询表里所有数据代码

 List<InfoPO> infoPOs = infoMapper.selectList(new EntityWrapper<>());

通过查看idea控制台,很快出现了内存溢出。

img

通过jconsole工具,查看内存使用情况

img

在14.26,发现内存直接被释放了。

流式查询

流式查询表里所有数据代码

@Select("select * from t_iot")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
@ResultType(InfoPO.class)
void selectAutoList(ResultHandler<InfoPO> handler);
infoMapper.selectAutoList(resultContext -> {
    resultContext.getResultObject();
});

通过查看idea控制台,程序运行正常

img

在通过jconsole工具,查看内存使用情况

img

发现内存消耗处于平稳状态。

流式查询原理

查看源码可知,我们使用流式查询时,必须要满足以下3个条件

/**
 * We only stream result sets when they are forward-only, read-only, and the
 * fetch size has been set to Integer.MIN_VALUE
 * 
 * @return true if this result set should be streamed row at-a-time, rather
 *         than read all at once.
 */
protected boolean createStreamingResultSet() {
    return ((this.query.getResultType() == Type.FORWARD_ONLY) && (this.resultSetConcurrency == java.sql.ResultSet.CONCUR_READ_ONLY)
            && (this.query.getResultFetchSize() == Integer.MIN_VALUE));
}
  1. resultSetConcurrency=ResultSet.CONCUR_READ_ONLY 设置只读结果集

  2. resultSetType = ResultSetType.FORWARD_ONLY 设置结果集的游标只能向下滚动

  3. fetchSize = Integer.MIN_VALUE 设置fetch size为int的最小值,这里和oracle/db2有区别.

    Oracle/db2是从服务器一次取出fetch size 条记录放在客户端,客户端处理完成一个批次后再向服务器取下一个批次,直到所有数据处理完成。

    mysql在执行ResultSet.next()方法时,会通过数据库连接一条一条的返回。MySQL按照自己的节奏不断的把buffer写回网络中。flush buffer的过程是阻塞式的,也就是说如果网络中发生了拥塞,send buffer被填满,会导致buffer一直flush不出去,那MySQL的处理线程会阻塞,从而避免数据把客户端内存撑爆。

设置三个参数之后,断点进入到了流式返回结果集ResultsetRowsStreaming。

img

ResultSet数据返回的结果,对象有3种实现方式

img

ResultsetRowsStatic 静态结果集,默认的查询方式,普通查询

ResultsetRowsCursor 游标结果集,服务器端基于游标查询

ResultsetRowsStreaming 动态结果集,流式查询

查看ResultsetRowsStatic类注释

/**
 * Represents an in-memory result set
 */
public class ResultsetRowsStatic extends AbstractResultsetRows implements ResultsetRows {

表示放在内存中的结果集。

查看ResultsetRowsStreaming类注释

/**
 * Provides streaming of Resultset rows. Each next row is consumed from the
 * input stream only on {@link #next()} call. Consumed rows are not cached thus
 * we only stream result sets when they are forward-only, read-only, and the
 * fetch size has been set to Integer.MIN_VALUE (rows are read one by one).
 * 
 * @param <T>
 *            ProtocolEntity type
 */
public class ResultsetRowsStreaming<T extends ProtocolEntity> extends AbstractResultsetRows implements ResultsetRows {

提供了Resultset行的流。获取下一行都是从仅在{@link #next()}调用时输入流。因此不会缓存已使用的行。我们只在结果集只有前进、只读和时才流结果集获取大小已设置为整数。MIN_VALUE(逐个读取行)。

总结

之前使用过db2处理流式查询,设置的fetch size为100,没有问题。这次使用mysql刚开始时也设置的100,发现内存溢出了,后来在网上看到mysql流式获取数据的坑,debug进去果然没走到ResultsetRowsStreaming类,设置fetch size 参数为Integer.MIN_VALUE后,才进了ResultsetRowsStreaming类。