这段代码让程序执行效率提高200倍,值得一看!

点赞的靓仔,你最帅哦!

源码已收录github 查看源码java

前言

前几天业务系统部门将咱们数据平台给投诉了,由于在工做时间内,业务系统查询不到想要的数据,这种问题可大可小,但毕竟影响到了业务的正常运行,全部的技术都是为业务服务的,因此不论技术难度大小,必需要进行整改,同时做为互联网的‘工匠精神’,咱们不光要让功能正常运行,还要让功能以最优的状态运行。git

系统介绍

整个系统能够从功能上分为3块:github

  1. 业务系统:在上游有不少的业务系统,业务系统的运行产生不少的数据,这些数据分散在不少的数据库中,大部分是MySQL数据库
  2. 数据智能平台:数据智能平台属于中台系统,主要为业务系统提供强大的数据支撑服务,下层链接数仓。
  3. 数据仓库: 数据仓库统一集中的管理全部的数据,数仓会将业务系统产生的数据按天进行加工、抽取、转换到数据仓库存储。

当一天结束后,各个业务系统产生了大量的数据,这些数据由定时任务进行加工、抽取到数据仓库存储,当半夜你还在睡觉的时候,这些定时任务就在默默的运行着。
sql

而天天加工的数据一般要求在上班工做时间以前加工完成,而后经过数据智能平台的查询系统供业务系统查询调用,这一次数据没有查询到是由于在次日早上10点,数据尚未加工完成。下面就是找问题优化了,由于正常来说,即便定时任务链再长,也不会慢到次日10点钟数据尚未出来。下面就是找问题,而后进行优化了。数据库

任务优化

经过任务日志发现有一个上游系统的数据抽取执行时间有3个小时,而数据量仅100万。固然,光凭这样还没法肯定这个任务是不是能够被优化的。缓存

查看任务代码,逻辑还比较简单:有一张原始数据表,记录商品信息以及定义的分类(这一点是虚构的,实际状况要复杂一些,我这里精简而后转换了一下,便于理解),而数仓的目标表是将分类和商品分别存储在不一样的表中,大体结构以下。app

那为何须要进行这样的转换呢? 这是由于整个大的系统,通常来讲只能定义一些基本的规范,而具体的细节规范则没法约束,好比A系统的身份证字段名称为card_no,而B系统的身份证字段名称为crdt_no(这种状况你们应该常常遇到);再好比处理实体关系的时候,处理方式也是不一样的,1对1的关系,能够建两张表关联,也能够一张表都存储,这就形成了多个系统的不统一性,而这种状况是不可避免的,由于从业务系统来讲,都保证了系统的正常运行。测试

而数仓对多个原始数据处理的时候就须要考虑到兼容的问题,因此就会出现如上图的转换过程。优化

而这个任务执行3个小时的缘由在于原始表中的一条记录,会转换到数仓表中的三张表中,并且这三张表是经过id进行关联,整个代码流程以下。spa

然而问题来了,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分钟。

总结

本文的提高程序执行效率是经过批量插入以及优化分页查询效率来实现的,欢迎借鉴、欢迎指正。