千锋教育-做有情怀、有良心、有品质的职业教育机构

手机站
千锋教育

千锋学习站 | 随时随地免费学

千锋教育

扫一扫进入千锋手机站

领取全套视频
千锋教育

关注千锋学习站小程序
随时随地免费学习课程

当前位置:首页  >  技术干货  > 记一次OOM问题的解决连载一 【流式查询】

记一次OOM问题的解决连载一 【流式查询】

来源:千锋教育
发布人:qyf
时间: 2023-03-02 20:44:00 1677761040

  一. 问题概述

  老师的一个学生入职了杭州中通全球创研中心,最近他给老师分享一个他们公司解决OOM问题的案例,老师觉得十分有趣,特意把这个案例记录下来,日后我会做成教学案例分享给学生。这个问题发生的背景如下:

  【在物流领域,针对各个下级网点而言,每月1日~9日是进行财务月结的重要时间节点。在这个关键节点上,各个网点需要使用导出功能输出寄派件、费用客户信息等多种信息进行汇总结算】。也就是说,在月初的时候,每个网点都要统计一个月的各种流水(寄件,收件等),最后再以excel表格的形式下载给客户。

  那么在这个业务中为什么很容易发生OOM异常呢?这是因为平均1个网点1个月的流水数据大约在30w行左右,根据计算得出大约500行数据就会占用1M内存,而1个站点把30万行一股脑地读到内存中,就会占用600M内存。试想一下,如果全国的网点都在月初集中下载报表的话,JVM是很容出现内存溢出的问题的!

  二. 解决方案

  那么这样一个棘手的问题,如果我们只用一个单一的解决方案是不够的,老师根据学生的描述,建议该学生主要采取以下几种解决方案。

  2.1 用硬盘空间置换内存空间

  如果我们在接到统计数据请求的时候,一次性把30w条数据从数据库读取到一个List集合中,这显然是不合理的,因为这样一个List集合就会占用600M内存。所以我们可以进行分页查询,每次查询1000条数据,然后往硬盘里写,多读取几次,一点一点的把所有的数据都读出来,再一点一点的往硬盘中写。这样在这个过程中,占用的内存就会少很多,主要变成了对硬盘空间的占用。而我们操作excel的技术,可以选择阿里巴巴的easyexcel。

  2.2 使用Mybatis的流式查询

  我们可以使用Mybatis的【流式查询】查询技术,在查询成功后返回的是一个迭代器而不是一个集合,应用每次都从迭代器中获取一条查询结果,能够降低内存的使用。试想一下,如果我们不使用流式查询,而想要一次性从数据库中读取30万条数据,内存是根本不够用的!这时我们只能选择分页查询,而分页查询的性能又取决于表设计以及索引的设计,大量数据分页查询的性能是很低的。老师对比了使用流式查询和分页查询的两种方案,得到的结论是取30万条数据时,流式查询的速度大约是分页查询的4~5倍左右。

  2.3 使用redission信号量限流

  生成一个月的流水报表是一个非常耗时的操作,用户也不可能马上就要结果,所以我这个学生的公司对同时生成报表的请求数量做了限制,同时只能处理10个报表的生成。在这期间如果再有生成报表的请求,我们将会让这些请求排队,等到前面的报表生成完毕后,再处理后面的请求。报表生成成功后,再通知客户主动去下载,老师建议这里使用redisson分布式锁的信号量来限制同时创建报表的线程数量。

  2.4 MQ解耦+微服务拆分

  本次业务中,读数据库,编写excel文件,上传到文件服务器这三个操作都非常耗时,学生的公司使用了MQ解耦,并把这次请求拆分成3个微服务,这样读、写、上传就不会相互影响了。

  三. 流式查询

  在这篇文章中,老师只给大家分享一下Mybatis流式查询的实现方法,其他的解决方案以后会在其他的文章中给大家呈现。

  3.1 概念

  流式查询就是查询成功后返回的是一个迭代器而不是一个集合,应用每次都从迭代器中获取一条查询结果,这样能够降低内存的使用。

  3.2 Mybatis实现流式查询

  接下来就是实现流失查询的具体过程。

  在mapper映射文件中,编写流式查询的逻辑。

<!--
1: fetchSize: 官方文档建议设置成Integer.MIN_VALUE
2: resultSetType="FORWARD_ONLY" 返回一个只向前的游标
3:注意我把表一次性查出,并没有使用分页逻辑,依靠流式查询一行一行得到结果
-->
<select id="selectFetchSize" fetchSize="-2147483648" resultSetType="FORWARD_ONLY" resultType="com.qf.shop.cms.entity.TContent">
select * from t_content
</select>

  在mapper接口文件中添加selectFetchSize方法。

  // 参数 ResultHandler 是一个回调接口,也就是从游标中获得一条数据就会回调接口中的方法

  void selectFetchSize(ResultHandlerhandler);

  自己编写一个类实现ResultHandler接口,在该接口中定义从游标获得一条数据后的回调逻辑。

  /**

  * 通过流式查询每获得一条数据的回调类

  */

  public class TContentResultHandler implements ResultHandler{

  /**

  * 这里每集满1000条数据 往硬盘的excel文件中追加一次数据

  */

  private final static int BATCH_SIZE = 1000;

  /**

  * 计数器

  */

  private int size=0;

  /**

  * 存储每批数据的临时容器

  */

  private ListtContents = new ArrayList<>();

  /**

  * 每从流式查询中获得一行结果,就会调用一次这个方法

  * @param resultContext

  */

  @Override

  public void handleResult(ResultContext resultContext){

  // 这里获取流式查询每次返回的单条结果

  TContent resultObject = resultContext.getResultObject();

  // 你可以看自己的项目需要分批进行处理或者单个处理,这里以分批处理为例

  tContents.add(resultObject);

  size++;

  if (size == BATCH_SIZE) {

  // 如果集满1000条就往文件中写一次

  handle();

  }

  }

  /**

  * 集满1000条 执行一次的逻辑

  */

  private void handle() {

  try {

  // 在这里可以对你获取到的批量结果数据进行需要的业务处理

  // 这里的业务是 往文件中写一次

  } finally {

  // 处理完每批数据后后将临时清空

  size = 0;

  tContents.clear();

  }

  }

  /**

  * 这个方法给外面调用,用来完成最后一批数据处理

  */

  public void end(){

  handle();// 处理最后一批不到BATCH_SIZE的数据

  }

  }

  在业务逻辑(service)层调用流式查询方法。

  @Autowired

  private TContentMapper contentMapper;

  public void streamQuery(){

  // 生成流式查询的回调对象

  TContentResultHandler tContentResultHandler = new TContentResultHandler();

  // 调用流式查询

  contentMapper.selectFetchSize(tContentResultHandler);

  // 执行完最后一批数据的逻辑

  tContentResultHandler.end();

  }

  四. 后话

  老师前面已经说到,为了解决本次产生的OOM问题,老师给大家列举了非常多的解决方案,但本篇文章介绍的流式查询只是其中的方案之一。至于其他的解决方案,老师将在后续的文章中为大家一一揭晓,敬请各位继续关注本公众号,如有问题,可以在评论区给我们留言哦。

tags:
声明:本站稿件版权均属千锋教育所有,未经许可不得擅自转载。
10年以上业内强师集结,手把手带你蜕变精英
请您保持通讯畅通,专属学习老师24小时内将与您1V1沟通
免费领取
今日已有369人领取成功
刘同学 138****2860 刚刚成功领取
王同学 131****2015 刚刚成功领取
张同学 133****4652 刚刚成功领取
李同学 135****8607 刚刚成功领取
杨同学 132****5667 刚刚成功领取
岳同学 134****6652 刚刚成功领取
梁同学 157****2950 刚刚成功领取
刘同学 189****1015 刚刚成功领取
张同学 155****4678 刚刚成功领取
邹同学 139****2907 刚刚成功领取
董同学 138****2867 刚刚成功领取
周同学 136****3602 刚刚成功领取
相关推荐HOT