一次fori循环的优化

背景

某项目上上传46条excel记录总是报超时的问题,把相关数据下载到测试环境测试,秒传;自己又写了个550条的excel记录上传在开发环境验证,秒传;查看前端限制了10秒,如果10秒都没传完说明太影响用户性能了,所以决定查看后端代码优化一下。
后端代码主要分为六部分:

  1. 解析excel,得到每行结果DemoExcelDTO集合List<DemoExcelDTO>;(此部分可优化代码少)
  2. 检查上级(模块名称)是否存在(fori循环查询数据库比较)
  3. 检查此行数据在否已经存在数据库(fori循环查询数据库比较)
  4. 检查表达式(需要发送http请求校验表达式的元素是否存在)
  5. 循环赋值准备保存数据库的数据(发送http请求获取指标表达式的元素拼装成类似1,2,3的String)
  6. 插入数据库

优化前

这里第1部分的代码已经具有很强的灵活性,并且优化空间少,就不在展示了;第6部分对数据库的操作也没有优化的空间;这里主要想对for循环优化进行思路解析,现贴上优化前的时间。

本次针对与46条评分指标上传的优化(除去最后一步的数据库保存)
------- 1.优化前 第一次8000+第二次9000+--------
批量上传--总消耗时间:10838
其中模块名称重复--消耗时间:2047
其中检查数据库重复--消耗时间:2411
其中检查表达式--消耗时间:2611
其中循环设置指标依赖集合--消耗时间:2508
------- 1 --------

优化前的代码

/**
     * 检查是否在数据库中存在
     */
    private String checkNameAndCodeDuplicateInDB(List<IndexScoreExcelDTO> scoreDTOS, Long scoreModelId) {
        StringBuilder msg = new StringBuilder();
        for (int i = 0; i < scoreDTOS.size(); i++) {
            IndexScoreExcelDTO indexscoreexceldto = scoreDTOS.get(i);
            Boolean exist = this.isExist(indexscoreexceldto.getName(), indexscoreexceldto.getCode(), scoreModelId);
            if (exist) {
                msg.append(String.format("行数为【%s】,错误原因:名称【%s】或编码【%s】已存在%s", i + 1, indexscoreexceldto.getName(),
                        indexscoreexceldto.getCode(), System.lineSeparator()));
            }
        }
        return msg.toString();
    }

        /**
     * 指标表达式检查
     */
    private String checkExpression(List<IndexScoreExcelDTO> scoreDTOS) {
        StringBuilder msg = new StringBuilder();
        String separator = System.lineSeparator();
        for (int i = 0; i < scoreDTOS.size(); i++) {
            IndexScoreExcelDTO indexScoreExcelDTO = scoreDTOS.get(i);
            String expression = indexScoreExcelDTO.getExpression();
            String errorMsg = ExpressionUtils
                    .getErrorMessageIfExpressionIsError(expression, expressionService, indexRemoteManager);
            if (StringUtils.isNotBlank(errorMsg)) {
                msg.append(
                        String.format("第%s行,指标表达式【%s】%s%s", i + 1, expression, errorMsg, separator));
            }
        }
        return msg.toString();
    }

思路解析

1.fori循环连接数据库比较校验 修改为 一次连接,内存比较校验(java8流)

可观察代码fori循环主要是因为需要知道是第几行的excel行有问题,那么我们为什么不能在最开始的时候设置行数,fori循环转换成foreach,或者java8的流呢?所以对excel转化进行了更改,新设置了一个属性excelLineNumber,这样就不必每次太过于关注循环的索引,因为每次连接数据都是比较耗时的,即使有连接池,我们也尽量避免多次连接数据库,消耗性能,除非是实时性非常高的功能;我们项目的实时性并没有那么高,所以可以修改为一次连接数据库,在内存中进行校验,能实现同样的需求,同时缩短了时间,可观察下面的代码,这里采用的java8的forEach,如果不像我们需要按照顺序展示多少行出问题了,还可以采用java8的并行流(parallelStream)进行优化;

/**
     * 检查是否在数据库中存在
     */
    private String checkNameAndCodeDuplicateInDB(List<IndexScoreExcelDTO> scoreDTOS, Long scoreModelId) {
        if(CollectionUtils.isEmpty(scoreDTOS)){
            return null;
        }
        StringBuilder msg = new StringBuilder();
        List<IndexScoreDO> indexScoreDOList = indexScoreDAO.findByScoreModelId(scoreModelId);
        List<String> indexScoreNameList = indexScoreDOList.stream().map(IndexScoreDO::getName).collect(Collectors.toList());
        List<String> indexScoreCodeList = indexScoreDOList.stream().map(IndexScoreDO::getCode).collect(Collectors.toList());
        scoreDTOS.forEach(scoreDTO -> {
            if (indexScoreNameList.contains(scoreDTO.getName().trim()) || indexScoreCodeList.contains(scoreDTO.getCode().trim())) {
                msg.append(String.format("行数为【%s】,错误原因:名称【%s】或编码【%s】已存在%s", scoreDTO.getExcelLineNumber(), scoreDTO.getName(),
                        scoreDTO.getCode(), System.lineSeparator()));
            }
        });
        return msg.toString();
    }

2.利用countDownLatch实现并行处理任务

CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。CountDownLatch使用场景之一实现最大的并行性,有时我们想同时启动多个线程,实现最大程度的并行处理任务。
本文中的ExpressionUtils.getErrorMessageIfExpressionIsError里循环调用Http请求去另一个工程获取数据,检验是否存在表达式里的元素,如果有46行数据,一行里的表达式存在了3个元素,那么将发送138次Http请求,最大的问题就是这里请求了,后面第5步又同样再去获取元素,又发送了138次http请求,所以这里又很大的优化空间;发送这么多次请求可不可以减少时间呢?利用线程池+countDownLatch并行处理任务;

 public static final ExecutorService POOL = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 5, (new ThreadFactoryBuilder()).setNameFormat("pool-toolkit-%d").build());
  
   /**
     * 指标表达式检查
     */
       private String checkExpression(List<IndexScoreExcelDTO> scoreDTOS) {
        StringBuilder msg = new StringBuilder();
        String separator = System.lineSeparator();
        long start = System.currentTimeMillis();
        CountDownLatch countDownLatch = new CountDownLatch(scoreDTOS.size());

        UserIdentifier userIdentifier = CurrentUserHelper.getUserIdentifier();
        for (int i = 0; i < scoreDTOS.size(); i++) {
            IndexScoreExcelDTO indexScoreExcelDTO = scoreDTOS.get(i);
            String expression = indexScoreExcelDTO.getExpression();
            final int a = i;
            ThreadPool.POOL.execute(() -> {
                AtomContextHolder.setContext(userIdentifier);
                String errorMsg = ExpressionUtils.getErrorMessageIfExpressionIsError(expression, expressionService, indexCallProcessor);
                if (StringUtils.isNotBlank(errorMsg)) {
                    msg.append(String.format("第%s行,指标表达式【%s】%s%s", a + 1, expression, errorMsg, separator));
                }
                countDownLatch.countDown();
            });
        }
        try {
            //  最好设置过期时间,不然可能造成无限等待
            countDownLatch.await(5, TimeUnit.SECONDS);
        } catch (Exception e) {
            logger.error("异步处理指标表达式检查时出错", e);
            if (e.getCause() instanceof RuntimeException) {
                throw  (RuntimeException) e.getCause();
            }else{
                throw new RuntimeException("处理指标表达式检查时异常",e);
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("其中检查表达式--消耗时间:" + (end - start));
        return msg.toString();
    }

运行结果

批量上传--总消耗时间:1791
其中模块名称重复--消耗时间:64
其中检查数据库重复--消耗时间:52
其中检查表达式--消耗时间:877
其中循环设置指标依赖集合--消耗时间:26

3.利用countDownLatch+Callable实现并行处理任务

Executor 框架中有一种方法可以实现异步,那就是实现 Callable 接口并重写call方法,并且Callable 可以在任务结束的时候提供一个返回值,Callable 的 call 方法也可以抛出异常;getErrorMessageIfExpressionIsError()方法中有业务断言需要抛出异常;

 /**
     * 指标表达式检查
     */
    private String checkExpression(List<IndexScoreExcelDTO> scoreDTOS) {
     StringBuilder msg = new StringBuilder();
        String separator = System.lineSeparator();
        long start = System.currentTimeMillis();
        CountDownLatch countDownLatch = new CountDownLatch(scoreDTOS.size());

        UserIdentifier userIdentifier = CurrentUserHelper.getUserIdentifier();
        ExecutorService executor = Executors.newFixedThreadPool(5);
        try {
        for (int i = 0; i < scoreDTOS.size(); i++) {
            IndexScoreExcelDTO indexScoreExcelDTO = scoreDTOS.get(i);
            String expression = indexScoreExcelDTO.getExpression();
            final int a = i;
                Callable<String> run = new Callable<String>(){
                    @Override
                    public String call() {
                        AtomContextHolder.setContext(userIdentifier);
                        return ExpressionUtils.getErrorMessageIfExpressionIsError(expression, expressionService, indexCallProcessor);
                    }
                };
                executor.submit(run);
                String errorMsg = run.call();
                if (StringUtils.isNotBlank(errorMsg)) {
                    msg.append(String.format("第%s行,指标表达式【%s】%s%s", a + 1, expression, errorMsg, separator));
                }
                countDownLatch.countDown();
        }
            //  最好设置过期时间,不然可能造成无限等待
            countDownLatch.await(5, TimeUnit.SECONDS);
        } catch (Exception e) {
            logger.error("异步处理指标表达式检查时出错", e);
            if (e.getCause() instanceof RuntimeException) {
                throw  (RuntimeException) e.getCause();
            }else{
                throw new RuntimeException("处理指标表达式检查时异常",e);
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("其中检查表达式--消耗时间:" + (end - start));
        return msg.toString();
}

运行结果

批量上传--总消耗时间:3555
其中模块名称重复--消耗时间:64
其中检查数据库重复--消耗时间:54
其中检查表达式--消耗时间:2659
其中循环设置指标依赖集合--消耗时间:32

4.利用CompletableFuture实现异步并行处理任务

CompletableFuture是jdk1.8引入的,里面弥补了jdk1.5的Futrue模型不足,学习谷歌的Google Guava中ListenableFuture和SettableFuture的特征,还提供了其它强大的功能,让Java拥有了完整的非阻塞编程模型:Future、Promise 和 Callback。
CompletableFuture能够将回调放到与任务不同的线程中执行,也能将回调作为继续执行的同步函数,在与任务相同的线程中执行。它避免了传统回调最大的问题,那就是能够将控制流分离到不同的事件处理器中。
indexCallProcessor接口中将http请求结果缓存到了ConcurrentHashMap中,在第5步中先获取缓存中的结果,如果结果为空再发送http请求。

 /**
     * 指标表达式检查 <br/>
     *  <p>1.利用串行Steam流异步多任务执行表达式检查</p>
     *  <p>2.当表达式执行完成后(CompletableFuture.whenComplete),拼接执行结果的错误</p>
     *  <p>3.当所有的CompletableFuture放入allOf,使用get()会阻塞主线程,直到allOf里面的所有线程都执行才唤醒。</p>
     *  <p>4.注:经简单测试(Stream+CompletableFuture异步+指定的线程池) > (Stream+CompletableFuture异步+ForkJoinPool)>(fori+countDownLatch+newFixedThreadPool)</p>
     */
    private String checkExpression(List<IndexScoreExcelDTO> scoreDTOS) {
        if(CollectionUtils.isEmpty(scoreDTOS)){
            return null;
        }
        StringBuilder msg = new StringBuilder();
        String separator = System.lineSeparator();
        UserIdentifier userIdentifier = CurrentUserHelper.getUserIdentifier();
        try {
            CompletableFuture[] expressionCheckFutureList =
                    scoreDTOS.stream().map(indexScoreExcelDTO ->
                        CompletableFuture.supplyAsync(() ->
                        {
                            // 因为是异步线程,需要传入当前user信息
                            ContextHolder.setContext(userIdentifier);
                            return ExpressionUtils.getErrorMessageIfExpressionIsError(indexScoreExcelDTO.getExpression(), expressionService, indexCallProcessor);
                        }, pool) .whenComplete((result, throwable) -> {
                                if (throwable == null && StringUtils.isNotBlank(result)) {
                                    msg.append(String.format("第%s行,指标表达式【%s】%s%s", indexScoreExcelDTO.getExcelLineNumber(), indexScoreExcelDTO.getExpression(), result, separator));
                                }
                            })
                    ).toArray(CompletableFuture[]::new);
            CompletableFuture.allOf(expressionCheckFutureList).get();
        } catch (InterruptedException e) {
            logger.error("异步处理指标表达式检查时的阻塞方法收到中断请求", e.getMessage());
            throw new RuntimeException("处理指标表达式检查时异常",e);
        } catch (ExecutionException e) {
            logger.error("异步处理指标表达式检查时出错", e);
            if (e.getCause() instanceof RuntimeException) {
                throw  (RuntimeException) e.getCause();
            }else{
                throw new RuntimeException("处理指标表达式检查时异常",e);
            }
        }
        return msg.toString();
    }

优化时间

------- 4.串行流+缓存http请求结果 优化后 第二次1926 第三次 2055--------
批量上传--总消耗时间:2050
其中模块名称重复--消耗时间:75
其中检查数据库重复--消耗时间:52
其中检查表达式--消耗时间:956
其中循环设置指标依赖集合--消耗时间:65

总结

本文主要从工作中上传时间过长导致失败的问题,引入了for循环优化的问题,采取了几种尝试性优化,如果我们时间较长的请求, 可以从线程并行、异步考虑,但也要合理采用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容