POI导出OOM
上万数据的导出,使用POI的XSSFWorkBook出现OOM,修改为SXSSWorkBook,定量刷盘
为什么不用easyExcel,因为easyExcel对于Excel样式的支持不够,客户要求颜色、格式等与前端界面相同
查询OOM
当数据来到百万的时候,查询的方法出现了OOM
场景模拟优化
以单表查询500w数据量进行模拟,其中表包含10列
基于本地数据库,可以完全忽略网速问题
默认情况
使用List接收500w数据
- Xmx6g OOM
- Xmx8g,查询成功,监控使用内存大约需要7g,耗时62s
使用游标Cursor
不修改任何参数只使用Cursor类出现
出现下面报错
java.lang.IllegalStateException: A Cursor is already closed.
查询相关信息后得知,使用游标Cursor的方式需要保持与数据库的连接
最简单的方式是直接使用 @Transactional(readOnly = true)
不修改其他参数,Cursor配合@Transactional(readOnly = true)
- Xmx6g,查询成功,最大使用内存4g,耗时64s。内存上升到3.9g左右平滑回收到3g,通过监控观察到老年代未发生回收,主要发生回收在年轻代
- Xmx3g,查询成功,最大使用内存接近3g,耗时62s。老年代上升到2.8g以后频繁回收新生代。
- Xmx2g,OOM
使用Cursor后内存占用下降明显,但仍然需要不少内存
使用Cursor + useCursorFetch=true + fetchSize
经过查阅网上博客发现,在使用Cursor需要搭配后面两个参数来才能达到游标查询的效果。
游标查询,即类似分页查询,jdbc驱动对mysql交互的一种分页方式
- Xmx2g,fetch=10000,查询时间太久了遂放弃,内存占用不大且回收主要在年轻代,年轻代达到250M左右进行回收持续往复。
- Xmx2g,fetch=100000,耗时70s,G1收集器的情况下发生mixed gc,内存最大占用约650M
- 那么自然试一下Xmx700m,fetch=100000,耗时78s,发生mixed gc,内存最大占用450M
- Xmx200m同上,young gc增多耗时5s吞吐量下降
- Xmx100m,出现full gc吞吐量下降严重,耗时达到90s
通过这种方式,可以有效的控制JDBC在每次从数据库获取的数据多少,但会增加与数据库的网络交互次数,通过控制fetchSize参数可以达到一个内存占用与耗时之间的平衡控制。
在使用这种方式的时候有一个很有趣的现象,在一开始执行请求的时候,内存的占用并不会发生变化,但mysql的io占用会急剧升高,直到mysql io结束占用以后,内存才会发生明显波动。
但是,这只是游标查询,如果fetchSize设置为1的话将会一直在与数据库交互,500w次的交互耗时会更长,有没有真正的流式的方式能够保证吞吐量的同时又保证内存消耗呢?
Cursor + fetchSize = Integer.MIN_VALUE
在mysql-jdbc中有下面这么一段源码
/**
* 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));
}
前面两个值在JDBC的源码中有直接设置默认值没有去过多了解 ,最后一个值则是fetchSize
- Xmx100m,fetchSize = Integer.MIN_VALUE,耗时达到了惊人的40s,且内存消耗最高80m,只平稳的发生了young gc
- Xmx50m,fetchSize = Integer.MIN_VALUE,耗时50s,程序发生了频繁的young gc,占用了极大cpu严重影响吞吐量
也就是说,在大数据量下的查询,正确的流式查询应该是Cursor + fetchSize = Integer.MIN_VALUE
在Mapper中的写法是
@Options(fetchSize = Integer.MIN_VALUE)
@Select("select * from table")
Cursor<Table> listByCursor();
在xml中的写法是,-2147483648则是Integer.MIN_VALUE的值
<select id="listByCursor" resultMap="BaseResultMap" fetchSize="-2147483648">
select * from table
</select>
可以具体查看mybatis plus中呢,可以具体查看mybatis plus的官方文档
MyBatis-Plus 从 3.5.4 版本开始支持流式查询,
这是 MyBatis 的原生功能,通过 ResultHandler 接口实现结果集的流式查询。
这种查询方式适用于数据跑批或处理大数据的业务场景。
但需要注意一个地方,官方文档中案例实际效果相当于上面单独使用了Cursor
我们既可以通过配置mybatis-plus.configuration.default-fetch-size为具体值来控制达到游标查询的效果,
也可以设置为-2147483648达到真正的流式查询