• 真实案例丨OOM内存溢出解决方案连载之一,【流式查询】不容错过!


    一. 问题概述

    耀哥的一个学生入职了杭州中通全球创研中心,最近他给耀哥分享一个他们公司解决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. <select id="selectFetchSize" fetchSize="-2147483648" resultSetType="FORWARD_ONLY" resultType="com.qf.shop.cms.entity.TContent">
    2.   select * from t_content
    3. </select>

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

    1. // 参数 ResultHandler 是一个回调接口,也就是从游标中获得一条数据就会回调接口中的方法
    2. void selectFetchSize(ResultHandler<TContent> handler);

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

    1. /**
    2.  *  通过流式查询每获得一条数据的回调类
    3.  */
    4. public class TContentResultHandler implements ResultHandler<TContent> {
    5.     /**
    6.      *  这里每集满1000条数据 往硬盘的excel文件中追加一次数据
    7.      */
    8.     private final static int BATCH_SIZE = 1000;
    9.     /**
    10.      *  计数器
    11.      */
    12.     private int size=0;
    13.     /**
    14.      * 存储每批数据的临时容器
    15.      */
    16.     private List<TContent> tContents = new ArrayList<>();
    17.     /**
    18.      * 每从流式查询中获得一行结果,就会调用一次这个方法
    19.      * @param resultContext
    20.      */
    21.     @Override
    22.     public void handleResult(ResultContext<? extends TContent> resultContext){
    23.         // 这里获取流式查询每次返回的单条结果
    24.         TContent resultObject = resultContext.getResultObject();
    25.         // 你可以看自己的项目需要分批进行处理或者单个处理,这里以分批处理为例
    26.         tContents.add(resultObject);
    27.         size++;
    28.         if (size == BATCH_SIZE) {
    29.             // 如果集满1000条就往文件中写一次
    30.             handle();
    31.         }
    32.     }
    33.     /**
    34.      *  集满1000条 执行一次的逻辑
    35.      */
    36.     private void handle() {
    37.         try {
    38.             // 在这里可以对你获取到的批量结果数据进行需要的业务处理
    39.             // 这里的业务是 往文件中写一次
    40.         } finally {
    41.             // 处理完每批数据后后将临时清空
    42.             size = 0;
    43.             tContents.clear();
    44.         }
    45.     }
    46.     /**
    47.      * 这个方法给外面调用,用来完成最后一批数据处理
    48.      */
    49.     public void end(){
    50.         handle();// 处理最后一批不到BATCH_SIZE的数据
    51.     }
    52. }

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

    1. @Autowired
    2. private TContentMapper contentMapper;
    3. public void streamQuery(){
    4.     // 生成流式查询的回调对象
    5.     TContentResultHandler tContentResultHandler = new TContentResultHandler();
    6.     // 调用流式查询
    7.     contentMapper.selectFetchSize(tContentResultHandler);
    8.     // 执行完最后一批数据的逻辑
    9.     tContentResultHandler.end();
    10. }

    四. 后话

    耀哥前面已经说到,为了解决本次产生的OOM问题,耀哥给大家列举了非常多的解决方案,但本篇文章介绍的流式查询只是其中的方案之一。

    至于其他的解决方案,耀哥将在后续的文章中为大家一一揭晓,如有问题,可以在评论区给我们留言哦。

  • 相关阅读:
    Python DCM转NRRD及NRRD转NII
    STM32矩阵按键
    你是怎么看待程序员不写注释这一事件的呢?
    微服务实战系列之玩转Docker(一)
    鸿蒙开发HarmonyOS4.0入门与实践
    HEC-RAS 1D/2D水动力与水环境模拟从小白到精通
    神经网络算法和人工智能,人工智能神经网络技术
    Pyecharts数据可视化(一)
    servlet中doGet方法无法读取body中的数据
    【DC-DC】AP9180 内置 MOS 管升压型恒流驱动芯片
  • 原文地址:https://blog.csdn.net/finally_vince/article/details/126647091