滥用Mybatis一级缓存引发OOM问题
滥用Mybatis一级缓存引发OOM问题
- 一、背景
- 二、问题定位
- 三、解决措施
- 四、最终结果
一、背景
我们公司某业务系统通过定时任务工具生成报表数据,此工具需要频繁与数据库进行交互,且数据量非常大。开发完成本版本需求后,发布到dev环境,发现报表一直没有正确生成,而且服务不可用。查询服务器日志,代码中有分页执行的日志,但是在第5页和第6页后,再也没有正常结束的日志记录,代表执行完第6页之后,服务器就不可用了。
二、问题定位
分析服务器日志,发现有"java.lang.outOfMemoryError:Java Heap Space"错误信息。分析定时任务运行代码,通过数据库分页处理数据,初步分析可能是每次从数据库获取7.5万条数据,一次性加载到服务器内存,导致内存溢出。
1:尝试减少分页获取的数据量,在本地修改后启动,生成报表无异常。但是部署到dev环境后,任然报内存溢出错误。
2:使用jconsole工具,连接远程dev环境,查询JVM信息,堆内存峰值最大为4G。默认情况下,服务器堆内存最大值是物理内存的1/4,dev环境的docker服务器为4C8G,则服务器堆内存为2G。
3:从服务器下载dump文件,使用MAT内存泄漏分析工具查看JVM堆数据。工具检测展示:造成堆内存泄漏的对象是HashMap对象,堆内存占用达到85%。
java.util.HashMap$Node[]
org.apachhe.ibatis.executor.SimpleExecutor.doQquery
这是Mybatis框架执行SimpleExecutor.doQuery()方法导致堆内存溢出。SimpleExecutor是Mybatis默认执行器,代码中每执行一次select/update就会开启一个JDBC Statement对象,用完直接关闭。
4:继续查看MAT工具,发现使用了PerpetualCache,单后存储到HashMap中,这是一级缓存操作类,sqlSession会话级缓存,也称为本地缓存。Mybatis对一级缓存的设计非常简单,使用了HashMap来保存,并未限制HashMap的容量和大小。
5:由于业务上需要生成报表,使用了Transaction开启了事务,使用游标cursor持续查询1.5小时,session会一直保持连接。不会向之前那样如果没有开启事务,每次查询完成,自动关闭连接。
三、解决措施
Mybatis使用flushCache属性来配置select语句级别的一级缓存,值为true代表开启。本业务场景中,SQL采用了flushCache= true开启一级缓存。
但是由于此场景的特殊性,开启一级缓存并不能提升查询效率,却会引发堆内存溢出问题。因此需要在SQL中,修改为flushCache= false即关闭一级缓存。
四、最终结果
SQL中关闭一级缓存后,本地远程连接dev环境,测试内存占用最高为0.5G,不会再达到之前的4G。部署到dev环境,正常生成报表,持续观察2天,未出现OOM问题。