写了一个分页 SQL,因为粗心出了 Bug 造成了 OOM!

数据库 MySQL
本文介绍了一个生产事故,并对事故和改进方法进行了分析,希望对你理解数据库分页有所帮助。

大家好,我是君哥。

最近上完线后,凌晨收到一个生产告警,一个 OOM 异常导致了服务重启。今天来分享一下这个事故。

1.事故现场

事故的代码逻辑并不复杂,从一个大概有 8 万数据的表里面查出数据,汇总后对数据做处理。因为数据量有 8 万,这里做了分页查询,每页查询 1000 条。这里贴一下代码:这里我们假定这个表名叫 sql_bug。

private void testSQLBug(){
 List<SQLBugData> sqlBugDatas = new ArrayList<>();
 int pageSize = 1000;
 int pageNumber = 0;
 while (true){
  List<SQLBugData> data = sqlBugDataMapper.queryData(pageSize, pageNumber);
  if (CollectionUtils.isEmpty(data)){
   break;
  }
  //过滤掉不符合条件的数据。
  filterData(data);
  sqlBugDatas.addAll(data);
  if (data.size() <= pageSize){
   break;
  }
  pageNumber++;
 }
}

下面我们看一下 sql,这里用 Oracle 数据库,orm 框架用的 mybatis,sql 在 mapper.xml 文件中:

select * from sql_bug order by id
offset #{pageNumber} rows fetch first #{pageSize} rows only

仔细看上面的 sql,我们可以看到 Offset 这个参数传入了 pageNumber。那后果是什么呢?查询语句每次偏移量加 1,表里有 8 万条数据,相当于得查询 79000 次循环才能结束。sqlBugDatas 数据量一直累加,最终触发 OOM。

2.原因分析

开发同事为什么会出这个 bug 呢?原因是错误地把 offset 这个变量理解成是页码的偏移量。正确的写法:

select * from sql_bug order by id
offset #{pageNumber}*#{pageSize} rows fetch first #{pageSize} rows only

为什么测试没有测出来呢?测试环境数据量比较小,并没有出现 OOM 的情况。

代码 review 为什么没有发现?由于交付的代码量很大,做代码 review 的同事主要关注点在业务逻辑的正确性上面,并没有精力能看到这么细节的问题。

3.分页写法

使用 offset 进行分页的写法很常见,比如使用 MySQL 的 limit 语法,sql 如下:

select * from sql_bug order by id
limit #{pageNumber}*#{pageSize},#{pageSize}

但是使用 Offset 进行分页的写法并不推荐,因为有深度分页的性能问题,后面的页耗时会越来越多。下图是阿里开发手册关于分页场景的一个规范。

遵循这个规范,我们每次查询的时候,不妨传入一个主键 id。先改写一下代码:

private void testSQLBug(){
 List<SQLBugData> sqlBugDatas = new ArrayList<>();
 String id = null;
 int pageSize = 0;
 while (true){
  List<SQLBugData> data = sqlBugDataMapper.queryData(id, pageSize);
  if (CollectionUtils.isEmpty(data)){
   break;
  }
  id = data.get(data.size()-1).getId();
  //过滤掉不符合条件的数据。
  filterData(data);
  sqlBugDatas.addAll(data);
  if (data.size() <= pageSize){
   break;
  }
 }
}

配合上面代码,把 sql 也改写一下,这里使用 mysql 语法:

select * from sql_bug 
<if test="id != null">            
 where id <![CDATA[>]]> #{id}
</if>   
order by id
limit #{pageSize}

也可以使用 rownum 来控制,下面再改写一下(Oracle 语法):

select * from(
 select * from sql_bug 
 <if test="id != null">            
  where id <![CDATA[>]]> #{id}
 </if>   
 order by id
)
where rownum <![CDATA[<]]> #{pageSize} + 1

我个人更推荐 rownum 写法,原因有 2 个:

  • 这种语法更容易理解,如果出问题的 sql 使用这个语法,大概率是不会出这个 bug 的;
  • 国内好多公司做信创改造,国产数据库对这种语法支持更好一些。

4.总结

本文介绍了一个生产事故,并对事故和改进方法进行了分析,希望对你理解数据库分页有所帮助。

责任编辑:姜华 来源: 君哥聊技术
相关推荐

2018-05-23 21:19:44

云计算企业云支出

2012-03-10 21:22:57

Lumia

2009-03-18 10:54:45

职场诚信危机

2013-09-04 09:59:33

拟物化UI设计扁平化

2020-10-28 08:18:54

Linux 5.10函数Linux

2022-11-13 10:07:22

SpringSpringBoot

2018-10-16 09:45:36

谷歌AI游戏

2023-08-07 13:33:50

谷歌AI

2025-02-11 09:17:57

2020-11-02 08:19:18

RPC框架Java

2020-05-07 07:42:00

Bugrm -fr *

2021-10-08 07:50:57

软件设计程序

2021-08-12 00:03:37

JSStrview视图

2024-02-04 16:14:38

线程开发

2021-05-10 09:52:35

技术隐私浏览器

2009-09-02 18:36:46

LinuxLinux操作系统Linux开发

2022-03-24 07:57:58

Python水果忍者游戏

2022-03-23 18:00:34

循环CPU线程

2024-04-22 00:00:01

Redis集群

2009-09-14 17:08:02

WebFormView
点赞
收藏

51CTO技术栈公众号