当指定查询数据过大时,我们一般使用分页查询的方式,一页一页的将数据放到内存处理。但有些情况不需要分页的方式查询数据,如果一下子将数据全部加载出来到内存中,很可能会发生OOM。这时我们可以使用流式查询解决问题。
为了更好的复现问题,将jvm参数,最大堆设置成212M。使用mysql数据库,表大小为730MB。
非流式查询表里所有数据代码
List<InfoPO> infoPOs = infoMapper.selectList(new EntityWrapper<>());
通过查看idea控制台,很快出现了内存溢出。
通过jconsole工具,查看内存使用情况
在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控制台,程序运行正常
在通过jconsole工具,查看内存使用情况
发现内存消耗处于平稳状态。
查看源码可知,我们使用流式查询时,必须要满足以下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)); }
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。
ResultSet数据返回的结果,对象有3种实现方式
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类。