系统性能优化,一直是程序员从初级通向高阶的必修课,也是在面试中被重点考查的点。
下图是我基于对性能优化的理解,梳理出来的五大方向和对应的16种策略:
下面我们就来一一进行讲解。
预计算
预计算是一种"笨鸟先飞"的思想,与之相对应的是实时计算。
实时计算: 当用户请求到来的时候,才进行数据的结果计算。
预计算: 在用户请求到来之前,先将结果计算好,并保存在缓存或数据库中。当用户触发请求的时候,直接返回准备好的结果。
预计算的优缺点非常分明,其优点是性能足够高。缺点是,可能数据结果为非实时数据,存在时延性,也可能是技术方案比较复杂。
预计算的另外需要考虑的点是:结果命中率和计算范围,也就是说,我们希望结果命中率越高越好,而计算范围越小越好。因为计算范围越大,计算的周期就越长,对硬件存储的消耗也就越大。
预计算又可以分为全量预计算和存量预计算 + 增量即席计算两种。
全量预计算
假设如下场景,我们计算一张销售报表:
报表中的内容为这家企业季度销售情况,如果用全量预计算的方式,我们按照前图的方式,通过每十分钟跑一次定时任务,将页面上的几十个业务汇总指标全部计算一遍,然后存储到MySQL或Redis中,等待前端请求调用。
这种方式叫全量预计算,代码实现简单,可能一条复杂大SQL就能搞定,但如果数据量特别庞大,可能会出现执行时间很长,甚至跑不出来结果的情况。
存量预计算 + 增量即席计算
还是以上图的销售报表为例,假设今天为12月15日,那么前三个季度的销售额已经是不可变的存量数据了,我们只需要预计算一次,并将它存储到MySQL或Redis中即可,不用每次跑任务都进行计算。
接下来我们再说说第四个季度,其实12月14日及以前的销售额也是不可变的存量数据,我们一样可以将它存储下来以作备用。
这时,当前端的请求发送过来,我们只需要即席计算12月15日的销售额数据,然后再跟12月14日及以前的销售额数据进行累加,即可得出实时的第四季度销售额数据。
另外一个问题是,在什么时间点进行存量数据累加,比较常见的做法是,每天的0点跑定时任务,把前一天的全天销售数据累加进之前存量数据中。
这种方式叫存量预计算 + 增量即席计算,同时兼备高性能和实时性的优点,唯一的缺点就是技术方案相对复杂。
总之,预计算场景非常适合于对数据时延性要求不高的统计分析场景。
并行计算
与并行计算相对应的是串行计算。
并行计算的所体现的思想是"人多力量大,众人拾柴火焰高",旨在通过将任务拆解后,以多路并行的方式,将任务执行的总时长进行缩短,以达到提升性能的目的。
其中,并行计算又可以分为:单机多线程并行计算、集群并行计算和集群并行 + 单机多线程并行计算。
单机多线程并行计算
目前Java的JUC包中有很多非常好用的工具类,如:ThreadPoolExecutor、ForkJoinPool、newFixedThreadPool、newCachedThreadPool、CountDownLatch、CyclicBarrier、Phaser、CompletableFuture等。
我们假设给大量用户进行短信发送的场景,newFixedThreadPool示例如下:
java
package com.example.demo;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MessageSender {
public static void main(String[] args) {
List<String> userList = List.of("张一", "张二", "张三", "张四", "张五", "张六", "张七", "张八", "张九", "张十",
"王一", "王二", "王三", "王四", "王五", "王六", "王七", "王八", "王九", "王十");
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (String user : userList) {
executorService.execute(new Task(user));
}
executorService.shutdown();
}
}
class Task implements Runnable {
private String user;
public Task(String user) {
this.user = user;
}
public void run() {
System.out.println("调用发送短信SDK,给用户" + user + "发短信。");
}
}
用CompletableFuture电商下单的前置校验场景:
vbnet
package com.example.demo;
import java.util.concurrent.*;
public class Order {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> basicCheck = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "基本信息校验";
});
CompletableFuture<String> riskControlCheck = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "风控信息校验";
});
CompletableFuture<String> result = CompletableFuture.allOf(basicCheck, riskControlCheck).thenApply(res -> {
return basicCheck.join() + "和" + riskControlCheck.join();
});
System.out.println("订单完成" + result.join() + ",可以正式下单了。");
}
}
除此之外,Java 1.8的stream中的并行流模式------parallelStream(),也是一种不错的选择。
我们还是假设给大量用户进行短信发送的场景:
typescript
import java.util.List;
public class MessageSender {
public static void main(String[] args) {
List<String> userList = List.of("张一", "张二", "张三", "张四", "张五", "张六", "张七",
"张八", "张九", "张十", "王一", "王二", "王三", "王四", "王五", "王六", "王七", "王八", "王九", "王十");
userList.parallelStream().forEach((entry) -> {
System.out.println("调用发送短信SDK,给用户" + entry + "发短信。");
});
}
}
集群并行计算
大家可能不太清楚,单机多线程并行计算和集群并行计算各自的适用场景,我这里先来解释一下。
单机多线程并行计算场景,适用于用户触发请求的短时间执行场景,而集群并行计算场景,则适用于后台定时任务的长时间处理场景。
原因在于,在集群模式下,如果其中的一台服务器,以单机多线程模式长时间处理任务,容易出现硬件资源消耗倾斜的情况,再加上还要处理业务请求,容易使其成为集群中的性能瓶颈点。
集群并行计算模式,可以使用XXL-JOB分布式任务调度平台的分片广播模式进行实现。
其实现核心原理为,通过XXL-JOB的调度器,往执行器集群中的每个节点都发送一个请求,然后各节点通过自己不同的shardIndex来执行不同的任务。
代码如下:
csharp
@XxlJob("broadcastJob")
public void broadcastJob() {
int shardCount = 10; // 分片总数
int shardIndex = XxlJobHelper.getShardIndex(); // 当前分片项
// 执行任务逻辑
for (int i = 1; i <= 全部任务ID; i++) {
if (i % shardCount == shardIndex) {
// 当前分片项需要执行的任务逻辑
System.out.println("分片 " + shardIndex + "负责执行任务" + i);
}
}
}
集群并行 + 单机多线程并行计算
这个不用过多解释,相当于前两种模式的集合,适用于将该集群作为特定的任务服务器,并将计算能力拉满的情况。
常见的大数据框架:Flink、Spark、Storm都是使用的这种方式。
总之,并行计算场景非常适合于,可将一个大任务拆解成若干个小任务,且各任务间没有先后依赖关系的场景。
结语
本期我们介绍了常见的两种性能优化方向,预计算和并行计算,以及对应的五种策略,后面几篇再跟大家介绍异步计算、存储系统优化和其他算法优化。