近期遭遇了一次生产环境的严重告警,涉及慢接口和CPU过载。经过排查,发现问题根源在于一段使用MyBatis的查询代码。当传入空列表作为查询条件时,MyBatis会忽略该条件,导致全表扫描,进而引发系统资源耗尽和频繁的Full GC
灾难回顾
前两天晚上,正在收拾包准备下班,电脑刚放进包里,我的手机就开始不停地震动,打开一看,是生产环境告警群发来的信息,一段是慢接口告警,另一段是CPU告警。在这种即将回家的时候看到告警信息,是每一个技术人员最不想发生的事情,但我依然从包里取出电脑,准备处理这一突发情况。
由于最近并没有上线新功能,我的第一反应是怀疑中间件或数据库出了问题,毕竟,经验告诉我,很多时候性能问题都是由这些基础设施引起的,但经过和运维同事的一轮快速排查,各中间件表现正常,数据库中的慢SQL也并未达到触发告警的阈值。
看来问题比我想象的要复杂,我坐在运维同事旁边,让他登录主机上执行top
命令,看看究竟怎么回事,从TOP
可以看出,CPU占用情况异常地显示在主机CPU一直保持在100%(主机16核),没有任何下降的趋势,这意味着,某个进程或者线程正在疯狂地消耗CPU资源。
我突然想起之前遇到过的类似情况,那次是因为JVM的Full GC
过于频繁导致的,有了上次的经验,我就让运维执行jstat -gcutil pid 1000
命令查看GC情况,果不其然,FGC(Full GC)
次数每几秒就增加1次,这频率高得异常,JVM显然在不停地进行垃圾回收,但为什么会这么频繁呢?
在这种情况下肯定是要先保住业务系统的运行,我的第一反应是尽快减轻服务器的压力,于是,我向领导申请决定重启部分机器,同时留下一台机器进行内存快照(dump)
的生成,以便后续分析。10分钟后,内存快照分析完成。分析结果让我颇为震惊。有个叫OrderAddress
的类占据了异常大的内存空间,包含对象数高达150多万。这明显不正常,很可能是它导致了频繁的Full GC。
为了找到问题的根源,我使用jstack命令(jstack -l pid)
查看了JVM的线程堆栈信息。通过搜索关键词OrderAddress
,我发现了很多与之相关的堆栈信息。这些信息就像是破案的关键线索,指引我逐步接近问题的真相。最终,基于这些堆栈信息,我找到了对应的代码位置。
场景回顾
在我们日常的开发工作中,经常会遇到与数据库交互的情况。假设我们有这样一张数据库表jy_order_address
,它记录了订单的收货地址信息:
create table `jy_order_address` (
`id` int(11) unsigned not null auto_increment comment 'id',
`order_id` int(11) not null comment 'jy_order.id',
...
...
...
) comment='订单收货地址';
为了满足某一业务需求,我们需要根据一批order_id
来查询对应的收货地址,在Java中,使用MyBatis
作为ORM
框架,我们可能会写出如下的代码:
public List<OrderAddress> selectByOrderIdList(List<Integer> orderIdList) {
Wrapper<OrderAddress> wrapper = new EntityWrapper<>();
wrapper.in("order_id", orderIdList);
List<OrderAddress> orderAddressList = orderAddressMapper.selectList(wrapper);
return orderAddressList;
}
这段代码逻辑很清晰,通过MyBatis的Wrapper
来组装SQL查询条件,实现在order_id
在指定列表中的筛选。
正常情况下,如果orderIdList
数据量不大,即使jy_order_address
表中有大量数据,上述查询也是非常高效的,对应的SQL如下:
select * from jy_order_address where order_id in(?,?,?,...);
然而,当传入一个空列表orderIdList = []
时,系统CPU占用率飙升,经过排查,发现竟然进行了全表扫描!
深入了解MyBatis的Wrapper
API后,我们发现了一个容易被忽视的细节,当查询条件的value为null或者空列表时,MyBatis会忽略该条件,这意味着上述查询代码在实际执行时变成了无条件的全表查询:
select * from jy_order_address;
对于一个包含150多万条数据的表来说,全表扫描无疑是一个巨大的负担,它会消耗大量的内存和CPU资源,进而导致JVM频繁进行Full GC(全局垃圾回收)。
预防措施
严格的参数校验,在使用Wrapper进行查询之前,应对每个参数进行非空校验,这确保了查询的准确性和系统的健壮性,防止因空值或无效值导致的不可预测的行为。
**审慎使用Wrapper,尽管Wrapper提供了方便的查询条件组装,但在某些情况下,它可能导致不期望的查询行为。**为了避免这种情况,推荐直接编写SQL语句,特别是在处理关键查询时。例如,使用order_id in()这样的条件时应当特别小心,因为这可能导致SQL语法错误,一个更安全的方法是添加一个始终为假的条件(如1<>1),这样即使其他条件出现问题,SQL仍然可以正确执行,只是返回0条数据。
限制查询结果,为了防止大量数据一次性加载到内存中导致系统崩溃,建议使用MyBatis拦截器为所有查询统一设置结果条数上限,例如,每次查询最多返回1000条记录,当达到这个限制时,系统可以发出警告,并提示开发者进行分页查询以获取更多数据,这样不仅能提高系统的稳定性,还能提升用户体验,使大数据量的处理更加流畅。