ITPub博客

首页 > 数据库 > 数据库开发技术 > 真实案例丨OOM内存溢出解决方案连载之一,【流式查询】不容错过!

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

原创 数据库开发技术 作者:Finally_m 时间:2022-09-05 08:03:00 0 删除 编辑
点击上方蓝字关注我们

一. 问题概述

耀哥的一个学生入职了杭州中通全球创研中心,最近他给耀哥分享一个他们公司解决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映射文件中,编写流式查询的逻辑。


<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(ResultHandler<TContent> handler);

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

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

public class TContentResultHandler implements ResultHandler<TContent> {

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

    private final static int BATCH_SIZE = 1000;
    /**
     *  计数器
     */

    private int size=0;
    /**
     * 存储每批数据的临时容器
     */

    private List<TContent> tContents = new ArrayList<>();

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

    @Override
    public void handleResult(ResultContext<? extends TContent> 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问题,耀哥给大家列举了非常多的解决方案,但本篇文章介绍的流式查询只是其中的方案之一。

至于其他的解决方案,耀哥将在后续的文章中为大家一一揭晓,敬请各位继续关注本公众号,如有问题,可以在评论区给我们留言哦。

本月福利,关注公众号回复【福利】抢书咯!

今日份干货,学会的同学回复“666”!

精选第一名,可以领取办公鼠标垫一个!

点击【阅读原文】,零基础免费学Java!

原文作者:Java架构栈;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2021-04-13

  • 博文量
    573
  • 访问量
    243919