关于java:这段代码让程序执行效率提升200倍值得一看

6次阅读

共计 4524 个字符,预计需要花费 12 分钟才能阅读完成。

点赞的靓仔,你最帅哦!

源码已收录 github 查看源码

前言

前几天业务零碎部门将咱们数据平台给投诉了,因为在工作工夫内,业务零碎查问不到想要的数据,这种问题可大可小,但毕竟影响到了业务的失常运行,所有的技术都是为业务服务的,所以不管技术难度大小,必须要进行整改,同时作为互联网的‘工匠精力’,咱们不光要让性能失常运行,还要让性能以最优的状态运行。

零碎介绍

整个零碎能够从性能上分为 3 块:

  1. 业务零碎:在上游有很多的业务零碎,业务零碎的运行产生很多的数据,这些数据扩散在很多的数据库中,大部分是 MySQL 数据库
  2. 数据智能平台:数据智能平台属于中台零碎,次要为业务零碎提供弱小的数据撑持服务,上层连接数仓。
  3. 数据仓库:数据仓库对立集中的治理所有的数据,数仓会将业务零碎产生的数据按天进行加工、抽取、转换到数据仓库存储。

当一天完结后,各个业务零碎产生了大量的数据,这些数据由定时工作进行加工、抽取到数据仓库存储,当中午你还在睡觉的时候,这些定时工作就在默默的运行着。

而每天加工的数据通常要求在下班工作工夫之前加工实现,而后通过数据智能平台的查问零碎供业务零碎查问调用,这一次数据没有查问到是因为在第二天早上 10 点,数据还没有加工实现。上面就是找问题优化了,因为失常来讲,即便定时工作链再长,也不会慢到第二天 10 点钟数据还没有进去。上面就是找问题,而后进行优化了。

工作优化

通过工作日志发现有一个上游零碎的数据抽取执行工夫有 3 个小时,而数据量仅 100 万。当然,光凭这样还无奈确定这个工作是否是能够被优化的。

查看工作代码,逻辑还比较简单:有一张原始数据表,记录商品信息以及定义的分类(这一点是虚构的,理论状况要简单一些,我这里精简而后转换了一下,便于了解),而数仓的指标表是将分类和商品别离存储在不同的表中,大抵构造如下。

那为什么须要进行这样的转换呢?这是因为整个大的零碎,一般来说只能定义一些根本的标准,而具体的细节标准则无奈束缚,比方 A 零碎的身份证字段名称为 card_no, 而 B 零碎的身份证字段名称为 crdt_no(这种状况大家应该常常遇到);再比方解决实体关系的时候,解决形式也是不同的,1 对 1 的关系,能够建两张表关联,也能够一张表都存储,这就造成了多个零碎的不统一性,而这种状况是不可避免的,因为从业务零碎来说,都保障了零碎的失常运行。

而数仓对多个原始数据处理的时候就须要思考到兼容的问题,所以就会呈现如上图的转换过程。

而这个工作执行 3 个小时的起因在于原始表中的一条记录,会转换到数仓表中的三张表中,而且这三张表是通过 id 进行关联,整个代码流程如下。

然而问题来了,100 万的数据,跑了 3 个小时,而后我开始尝试去优化程序的执行流程,大略从一下几点动手

  1. 将分类缓存,分类在零碎中曾经固定,不会发生变化,缓存能够缩小查询数据库的次数
  2. 每次从原表中读取的数据更多,从原来的 500/ 次 -> 2000/ 次

通过优化,效率有一些晋升,但并不是很显著(有同学可能要问了,这些都是很根本的,为什么最开始做?咳咳。。。这个嘛,历史起因吧,在最开始数据可能不多,不管以什么形式执行,都差异不大,比方执行 10 分钟和执行 20 分钟,看似 2 倍的执行效率,然而因为没有影响到业务零碎,且始终失常运行,也就没有看出问题)。

这里数据是须要关联的,所以咱们是须要插入数据并拿到这条记录的自增长 id,而后插入到关联表,而表构造根本不可能去动的(表构造动了那真是牵一发而动全身了,第二天准得被叫去喝茶)。

那么咱们先来剖析一下这里为什么执行这么慢呢。

  1. 原表 100 万的数据,每次查问出 2000 条,所以查问的总次数就是 1000000/2000 = 500 次,这必定耗费不了多少工夫。这里根本没有优化的空间,就算一次全副查问进去,也仅仅节俭 499 次的查问工夫(也不可能一次查问这么多数据)
  2. 查问的 2000 条数据,数据转换,而后顺次插入到信息表以及关联表中,这里是一条一条解析执行的,总计插入数据库 4000 次,毫无疑问,这里是最耗时的。数据转换是必须的,而且是在内存中操作,所以耗时不是特地多;那么剩下的就是总计 100 万 * 2 的数据库插入次数,是否进行优化呢?

首先想到的就是批量插入,批量插入能够无效的升高数据库拜访次数。然而这里不能进行批量插入是因为须要取到自增长 id,感觉陷入了窘境。

当天早晨昨晚静止之后,抛开懊恼,感觉浑身舒坦。

忽然, 脑袋灵光一闪,数据库的自增长 id 是由数据库管制的数值,而自增长的步长咱们是晓得的,比方自增长步长为 1,以后自增长 id 为 1 的话,那么能够必定,下一条记录的自增长 id 就为 2,以此类推。

那是否能够插入一条记录,取到自增长 id,而后就能够计算出之后所有数据的自增长 id,而不再须要每条记录都去取自增长 id 了。

然而这样也有一个问题,就是在数据转换导入的过程中,不能有其余的程序向表中插入数据,不然会导致程序计算的自增长 id 匹配不上。而这个问题基本不存在,因为数仓的数据都是由原始表计算插入的,在同一时间是没有其余的工作写这张表,那么咱们就能够放心大胆的干了。

我将这一部分逻辑形象进去做成了一个 demo,并填充了 100 万的数据,优化前的外围代码如下:

private void exportSource(){
    List<Source> sources;
    // 刷新日期,这里属性作为日期,其实应该以局部变量当作参数传递会更好,原谅我偷个懒
    date = new Date();
    int pageNum = 1;
    do{sources = sourceService.selectList(pageNum++, pageSize);
        System.out.println(sources);
        for (Source source : sources) {
            // 数据转换
            Target transfer = transfer(source);
            // 插入数据,返回自增长 id
            targetService.insert(transfer);
            TargetCategory targetCategory = new TargetCategory();
            Category category = allCategory.get(source.getCategoryName());
            if(category != null){targetCategory.setCategoryId(category.getId());
            }
            targetCategory.setTargetId(transfer.getId());
            // 插入分类数据
            targetCategoryService.insert(targetCategory);
        }
    }while(sources.size() > 0);
}

效率就不说了,我跑了 1 个小时,差不多跑了 20 万的数据(预计总运行工夫大于 5 小时), 而后没持续跑了, 在这个根底上做了优化。

private void exportSourcev2(){
    List<Source> sources;
    // 刷新日期,这里属性作为日期,其实应该以局部变量当作参数传递会更好,原谅我偷个懒
    date = new Date();
    int pageNum = 1;
    Integer startId = 0;
    do{sources = sourceService.selectList(pageNum++, pageSize);
        List<Target> sourceList = new ArrayList();
        List<TargetCategory> targetCategoryList = new ArrayList();
        for (Source source : sources) {
            // 数据转换
            Target transfer = transfer(source);
            // 第一次,取出自增长 id,前面就间接计算
            if(startId == 0){
                // 插入数据,返回自增长 id
                targetService.insert(transfer);
                startId = transfer.getId();}else{
                startId++;
                sourceList.add(transfer);
            }
            TargetCategory targetCategory = new TargetCategory();
            Category category = allCategory.get(source.getCategoryName());
            if(category != null){targetCategory.setCategoryId(category.getId());
            }
            targetCategory.setTargetId(transfer.getId());
            targetCategoryList.add(targetCategory);
        }
        if(sourceList.size() > 0){targetService.insertBatch(sourceList);
        }
        if(targetCategoryList.size() > 0){targetCategoryService.insertBatch(targetCategoryList);
        }
    }while(sources.size() > 0);
}


从测试后果来看,执行工夫曾经大大降低,从至多 5 小时的运行工夫缩短到 12 分钟不到。

才 11 分钟,咱们怎么就满足了,不够不够!!!!

好吧,可怜的博主持续动动歪脑筋,想个办法满足各位看官。其实从测试打印的 SQL 速度就可能感觉进去,在刚刚开始的时候,SQL 是刷刷刷的打印,到了前面,SQL 是刷。。。刷。。。刷的打印,感觉像是快没油了的汽车,从这个中央动手,看是否优化。

public List<Source> selectList(Integer pageNum, Integer pageSize) {
    // 分页查问
    PageHelper.startPage(pageNum,pageSize);
    List<Source> sources = sourceMapper.selectList();
    return sources;
}
// 打印的某条查问 SQL
==>  Preparing: SELECT * FROM source LIMIT ?, ?
==> Parameters: 998000(Long), 2000(Integer)

咱们把这条 SQL 在 Navcat 执行下看看工夫呢

查问 1 条记录竟然用时 1 秒,再来看另外一个查问。

用时 70ms,这个挺失常的。因为 limit 查问会查问出 offset 的所有数据而后将 offest 之前的数据抛弃,就是进行了全表检索,所以造成效率低。能够通过 where id 构建条件来优化查问效率。

    @Select("SELECT  t.*" +
            "FROM    (" +
            "SELECT  id" +
            "FROM    source" +
            "ORDER BY" +
            "id" +
            "LIMIT #{offset}, #{size}" +
            ") q" +
            "JOIN    source t" +
            "ON      t.id = q.id")
    List<Source> selectList(PageParam page);


这样一来,查问效率也失去了优化。

从实测后果,整体效率晋升一倍还多,由 12 分钟晋升至 5 分钟。

总结

本文的晋升程序执行效率是通过批量插入以及优化分页查问效率来实现的,欢送借鉴、欢送斧正。

正文完
 0