最近做了一个同步数据的需求,需要将代理人学习课程的记录同步至ck,便于数据分析及对应报表查询导出等功能。线上大约十几万代理人,课程五十门左右,累计大约五百万的数据。于是写了个定时任务计算学习结果后插入到clickhouse中。
上代码:
代码很简单,刚做的时候考虑到数据量较大 没有一次性处理数据,而是分课程去循环处理代理人学习课程的数据,每次根据课程id查出的代理人课程数据也就十几w条,然后再切割数组,每次批量插入一千条,插入完成后,再将此课程数据置空,移除引用, 供GC回收。
接下来 开开心心的上线啦
看代码是不是怎么也看不出来会发生oom?
于是,某一天的早晨,告警响起,内存溢出了。
排查过程
之前部署已经设置好了在内存溢出时生成内存快照及gc日志
查看日志发现是上图代码396行发生内存溢出,再一看代码,卧槽, 这地方咋可能内存溢出。
接着将服务器的内存快照下载到本地
使用mat打开内存快照看了一波,找到大对象后
卧槽, 哪来的三百多万的课程对象 懵逼中。。。
再仔细一看 ,发现了不对劲,怎么是在这个cache对象里面,我没缓存他们呀。。。。
那这个cache对象是干啥的? 查了下, 原来是mybatis的一级缓存
那我之前置空课程对象的代码就没啥用了,对象依然被缓存引用,导致此方法内一直产生的课程对象不能被回收,直到内存被撑爆。。。
解决方案
可是我都没有用事务,整个方法怎么会公用一个session开启缓存呢?
再仔细看了下代码,类上被加了个事务注解,导致启用事务,但是没几把用(clickhouse不支持事务)。。。
但因为类里面还有其它方法 不能将类上的注解移除
那就只有三种解决办法了
- 将此方法移到另外一个类 不开启事务
- 在update方法上添加 @Options(flushCache= Options.FlushCachePolicy.TRUE) 清除缓存,也可以手动获取sqlSeession对象调用清除缓存的方法
- 每执行一次update方法手动提交一次事务 事务提交后session关闭 一级缓存会自动清除
这里我选择的是第二种方式,在查询课程数据的方法上也加了注解。查询和update都清缓存 因为每次都是查询的不同数据 此处的一级缓存没啥用
至于Options注解和在一个事务内update\select为什么会使用一级缓存(全局开启一级缓存的情况下) 感兴趣的小伙伴可以自行了解