系统性能优化系列——五大方向,16种策略

系统性能优化,一直是程序员从初级通向高阶的必修课,也是在面试中被重点考查的点。

下图是我基于对性能优化的理解,梳理出来的五大方向和对应的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都是使用的这种方式。

总之,并行计算场景非常适合于,可将一个大任务拆解成若干个小任务,且各任务间没有先后依赖关系的场景。

结语

本期我们介绍了常见的两种性能优化方向,预计算和并行计算,以及对应的五种策略,后面几篇再跟大家介绍异步计算、存储系统优化和其他算法优化。

相关推荐
NMBG224 分钟前
[JAVAEE] 面试题(四) - 多线程下使用ArrayList涉及到的线程安全问题及解决
java·开发语言·面试·java-ee·intellij-idea
王二端茶倒水23 分钟前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员
夜色呦1 小时前
现代电商解决方案:Spring Boot框架实践
数据库·spring boot·后端
爱敲代码的小冰2 小时前
spring boot 请求
java·spring boot·后端
java小吕布3 小时前
Java中的排序算法:探索与比较
java·后端·算法·排序算法
Goboy3 小时前
工欲善其事,必先利其器;小白入门Hadoop必备过程
后端·程序员
李少兄4 小时前
解决 Spring Boot 中 `Ambiguous mapping. Cannot map ‘xxxController‘ method` 错误
java·spring boot·后端
代码小鑫4 小时前
A031-基于SpringBoot的健身房管理系统设计与实现
java·开发语言·数据库·spring boot·后端
Json____4 小时前
学法减分交管12123模拟练习小程序源码前端和后端和搭建教程
前端·后端·学习·小程序·uni-app·学法减分·驾考题库
monkey_meng4 小时前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust