CompletableFuture原理与实践-APP楼层异步化

随着商城流量增长,同步加载方式暴露出性能瓶颈。文章探讨了商城服务的I/O密集特性,强调并行加载的必要性,并详细介绍了Java中的CompletableFuture如何实现异步并行数据获取,减少响应时间,提高系统吞吐量。同时,指出了CompletableFuture的实现原理和设计思想,并提到了自定义线程池的重要性。
摘要由CSDN通过智能技术生成

背景

随着用户数量的持续上升,商城各系统服务面临的压力也越来越大。作为网站的核心,商城提供了首页,商品详情,门店详情,订单页一系列核心功能,业务对系统吞吐量的要求也越来越高。而商城API服务是流量入口,所有商城端流量都会由其调度、聚合,对外面向商家提供功能接口,对内调度各个下游服务获取数据进行聚合,具有鲜明的I/O密集型(I/O Bound)特点。在当前日订单规模已达千万级的情况下,使用同步加载方式的弊端逐渐显现,因此我们开始考虑将同步加载改为并行加载的可行性。

为什么需要并行

商城API服务是典型的I/O密集行服务,除此之外,商城首页,门店详情,商品详情,等业务还有两大特点:

  1. 一次查询必须返回各个楼层的全部信息,首页全部信息,包括:搜索框,热搜词,banner,宫格瓷片,金刚区,feed流,弹窗等,需要从下游调用近30个中台服务。
  2. 商城与服务端的交互非常频繁,feed流中展示的sku信息的价格为实时变动,导致每次数据拉取时,都需要拉取最新最新数据。

 

在商城入口如此大的流量下,为了保证用户体验,保证接口的高性能,并行从下游获取数据就成为必然。

实现方式

并行从下游获取数据,从IO模型上来讲分为同步模型异步模型

同步模型

在同步调用的场景下,接口耗时长、性能差,接口响应时长T > T1+T2+……+Tn,这时为了缩短接口的响应时间,一般会使用线程池的方式并行获取数据,首页,门详,商详目前采用的正是这种方式。

 

缺点

  1. CPU资源大量浪费在阻塞等待上,导致CPU资源利用率低
  2. 为了增加并发度,会引入更多额外的线程池,随着CPU调度线程数的增加,会导致更严重的资源争用,宝贵的CPU资源被损耗在上下文切换上

CompletableFuture

身世

CompletableFuture是由Java 8引入的,实现了Future、CompletionStage两个接口

  • Future

        Future表示异步计算的结果

  • CompletionStage

        CompletionStage用于表示异步执行过程中的一个步骤(Stage),这个步骤可能是由另外一个CompletionStage触发的,随着当前步骤的完成,也可能会触发其他一系列CompletionStage的执行。从而我们可以根据实际业务对这些步骤进行多样化的编排组合,CompletionStage接口正是定义了这样的能力,我们可以通过其提供的thenAppy、thenCompose等函数式编程方法来组合编排这些步骤

使用

分类

在使用CompletableFuture时,可以按照以来的数量进行分类为:零依赖,一元依赖,二元依赖,多元依赖

  • 零依赖
  1. 使用CompletableFuture.runAsync或CompletableFuture.supplyAsync发起异步调用
  2. CompletableFuture.completedFuture()
  3. 始化一个未完成的CompletableFuture,然后通过complete()、completeExceptionally(),完成该CompletableFuture
  • 一元依赖

        一元CompletableFuture的依赖可以通过thenApply、thenAccept、thenCompose等方法来实现

  • 二元依赖

        二元依赖可以通过thenCombine等回调来实现

  • 多元依赖

        多元依赖可以通过allOfanyOf方法来实现,区别是当需要多个依赖全部完成时使用allOf,当多个依赖中的任意一个完成即可时使用anyOf,然后再执行thenApply获取其它的方案来出发逻辑单元

原理

CompletableFuture中包含两个字段:resultstack。result用于存储当前CF的结果,stack(Completion)表示当前CompletableFuture完成后需要触发的依赖动作(Dependency Actions),去触发依赖它的CompletableFuture的计算,依赖动作可以有多个(表示有多个依赖它的CompletableFuture),以栈的形式存储,stack表示栈顶元素。

这种方式类似“观察者模式”,依赖动作(Dependency Action)都封装在一个单独Completion子类中。下面是Completion类关系结构图。CompletableFuture中的每个方法都对应了图中的一个Completion的子类,Completion本身是观察者的基类。

  • UniCompletion继承了Completion,是一元依赖的基类,例如thenApply的实现类UniApply就继承自UniCompletion。
  • BiCompletion继承了UniCompletion,是二元依赖的基类,同时也是多元依赖的基类。例如thenCombine的实现类BiRelay就继承自BiCompletion。

 设计思想

按照类似“观察者模式”的设计思想,原理分析可以从“观察者”和“被观察者”两个方面着手。由于回调种类多,但结构差异不大,以thenApply为例,如下图

 

被观察者

  1. 每个CompletableFuture都可以被看作一个被观察者,其内部有一个Completion类型的链表成员变量stack,用来存储注册到其中的所有观察者。当被观察者执行完成后会弹栈stack属性,依次通知注册到其中的观察者。上面例子中步骤fn2就是作为观察者被封装在UniApply中。
  2. 被观察者CF中的result属性,用来存储返回结果数据。这里可能是一次RPC调用的返回值,也可能是任意对象,在上面的例子中对应步骤fn1的执行结果。

观察者

CompletableFuture支持很多回调方法,例如thenAccept、thenApply、exceptionally等,这些方法接收一个函数类型的参数f,生成一个Completion类型的对象(即观察者),并将入参函数f赋值给Completion的成员变量fn,然后检查当前CF是否已处于完成状态(即result != null),如果已完成直接触发fn,否则将观察者Completion加入到CF的观察者链stack中,再次尝试触发,如果被观察者未执行完则其执行完毕之后通知触发。

  1. 观察者中的dep属性:指向其对应的CompletableFuture,在上面的例子中dep指向sonCompletableFuture。
  2. 观察者中的src属性:指向其依赖的CompletableFuture,在上面的例子中src指向parentCompletableFuture。
  3. 观察者Completion中的fn属性:用来存储具体的等待被回调的函数。这里需要注意的是不同的回调方法(thenAccept、thenApply、exceptionally等)接收的函数类型也不同,即fn的类型有很多种,在上面的例子中fn指向fn2。

 补充

  • 线程池

        CompletableFuture是否使用默认线程池的依据,和机器的CPU核心数有关。当CPU核心数-1大于1时,才会使用默认的线程池,否则将会为每个CompletableFuture的任务创建一个新线程去执行。即,CompletableFuture的默认线程池,只有在双核以上的机器内才会使用。在双核及以下的机器中,会为每个任务创建一个新线程,等于没有使用线程池,且有资源耗尽的风险。因此建议,在使用CompletableFuture时,务必要自定义线程池。因为即便是用到了默认线程池,池内的核心线程数,也为机器核心数-1。也就意味着假设你是4核机器,那最多也只有3个核心线程,对于CPU密集型的任务来说倒还好,但是我们平常写业务代码,更多的是IO密集型任务,对于IO密集型的任务来说,这其实远远不够用的,会导致大量的IO任务在等待,导致吞吐率大幅度下降,即默认线程池比较适用于CPU密集型任务。

  • 回调及加强

        无法做到对每一个执行单元的回调。譬如A执行完毕成功了,后面是B,我希望A在执行完后就有个回调结果,方便我监控当前的执行状况,或者打个日志什么的。失败了,我也可以记录个异常信息什么的。此时,CompleteableFuture就无能为力了。

AsyncTool

 

 

Iworker

最小执行单元需要实现该接口

/**
 * 每个最小执行单元需要实现该接口
 * T,V两个泛型,分别是入参和出参类型
 *
 * 多个不同的worker之间,没有关联,分别可以有不同的入参、出参类型
 */
@FunctionalInterface
public interface IWorker<T, V> {
    /**
     * 在这里做耗时操作,如rpc请求、IO等
     *
     * @param object      object
     * @param allWrappers 持有所有Wrapper的引用,key是id
     */
    V action(T object, Map<String, WorkerWrapper> allWrappers) throws InterruptedException;

    /**
     * 超时、异常时,返回的默认值
     *
     * @return 默认值
     */
    default V defaultValue() {
        return null;
    }
}

 Icallback

/**
 * 每个执行单元Worker执行前调用begin,执行后调用result
 */
@FunctionalInterface
public interface ICallback<T, V> {

    /**
     * 任务开始的监听
     */
    default void begin() {

    }

    /**
     * worker执行完毕后,会回调该接口,带着执行成功、失败、原始入参、和详细的结果。
     */
    void result(boolean success, T param, WorkResult<V> workResult);
}

DefaultCallback

/**
 * 默认回调类,如果不设置的话,会默认给这个回调
 */
public class DefaultCallback<T, V> implements ICallback<T, V> {
    @Override
    public void begin() {
        
    }

    @Override
    public void result(boolean success, T param, WorkResult<V> workResult) {

    }

}

WorkerWrapper

/**
 * 对每个worker及callback进行包装
 */
public class WorkerWrapper<T, V> {
    //该wrapper的唯一标识
    private String id;
    //worker将来要处理的param
    private T param;
    private IWorker<T, V> worker;
    private ICallback<T, V> callback;
    /**
     * 在自己后面的wrapper,如果没有,自己就是末尾;如果有一个,就是串行;如果有多个,有几个就需要开几个线程</p>
     */
    private List<WorkerWrapper<?, ?>> nextWrappers;
    /**
     * 依赖的wrappers,有2种情况,1:必须依赖的全部完成后,才能执行自己 2:依赖的任何一个、多个完成了,就可以执行自己
     * 通过must字段来控制是否依赖项必须完成
     */
    private List<DependWrapper> dependWrappers;
    /**
     * 标记该事件是否已经被处理过了,譬如已经超时返回false了,后续rpc又收到返回值了,则不再二次回调
     * <p>
     * 0-init, 1-finish, 2-error, 3-working
     */
    private AtomicInteger state = new AtomicInteger(0);
    /**
     * 收集所有的wrapper,key是id,以便用于在Worker工作单元中,获取任意Worker的执行结果。
     */
    private Map<String, WorkerWrapper> forParamUseWrappers;
    /**
     * 存放任务结果,action中的返回值会赋值给它,在result的回调中,可以拿到这个结果。
     */
    private volatile WorkResult<V> workResult = WorkResult.defaultResult();
    /**
     * 是否在执行自己前,去校验nextWrapper的执行结果<p>
     * 1   4
     * -------3
     * 2
     * 如这种在4执行前,可能3已经执行完毕了(被2执行完后触发的),那么4就没必要执行了。
     * 注意,该属性仅在nextWrapper数量<=1时有效,>1时的情况是不存在的
     */
    private volatile boolean needCheckNextWrapperResult = true;
}

例子

home-server: 本项目以电商APP楼层设计为例,使用AsyncTool为服务编排框架。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值