优化前

我们有一个定时任务,循环从数据库捞一批数据(业务上称它为资源)出来处理,一次捞取1000条。处理流程较长,需要查询这批资源的各种关联信息,还要根据组织查询一批用户,根据特定的算法计算出每一条资源需要分发给哪个用户,最后执行分发,然后把分发结果落库,并发送钉钉通知。

任务的性能很差。仅仅1000条资源,就需要十多分钟才能分发完。目前的业务一般一天会分发2w条资源左右,有时候会分发几个小时才发完,非常不合理。

  

定位耗时环节

于是我们打算优化一下这个任务。那首先要做的是理清楚代码逻辑和架构,第二步就是定位程序中比较耗时的环节,好做针对性的优化。

使用Arthas的trace命令可以查询某个方法的内部调用路径,并输出方法路径上的每个节点耗时。非常适合于用来定位任务的耗时环节。

❝尽量不要在生产环境的机器上执行trace命令,尤其是调用量较大的业务。❞

# 使用trace
trace demo.MathGame run
# 过滤掉jdk的函数
trace -j  demo.MathGame run
# 根据调用耗时过滤
trace demo.MathGame run '#cost > 10'

使用trace命令后,可以明显发现一些环节调用耗时不太正常,比如每个资源都会去捞取某些组织相应的用户列表,大概几千个用户,然后再遍历查询和组装每个用户的信息,这一套下来就差不多3秒钟了。

龟速

 

优化

在分析代码过程中,发现还有其它一些不合理的设计,后面对这些不合理的地方都进行了一定程度的优化。

使用缓存避免重复查询

我们发现,在组装用户信息的时候耗时比较严重,因为要去请求其它服务,然后还要去数据库拿数据。

优化前,重复请求

但每个资源都要去做同样的事情:拿某几个组织下所有用户的信息,产生了大量的重复查询。

很明显,我们只需要第一次去查询就够了,查询出来后,放入到缓存里面,后续资源只需要去缓存里面取就行了。

这里的缓存我们使用的是Redis,而不是内存缓存。因为我们在分发完成后会对用户信息做修改,而后面打算把它做成分布式的,多台机器共享用户信息,所以没有用内存缓存。

优化后

串行改并行

在代码分析过程中,发现很多是通过循环串行去做的。比如查询用户的详细信息并拼装,还有分发资源的时候、以及一些计算的时候。

虽然程序中在某些环节使用了多线程,但还是有些比较耗时的地方是串行的,导致整个程序比较慢。我们的机器是4核的,所以可以重复利用多核的优势,使用多线程去做一些性能上的优化。

主要有两种场景的串行可以改成并行:

循环

对于一个集合,我们下意识地通常会使用for循环去遍历它,做一些事情:

List<Result> list = new LinkedList<>();
for(Resource resource : resources) {User user = computeTarget(resource, users);Result result = distribute(resource, user);list.add(result);
}

如果循环体里面的操作比较耗时,这种串行循环就是比较慢的。这种情况可以简单地使用Java 8的并行Stream(其底层是Fork/Join框架),来达到一个并行的效果。不过如果改成了并行以后,需要注意线程安全的问题,比如上述代码,我们会把结果加到一个List里面,原本串行的时候使用一个简单的ArrayList就行了。但我们常用的ArrayList和LinkedList都不是线程安全的。所以这里需要替换成一个高性能的线程安全的List:

List<Result> list = new CopyOnWriteArrayList<>();
resources.parallelStream().forEach(resource -> {User user = computeTarget(resource, users);Result result = distribute(resource, user);list.add(result);
})

使用Java 8的并行Stream有一个问题,就是一个程序内部是用的同一个Fork/Join线程池,用户不好去调参。所以我们可以使用自定义的线程池来实现串行改并行的需求:

List<Result> list = new CopyOnWriteArrayList<>();List<Callable<Void>> callables = resources.stream().map(s -> (Callable<Void>) () -> {User user = computeTarget(resource, users);Result result = distribute(resource, user);list.add(result);return null;}).collect(Collectors.toList());executorService.invokeAll(callables, 20, TimeUnit.SECONDS);

没有前后关系的耗时操作

另一种典型的串行方式就是在代码中要调用多个API,但它们可能彼此并不需要有前后关系。比如我们可能要调用多个服务或者查询数据库,来最后拼装成一个东西,但每个操作要拼装的属性彼此是独立的,这个时候我们也可以改成并行的。

举个例子,改造前的代码可能是这样:

// 串行方式:
OneDTO one = oneService.get();
TwoDTO two = twoService.get();
ThreeDTO three = threeService.get();
nextHandle(new Result(one, two, three));

我们使用JDK自带的神器CompletableFuture来简单改造一下:

// 并行方式:
Result result = new Result();
CompletableFuture oneFuture = CompletableFuture.runAsync(() -> result.setOne(oneService.get()));
CompletableFuture twoFuture = CompletableFuture.runAsync(() -> result.setTwo(twoService.get()));
CompletableFuture threeFuture = CompletableFuture.runAsync(() -> result.setThree(threeService.get()));CompletableFuture.allOf(oneFuture, twoFuture, threeFuture).thenRun( () -> nextHandle(result))

同步改异步

同步改成异步有时候能够带来巨大的性能提升。一个操作不管你在同步的时候会消耗多少时间,一旦我改成了异步,那对于当前的程序来说,它就是无限趋近于0。

什么情况下同步可以改成异步?这个其实是业务场景决定的。在我这个场景,资源分发完成后的一些后置操作其实是可以直接改成异步的,比如:通知用户、分发结果写入数据库等。

同步改异步也非常简单,丢到线程池里面去做就完事了。

executorService.submit(() -> {someAction();
});

单机改分布式

前面介绍了串行改并行。比如我们1000个资源,如果一个执行1秒钟,那串行是不是就是1000秒。如果用10个线程去并行,就变成了100秒。

线程数量是受到机器限制的,不可能扩增到很大。但机器可以,并且多个机器和每台机器上的线程数量是可以相乘的。

我们这个服务假设有10台机器,然后再每台机器用10个线程去并行,那1000个资源分散到10台机器上去处理,只需要10秒。如果我们扩展到了100台机器,它只需要1秒。

我们来看看单机下的运行模式:我们从库里面捞1000个资源,然后自己处理了,其它机器比较空闲。

单机

单机改分布式其实很简单,我们只需要在入口处去改造就行了。捞取资源后,通过消息发出去,然后其它机器接收消息,获取资源开始处理。

❝或者发送端不捞取资源,直接切割好每个消息的start和offset,通过消息发送出去,让接收端去捞资源。❞

分布式

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

我是如何把一个15分钟的程序优化到了10秒的相关推荐

  1. excel其中一个页签慢_技巧 | 只需10秒,便可对Excel多页签进行快速有效汇总

    在Excel中,多页签的数据汇总,是一个出镜频率极高的问题,有人用VBA,有人用函数,有人用最传统的方式--总之,就是八仙过海,各显神通-- 然而,模式虽多,但当Excel小白实操起来,大脑又是一片空 ...

  2. 一个15分钟的视频,教你用Python创建自己的微信聊天机器人文中赠送全部源代码

    你好,我是程序员晚枫. 在社区交流群(

  3. 如何用15分钟开启高效的一天

    原文在这里:http://www.lifehack.org/articles/productivity/14-things-productive-people-the-first-15-minutes ...

  4. 在15分钟内使用Spring Boot和Spring Security构建一个Web应用程序

    "我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证. 开发人员 ...

  5. 一个具有Spring Boot,Spring Security和Stormpath的简单Web应用程序-15分钟

    建筑物身份管理,包括身份验证和授权? 尝试Stormpath! 我们的REST API和强大的Java SDK支持可以消除您的安全风险,并且可以在几分钟内实现. 注册 ,再也不会建立auth了! 更新 ...

  6. vr设备应用程序_在15分钟内构建一个VR Web应用程序

    vr设备应用程序 在15分钟内,您可以开发一个虚拟现实应用程序,并在Web浏览器,VR头盔或Google Daydream上运行它. 关键是A-Frame ,这是Mozilla VR Team构建的开 ...

  7. 开放下载!《15分钟打造你自己的小程序》(内附详细代码)

    简介: 零基础开发你自己的支付宝小程序,教程在手不迷路,从入门到精通,还有详细代码在里面哦~ <15分钟打造你自己的小程序>独家电子书上线啦!零基础开发你自己的支付宝小程序,手把手教你从入 ...

  8. 30分钟,让你成为一个更好的程序员

    我相信激励是非常重要的.这也是为什么我常常把时间管理(这些书激励我不管改进我的时间管理方法)的书和软件开发拿出来看看.我最近刚看完一本 书,"Apprenticeship Patterns: ...

  9. python 神经网络工具_神经网络15分钟入门!使用python从零开始写一个两层神经网络...

    本篇是该系列的第三篇,建议在阅读本篇文章之前先看前两篇文章. 在本文中将使用python实现之前描述的两层神经网络,并完成所提出的"象限分类"的问题. 需要注意的是,虽然标题叫做神 ...

最新文章

  1. 如何链接两个名字一样动态库
  2. slowfast解读:用于计算机视觉视频理解的双模CNN
  3. 什么是非集计模型_集计与非集计模型的关系
  4. 基于ASP.net耳机网店商城系统(前台页面+后台页面)
  5. Arbitrage(判断正环 spfa写法)
  6. 3004基于二叉链表的二叉树的双序遍历(附题意解释)
  7. 关于group by的用法 原理
  8. word 的使用(四)—— 对齐与美观(表格)
  9. EF并非我们想象的那么智能
  10. 微软面试智力题 (附答案)
  11. 开学季,微信公众号图文排版必备十大素材
  12. 计算机考研各科时间安排,计算机考研专业课复习全程的时间安排
  13. java毕业设计产品销售管理系统Mybatis+系统+数据库+调试部署
  14. 用js写出水仙花数_JavaScript求取水仙花数
  15. Linux兄弟连学习
  16. 调用链监控 - Tracing - APM
  17. HTML+CSS一篇文章搞定
  18. 关于1-n中缺失的1个数字算法的优化
  19. 云原生一站式DevOps平台----云效
  20. Mac:VSCode 批量替换快捷键

热门文章

  1. java项目 js报错红叉,解决js红叉,java Resources红叉
  2. java后端模拟http请求_Http请求后端
  3. 虚拟机机操作系统已禁用 cpu。请关闭或重置虚拟机。_黑科技教学丨Win10竟然内置了一台虚拟机?教你如何玩转它...
  4. 海信空调拥抱变革 扎实技术+多样产品组合引爆市场
  5. centos 安装低版本内核_Docker安装教程
  6. loadrunner之协议选择
  7. 计组之数据运算:3、定点数、浮点数
  8. 操作系统之计算机系统概述:1、操作系统概述(定义、功能、作用)
  9. (数据库系统概论|王珊)第七章数据库设计-第二节:需求分析
  10. 数据结构-树与二叉树