利用CompletableFuture提高接口的响应速度

之前在做一个监控大屏项目的时候,其中有一个图表是展示统计选中设备的指标数据,比如cpu负载、内存使用率等,我们知道,监控数据通常是放在时序数据库里面的,我们当时采用的是国产的tdengin数据,根据tdengin官方的建议,通常是一个设备单独建一张表,那么为了完成图表,就需要查询多张表(因为页面要支持同时选中多个设备查询),最开始我们为了快速上线功能,采用了线性查询,也就循环遍历页面传过来的设备信息,挨个去查对应的表,伪代码如下:

select avg(cpu_load),avg(memmory) from equipment1 where create_time between... and ...

select avg(cpu_load),avg(memmory) from equipment2 where create_time between... and ...

select avg(cpu_load),avg(memmory) from equipment3 where create_time between... and ...

....

假设每一个sql执行耗时100ms,那么接口响应的时间就是100*选中的设备数,比如如果选中了10个设备,那么接口响应时间就是1s,第一版上线之后,勉强能用,因为整个大屏的指标图表比较多,一两个图表慢一点,也不是那么的要紧,但是后面还是被diss了,于是我们想着优化一下代码,当时有两种思路,一种是加缓存,另一种是采用多线程的方式去查:


加缓存:把查询条件作为key,将查询到的数据放到redis里面去,但是这种方式,不太好评估过期时间,因为监控数据本身就是对数据实时性要求较高,而且我们检查tdengin数据库,发现负载并不高,于是我们采用多线程的方式并行的去查询

多线程:我们知道java里面在多线程实现方式上可以通过callable接口的方式来获取执行结果,而实际上callable接口最终在执行的时候其实是包装成FutureTask的,FutureTask实现了RunnableFuture接口,RunnableFuture实现了Runnable和Future接口,而在提交线程任务的时候,肯定是要用线程池的,也就是ThreadPoolExecutor,于是第二版我们采用ThreadPoolExecutor异步执行的方式,也就是下面代码中的execWithThreadPoolExecutor方法

import org.apache.commons.lang3.time.StopWatch;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ThreadPoolExecutorApplication {
    private static List<String> equipments = Stream.of("equipment1", "equipment2", "equipment3", "equipment4", "equipment5", "equipment6", "equipment7", "equipment1").collect(Collectors.toList());

    public static void main(String[] args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        execWithCompletableFuture();
        stopWatch.stop();
        System.err.println(stopWatch);
    }

    private static void execSerialize() {
        for (String equipment : equipments) {
            System.err.println(equipment + "此处模拟sql执行。。。。耗时100ms");
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static void execWithThreadPoolExecutor() {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 1800, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100));
        List<Future> futures = new ArrayList<>(equipments.size());
        for (String equipment : equipments) {
            Future<Integer> future = threadPoolExecutor.submit(() -> {
                System.err.println(equipment + "此处模拟sql执行。。。。耗时100ms");
                TimeUnit.MILLISECONDS.sleep(100);
                return 100;
            });
            futures.add(future);
        }
        for (Future future : futures) {
            try {
                System.err.println(future.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        threadPoolExecutor.shutdown();
    }

    private static void execWithCompletableFuture() {
        List<CompletableFuture> futures = new ArrayList<>(equipments.size());
        for (String equipment : equipments) {
            CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
                System.err.println(equipment + "此处模拟sql执行。。。。耗时100ms");
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return 100;
            });
            futures.add(future);
        }
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])).join();
        for (CompletableFuture future : futures) {
            try {
                System.err.println(future.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
}

另外一种就是代码中的execWithCompletableFuture方法,CompletableFuture其本质还是线程池的玩法,它还有其他很多编排线程的玩法,比ThreadPoolExecutor花活多的多,后续有时间再追更

ps:可以运行对比一下execSerialize和execWithCompletableFuture以及execWithThreadPoolExecutor的耗时,随着equipments数量的增加,execSerialize执行市场也是线性增长的,而execWithCompletableFuture和execWithThreadPoolExecutor的耗时相对稳定的多

猜你喜欢

转载自blog.csdn.net/qq_17805707/article/details/133268544