网页制作与网站建设策划书案例,开封网站建设流程,网站制作公司有没有版权,建设网站的费用如何账务处理0.背景
一个接口可能需要调用 N 个其他服务的接口#xff0c;这在项目开发中还是挺常见的。举个例子#xff1a;用户请求获取订单信息#xff0c;可能需要调用用户信息、商品详情、物流信息、商品推荐等接口#xff0c;最后再汇总数据统一返回。
如果是串行#xff08;按…0.背景
一个接口可能需要调用 N 个其他服务的接口这在项目开发中还是挺常见的。举个例子用户请求获取订单信息可能需要调用用户信息、商品详情、物流信息、商品推荐等接口最后再汇总数据统一返回。
如果是串行按顺序依次执行每个任务执行的话接口的响应速度会非常慢。考虑到这些接口之间有大部分都是 无前后顺序关联 的可以 并行执行 就比如说调用获取商品详情的时候可以同时调用获取物流信息。通过并行执行多个任务的方式接口的响应速度会得到大幅优化。 对于存在前后顺序关系的接口调用可以进行编排如下图所示。 在外卖商家端交易业务存在这样的特点 1.服务端必须一次返回订单卡片所有内容根据商家端和服务端的“增量同步协 议注 1”服务端必须一次性返回订单的所有信息包含订单主信息、商品、结算、配送、用户信息、骑手信息、餐损、退款、客服赔付参照下面订单卡片截图等需要从下游三十多个服务中获取数据。在特定条件下如第一次登录和长时间没登录的情况下客户端会分页拉取多个订单这样发起的远程调用会更多。 2.商家端和服务端交互频繁商家对订单状态变化敏感多种推拉机制保证每次变更能够触达商家导致 App 和服务端的交互频繁每次变更需要拉取订单最新的全部内容在外卖交易链路如此大的流量下为了保证商家的用户体验保证接口的高性能并行从下游获取数据就成为必然。 综上所述在多模块大流量的场景下原先的串行接口的响应是行不通的所以并行接口的响应是必然的
1.CompletableFuture使用与原理
1.1Future介绍
Future 类是异步思想的典型运用主要用在一些需要执行耗时任务的场景避免程序一直原地等待耗时任务执行完成执行效率太低。具体来说是这样的当我们执行某一耗时的任务时可以将这个耗时任务交给一个子线程去异步执行同时我们可以干点其他事情不用傻傻等待耗时任务执行完成。等我们的事情干完后我们再通过 Future 类获取到耗时任务的执行结果。这样一来程序的执行效率就明显提高了。
这其实就是多线程中经典的 Future 模式你可以将其看作是一种设计模式核心思想是异步调用主要用在多线程领域并非 Java 语言独有。
在 Java 中Future 类只是一个泛型接口位于 java.util.concurrent 包下其中定义了 5 个方法主要包括下面这 4 个功能
取消任务判断任务是否被取消;判断任务是否已经执行完成;获取任务执行结果。
// V 代表了Future执行的任务返回值的类型
public interface FutureV {// 取消任务执行// 成功取消返回 true否则返回 falseboolean cancel(boolean mayInterruptIfRunning);// 判断任务是否被取消boolean isCancelled();// 判断任务是否已经执行完成boolean isDone();// 获取任务执行结果V get() throws InterruptedException, ExecutionException;// 指定时间内没有返回计算结果就抛出 TimeOutException 异常V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutExceptio}
//举例
public class FutureTest {public static void main(String[] args) throws InterruptedException, ExecutionException {ExecutorService executorService Executors.newFixedThreadPool(5);UserInfoService userInfoService new UserInfoService();MedalService medalService new MedalService();long userId 666L;long startTime System.currentTimeMillis();//调用用户服务获取用户基本信息FutureTaskUserInfo userInfoFutureTask new FutureTask(new CallableUserInfo() {Overridepublic UserInfo call() throws Exception {return userInfoService.getUserInfo(userId);}});executorService.submit(userInfoFutureTask);Thread.sleep(300); //模拟主线程其它操作耗时FutureTaskMedalInfo medalInfoFutureTask new FutureTask(new CallableMedalInfo() {Overridepublic MedalInfo call() throws Exception {return medalService.getMedalInfo(userId);}});executorService.submit(medalInfoFutureTask);UserInfo userInfo userInfoFutureTask.get();//获取个人信息结果MedalInfo medalInfo medalInfoFutureTask.get();//获取勋章信息结果System.out.println(总共用时 (System.currentTimeMillis() - startTime) ms);}
}class UserInfoService {public UserInfo getUserInfo(Long userId) throws InterruptedException {Thread.sleep(300);//模拟调用耗时return new UserInfo(666, 捡田螺的小男孩, 27); //一般是查数据库或者远程调用返回的}}class MedalService {public MedalInfo getMedalInfo(long userId) throws InterruptedException {Thread.sleep(500); //模拟调用耗时return new MedalInfo(666, 守护勋章);}
}
如果我们不使用Future进行并行异步调用而是在主线程串行进行的话耗时大约为300500300 1100 ms。可以发现future线程池异步配合提高了程序的执行效率。
但是Future对于结果的获取不是很友好只能通过阻塞或者轮询的方式得到任务的结果。
Future.get() 就是阻塞调用在线程获取结果之前get方法会一直阻塞。Future提供了一个isDone方法可以在程序中轮询这个方法查询执行结果。
阻塞的方式和异步编程的设计理念相违背而轮询的方式会耗费无谓的CPU资源。因此JDK8设计出CompletableFuture。CompletableFuture提供了一种观察者模式类似的机制可以让任务执行完成后通知监听的一方。
1.2CompletableFuture的背景和定义
1.2.1CompletableFuture解决的问题
CompletableFuture 是由 Java 8 引入的在 Java8 之前我们一般通过 Future 实现 异步。 ● Future 用于表示异步计算的结果只能通过阻塞或者轮询的方式获取结果 而且不支持设置回调方法Java 8 之前若要设置回调一般会使用 guava 的ListenableFuture回调的引入又会导致臭名昭著的回调地狱下面的例子会通过 ListenableFuture 的使用来具体进行展示。 ● CompletableFuture 对 Future 进行了扩展可以通过设置回调的方式处理计 算结果同时也支持组合操作支持进一步的编排同时一定程度解决了回调地狱的问题。 下面将举例来说明我们通过 ListenableFuture、CompletableFuture 来实现异步 的差异。假设有三个操作 step1、step2、step3 存在依赖关系其中 step3 的执行依赖 step1 和 step2 的结果。
Future(ListenableFuture) 的实现回调地狱如下 ExecutorService executor Executors.newFixedThreadPool(5);ListeningExecutorService guavaExecutor MoreExecutors.listeningDecorator(executor);ListenableFutureString future1 guavaExecutor.submit(() - {//step 1System.out.println(“执行 step 1”);return“step1 result”;});ListenableFutureString future2 guavaExecutor.submit(() - {//step 2System.out.println(“执行 step 2”);return“step2 result”;});ListenableFutureListString future1And2 Futures.allAsList(future1,future2);
Futures.addCallback(future1And2,new FutureCallbackListString(){Overridepublic void onSuccess (List String result) {System.out.println(result);ListenableFutureString future3 guavaExecutor.submit(() - {System.out.println(“执行 step 3”);return“step3 result”;});Futures.addCallback(future3, new FutureCallbackString() {Overridepublic void onSuccess(String result) {System.out.println(result);}Overridepublic void onFailure(Throwable t) {}}, guavaExecutor);}Overridepublic void onFailure (Throwable t){}},guavaExecutor);CompletableFuture 的实现如下
ExecutorService executor Executors.newFixedThreadPool(5);CompletableFutureString cf1 CompletableFuture.supplyAsync(() - {System.out.println(“执行 step 1”);}return“step1 result”;
}, executor);
CompletableFutureString cf2 CompletableFuture.supplyAsync(() - {System.out.println(“执行 step 2”);return“step2 result”;
});
cf1.thenCombine(cf2, (result1, result2) - {System.out.println(result1 “ , “ result2);System.out.println(“执行 step 3”);return“step3 result”;
}).thenAccept(result3 - System.out.println(result3));显然CompletableFuture 的实现更为简洁可读性更好。
1.2.1 CompletableFuture 的定义 CompletableFuture 实现了两个接口如上图所示Future、CompletionStage。 Future 表示异步计算的结果CompletionStage 用于表示异步执行过程中的一个步 骤Stage这个步骤可能是由另外一个 CompletionStage 触发的随着当前步骤的完成也可能会触发其他一系列 CompletionStage 的执行。从而我们可以根据实际业务对这些步骤进行多样化的编排组合CompletionStage 接口正是定义了这样的能力我们可以通过其提供的 thenAppy、thenCompose 等函数式编程方法来组合编排这些步骤。
1.3CompletableFuture 的使用
下面我们通过一个例子来讲解 CompletableFuture 如何使用使用 CompletableFuture 也是构建依赖树的过程。一个 CompletableFuture 的完成会触发另外一系列依赖它的 CompletableFuture 的执行 如上图所示这里描绘的是一个业务接口的流程其中包括 CF1\CF2\CF3\CF4\CF5 共 5 个步骤并描绘了这些步骤之间的依赖关系每个步骤可以是一次 RPC 调用、一次数据库操作或者是一次本地方法调用等在使用 CompletableFuture 进行 异步化编程时图中的每个步骤都会产生一个 CompletableFuture 对象最终结果
也会用一个 CompletableFuture 来进行表示。根据 CompletableFuture 依赖数量可以分为以下几类零依赖、一元依赖、二元依赖和多元依赖
1.3.1 零依赖CompletableFuture 的创建
我们先看下如何不依赖其他 CompletableFuture 来创建新的 CompletableFuture 如上图红色链路所示接口接收到请求后首先发起两个异步调用 CF1、CF2主要有三种方式 ExecutorService executor Executors.newFixedThreadPool(5);//1、使用 runAsync 或 supplyAsync 发起异步调用CompletableFutureString cf1 CompletableFuture.supplyAsync(() - {return result1;}, executor);//2、CompletableFuture.completedFuture() 直接创建一个已完成状态的CompletableFutureCompletableFutureString cf2 CompletableFuture.completedFuture(result2);//3、先初始化一个未完成的 CompletableFuture然后通过 complete()、completeExceptionally()完成该 CompletableFutureCompletableFutureString cf new CompletableFuture();cf.complete(success);第三种方式的一个典型使用场景就是将回调方法转为 CompletableFuture然后再依赖 CompletableFure 的能力进行调用编排示例如下
FunctionalInterface
public interface ThriftAsyncCall {void invoke() throws TException;
}/*** 该方法为美团内部 rpc 注册监听的封装可以作为其他实现的参照* OctoThriftCallback 为 thrift 回调方法* ThriftAsyncCall 为自定义函数用来表示一次 thrift 调用定义如上*/static T CompletableFutureT toCompletableFuture(finalOctoThriftCallback?, T callback, ThriftAsyncCall thriftCall) {// 新建一个未完成的 CompletableFutureCompletableFutureT resultFuture new CompletableFuture();// 监听回调的完成并且与 CompletableFuture 同步状态callback.addObserver(new OctoObserverT() {Overridepublic void onSuccess(T t) {resultFuture.complete(t);}Overridepublic void onFailure(Throwable throwable) {resultFuture.completeExceptionally(throwable);}});if (thriftCall ! null) {try {thriftCall.invoke();} catch (TException e) {resultFuture.completeExceptionally(e);}}return resultFuture;}1.3.2 一元依赖依赖一个 CF 如上图红色链路所示CF3CF5 分别依赖于 CF1 和 CF2这种对于单个 CompletableFuture 的依赖可以通过 thenApply、thenAccept、thenCompose 等方法来实现代码如下所示
CompletableFutureString cf3 cf1.thenApply(result1 - {//result1 为 CF1 的结果//......return“result3”;
});
CompletableFutureString cf5 cf2.thenApply(result2 - {//result2 为 CF2 的结果//......return“result5”;
});1.3.3 二元依赖依赖两个 CF 如上图红色链路所示CF4 同时依赖于两个 CF1 和 CF2这种二元依赖可以通过thenCombine 等回调来实现如下代码所示
CompletableFutureString cf4 cf1.thenCombine(cf2, (result1, result2) - {//result1 和 result2 分别为 cf1 和 cf2 的结果return“result4”;
});1.3.4 多元依赖依赖多个 CF 如上图红色链路所示整个流程的结束依赖于三个步骤 CF3、CF4、CF5这种多元依赖可以通过 allOf 或 anyOf 方法来实现区别是当需要多个依赖全部完成时使用 allOf当多个依赖中的任意一个完成即可时使用 anyOf如下代码所示
CompletableFutureVoid cf6 CompletableFuture.allOf(cf3, cf4, cf5);
CompletableFutureString result cf6.thenApply(v - {// 这里的 join 并不会阻塞因为传给 thenApply 的函数是在 CF3、CF4、CF5 全部完成时才会执行 。result3 cf3.join();result4 cf4.join();result5 cf5.join();// 根据 result3、result4、result5 组装最终 result;return“result”;
});1.4CompletableFuture 原理
CompletableFuture 中包含两个字段result 和 stack。result 用于存储当前 CF的结果stackCompletion表示当前 CF 完成后需要触发的依赖动作Dependency Actions去触发依赖它的 CF 的计算依赖动作可以有多个表示有多个依赖它的 CF以栈Treiber stack的形式存储stack 表示栈顶元素。 这种方式类似“观察者模式”依赖动作Dependency Action都封装在一个单独Completion 子类中。下面是 Completion 类关系结构图。CompletableFuture 中的每个方法都对应了图中的一个 Completion 的子类Completion 本身是观察者的基类。 1.UniCompletion 继承了 Completion是一元依赖的基类例如 thenApply的实现类 UniApply 就继承自 UniCompletion。 2.BiCompletion 继承了 UniCompletion是二元依赖的基类同时也是多元依赖的基类。例如 thenCombine 的实现类 BiRelay 就继承自 BiCompletion。 1.4.1 CompletableFuture 的设计思想
按照类似“观察者模式”的设计思想原理分析可以从“观察者”和“被观察者”两个方面着手。由于回调种类多但结构差异不大所以这里单以一元依赖中的thenApply 为例不再枚举全部回调类型。如下图所示 1.4.1.1 被观察者
每 个 CompletableFuture 都 可 以 被 看 作 一 个 被 观 察 者 其 内 部 有 一 个 Completion 类型的链表成员变量 stack用来存储注册到其中的所有观察者。当被观察者执行完成后会弹栈 stack 属性依次通知注册到其中的观察者。上面例子中步骤 fn2 就是作为观察者被封装在 UniApply 中。被观察者 CF 中的 result 属性用来存储返回结果数据。这里可能是一次RPC 调用的返回值也可能是任意对象在上面的例子中对应步骤 fn1 的执行结果
1.4.1.2 观察者
CompletableFuture 支持很多回调方法例如 thenAccept、thenApply、exceptionally 等这些方法接收一个函数类型的参数 f生成一个 Completion 类型的对象即观察者并将入参函数 f 赋值给 Completion 的成员变量 fn然后检查当前CF 是否已处于完成状态即 result ! null如果已完成直接触发 fn否则将观察者 Completion 加入到 CF 的观察者链 stack 中再次尝试触发如果被观察者未执行完则其执行完毕之后通知触发。 观察者中的 dep 属性指向其对应的 CompletableFuture在上面的例子中dep 指向 CF2。观察者中的 src 属性指向其依赖的 CompletableFuture在上面的例子中src 指向 CF1。观察者 Completion 中的 fn 属性用来存储具体的等待被回调的函数。这里需要注意的是不同的回调方法thenAccept、thenApply、exceptionally 等接收的函数类型也不同即 fn 的类型有很多种在上面的例子中 fn 指向 fn2。 1.4.2 整体流程
1.4.2.1 一元依赖
这里仍然以 thenApply 为例来说明一元依赖的流程
将观察者 Completion 注册到 CF1此时 CF1 将 Completion 压栈。当 CF1 的操作运行完成时会将结果赋值给 CF1 中的 result 属性。依次弹栈通知观察者尝试运行。 初步流程设计如上图所示这里有几个关于注册与通知的并发问题大家可以思考下 Q1在观察者注册之前如果 CF 已经执行完成并且已经发出通知那么这时观察者由于错过了通知是不是将永远不会被触发呢 A1不会。在注册时检查依赖的 CF 是否已经完成。如果未完成即 result null则将观察者入栈如果已完成result ! null则直接触发观察者操作。 Q2在”入栈“前会有”result null“的判断这两个操作为非原子操作CompletableFufure 的实现也没有对两个操作进行加锁完成时间在这两个操作之间观察者仍然得不到通知是不是仍然无法触发 A2不会。入栈之后再次检查 CF 是否完成如果完成则触发。 Q3当依赖多个 CF 时观察者会被压入所有依赖的 CF 的栈中每个 CF 完成的时候都会进行那么会不会导致一个操作被多次执行呢 如下图所示即当 CF1、CF2 同时完成时如何避免 CF3 被多次触发。 A3CompletableFuture 的实现是这样解决该问题的观察者在执行之前会先通过 CAS 操作设置一个状态位将 status 由 0 改为 1。如果观察者已经执行过了那么 CAS 操作将会失败取消执行 1.4.2.2thenApply源码分析 public U CompletableFutureU thenApply(Function? super T,? extends U fn) {return uniApplyStage(null, fn);}private V CompletableFutureV uniApplyStage(Executor e, Function? super T,? extends V f) {if (f null) throw new NullPointerException();//1.创建一个新的CompletableFuture对象CompletableFutureV d new CompletableFutureV();if (e ! null || !d.uniApply(this, f, null)) {//2.构建UniApplye代表线程池 d 代表新的CompletableFuture this 代表当前f 代表方法 这个时候 UniApply 内部的所有的引用都处于为null的状态UniApplyT,V c new UniApplyT,V(e, d, this, f);//3. c其实就是Completion对象被push到栈中push(c);//4. 尝试执行cc.tryFire(SYNC);}// 5. 这个d会一直返回到调用thenApply的地方后续的链式调用会作用在这个d上面return d;}SuppressWarnings(serial)static final class UniApplyT,V extends UniCompletionT,V {Function? super T,? extends V fn;UniApply(Executor executor, CompletableFutureV dep,CompletableFutureT src,Function? super T,? extends V fn) {//2.1 向上执行super(executor, dep, src); this.fn fn;}}abstract static class UniCompletionT,V extends Completion {Executor executor; // executor to use (null if none)CompletableFutureV dep; // the dependent to completeCompletableFutureT src; // source for actionUniCompletion(Executor executor, CompletableFutureV dep,CompletableFutureT src) {//2.2 dep就是新创建的d src就是当前的thisthis.executor executor; this.dep dep; this.src src;}}
关于执行第2步的时候构建的对象如下图 src和dep都是空的CompletableFuturenext为Null这里我们会发现所有的都是继承Completion对象最终所有都是构建都可以理解为Completion对象
关于执行第3步的时候构建的UniApply对象的内容完成压栈的操作将CompletableFuture的stack属性指向Completion对象
接下来看第4步操作尝试执行Completion SuppressWarnings(serial)static final class UniApplyT,V extends UniCompletionT,V {Function? super T,? extends V fn;UniApply(Executor executor, CompletableFutureV dep,CompletableFutureT src,Function? super T,? extends V fn) {super(executor, dep, src); this.fn fn;}final CompletableFutureV tryFire(int mode) {//4.1 d新创建的 a(也是c中的src) 就是原来的CompletableFutureV d; CompletableFutureT a;//4.2 如果uniApply执行成功则会进到下面的postFire调用否则返回null 如果返回null,就要等待以后的主动complete来再次触发if ((d dep) null ||!d.uniApply(a src, fn, mode 0 ? null : this))return null;//4.5 tryFire成功后会把以下几个属性设为null表面此Completion已经完成任务变成dead状态dep null; src null; fn null;//4.6 出栈return d.postFire(a, mode);}}final S boolean uniApply(CompletableFutureS a,Function? super S,? extends T f,UniApplyS,T c) {Object r; Throwable x;//4.3 如果a(也是c中的src)没有准备完成那result是空这里就会直接返回falseif (a null || (r a.result) null || f null)return false;tryComplete: if (result null) {if (r instanceof AltResult) {if ((x ((AltResult)r).ex) ! null) {completeThrowable(x, r);break tryComplete;}r null;}try {if (c ! null !c.claim())return false;SuppressWarnings(unchecked) S s (S) r;//4.4 如果r不为空则会作为f的输入参数f的输出则成为当前CompletableFuture的完成值completeValue(f.apply(s));} catch (Throwable ex) {completeThrowable(ex);}}return true;}
第5步返回d, 这个d会返回到调用thenApply的地方后续的链式调用会作用在这个d上面接下来我们可以看到base对象就是我们构建好的第一个链
这里我们可以猜测后续的执行thenApply的方法也就是执行完成test1的第二行代码,生成的结构如下图 当我们的代码执行到test1的第3行的时候也就是complete方法该方法也就是为了解决我们执行tryFire执行失败后动作源码如下:
public boolean complete(T value) {boolean triggered completeValue(value);postComplete();return triggered;}final void postComplete() {//1. this表示当前的CompletableFuture, 也就是我们baseCompletableFuture? f this; Completion h;//2. 判断stack是否为空 或者如果f的栈为空且不是this则重置while ((h f.stack) ! null ||(f ! this (h (f this).stack) ! null)) {CompletableFuture? d; Completion t;//3. CAS出栈if (f.casStack(h, t h.next)) {if (t ! null) { //4.出栈的h不是最后一个元素最后一个元素直接执行7即可if (f ! this) {//5. 如果f不是this将刚出栈的h, 入this的栈顶//我猜测这个地方大家会有迷惑pushStack(h);continue;}h.next null; //6. detach}f (d h.tryFire(NESTED)) null ? this : d; //7.调用tryFire}}}
1.4.2.3 二元依赖
我们以 thenCombine 为例来说明二元依赖 thenCombine 操作表示依赖两个 CompletableFuture。其观察者实现类为 BiApply 如上图所示BiApply 通过 src 和 snd 两个属性关联被依赖的两个 CFfn 属性的类型为 BiFunction。与单个依赖不同的是在依赖的 CF 未完成的情况下thenCombine 会尝试将 BiApply 压入这两个被依赖的 CF 的栈中每个被依赖的 CF 完成时都会尝试触发观察者 BiApplyBiApply 会检查两个依赖是否都完成如果完成则开始执行。这里为了解决重复触发的问题同样用的是上一章节提到的 CAS 操作执行时会先通过 CAS 设置状态位避免重复触发。
1.4.2.4 多元依赖
依赖多个 CompletableFuture 的回调方法包括 allOf、anyOf区别在于 allOf观察者实现类为 BiRelay需要所有被依赖的 CF 完成后才会执行回调而 anyOf 观 察者实现类为 OrRelay任意一个被依赖的 CF 完成后就会触发。二者的实现方式都是将多个被依赖的 CF 构建成一棵平衡二叉树执行结果层层通知直到根节点触发回调监听 1.4.3 小结
本章节为 CompletableFuture 实现原理的科普旨在尝试不粘贴源码而通过结构图、流程图以及搭配文字描述把 CompletableFuture 的实现原理讲述清楚。把晦 涩的源码翻译为“整体流程”章节的流程图并且将并发处理的逻辑融入便于大家理解
2.实践总结
2.1线程阻塞问题
2.1.1 代码执行在哪个线程上
要合理治理线程资源最基本的前提条件就是要在写代码时清楚地知道每一行代码都将执行在哪个线程上。下面我们看一下 CompletableFuture 的执行线程情况。CompletableFuture 实现了 CompletionStage 接口通过丰富的回调方法支持各种组合操作每种组合场景都有同步和异步两种方法。 同步方法即不带 Async 后缀的方法有两种情况。
● 如果注册时被依赖的操作已经执行完成则直接由当前线程执行。
● 如果注册时被依赖的操作还未执行完则由回调线程执行
异步方法即带 Async 后缀的方法可以选择是否传递线程池参数 Executor 运行在指定线程池中当不传递 Executor 时会使用 ForkJoinPool 中的共用线程池CommonPoolCommonPool 的大小是 CPU 核数 -1如果是 IO 密集的应用线程数可能成为瓶颈。
ExecutorService threadPool1 new ThreadPoolExecutor(10, 10, 0L,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue(100));
CompletableFutureString future1 CompletableFuture.supplyAsync(() - {System.out.println(“supplyAsync 执行线程” Thread.currentThread().
getName());// 业务操作return“”;
}, threadPool1);
// 此时如果 future1 中的业务操作已经执行完毕并返回则该 thenApply 直接由当前
main 线程执行否则将会由执行以上业务操作的 threadPool1 中的线程执行。
future1.thenApply(value - {System.out.println(“thenApply 执行线程” Thread.currentThread().
getName());return value “1”;
});
// 使用 ForkJoinPool 中的共用线程池 CommonPool
future1.thenApplyAsync(value - {
//do somethingreturn value “1”;});
// 使用指定线程池
future1.thenApplyAsync(value - {
//do somethingreturn value “1”;
}, threadPool1);
2.1.2 异步回调要传线程池
前面提到异步回调方法可以选择是否传递线程池参数 Executor这里我们建议强制传线程池且根据实际情况做线程池隔离。当不传递线程池时会使用 ForkJoinPool 中的公共线程池 CommonPool这里所有调用将共用该线程池核心线程数 处理器数量 -1单核核心线程数为 1所有异步回调都会共用该 CommonPool核心与非核心业务都竞争同一个池中的线程很容易成为系统瓶颈。手动传递线程池参数可以更方便的调节参数并且可以给不同的业务分配不同的线程池以求资源隔离减少不同业务之间的相互干扰。
2.1.3 线程池循环引用会导致死锁
public Object doGet() {ExecutorService threadPool1 new ThreadPoolExecutor(10, 10, 0L,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue(100));CompletableFuture cf1 CompletableFuture.supplyAsync(() - {//do sthreturn CompletableFuture.supplyAsync(() - {System.out.println(“child”);return“child”;}, threadPool1).join();// 子任务}, threadPool1);return cf1.join();
}
如上代码块所示doGet 方法第三行通过 supplyAsync 向 threadPool1 请求线程并且内部子任务又向 threadPool1 请求线程。threadPool1 大小为 10当同一时刻有 10 个请求到达则 threadPool1 被打满子任务请求线程时进入阻塞队列排队但是父任务的完成又依赖于子任务这时由于子任务得不到线程父任务无法完成。
主线程执行 cf1.join() 进入阻塞状态并且永远无法恢复。为了修复该问题需要将父任务与子任务做线程池隔离两个任务请求不同的线程池避免循环依赖导致的阻塞
2.1.4 异步 RPC 调用注意不要阻塞 IO 线程池
服务异步化后很多步骤都会依赖于异步 RPC 调用的结果这时需要特别注意一点如果是使用基于 NIO比如 Netty的异步 RPC则返回结果是由 IO 线程负责设置的即回调方法由 IO 线程触发CompletableFuture 同步回调如 thenApply、thenAccept 等无 Async 后缀的方法如果依赖的异步 RPC 调用的返回结果那么 这些同步回调将运行在 IO 线程上而整个服务只有一个 IO 线程池这时需要保证同步回调中不能有阻塞等耗时过长的逻辑否则在这些逻辑执行完成前IO 线程将一直被占用影响整个服务的响应。
2.2 其他
2.2.1 异常处理
由于异步执行的任务在其他线程上执行而异常信息存储在线程栈中因此当前线程除非阻塞等待返回结果否则无法通过 try\catch 捕获异常。CompletableFuture提供了异常捕获回调 exceptionally相当于同步调用中的try\catch。使用方法如下所示
Autowired
private WmOrderAdditionInfoThriftService
wmOrderAdditionInfoThriftService;// 内部接口
public CompletableFutureInteger getCancelTypeAsync(long orderId) {CompletableFutureWmOrderOpRemarkResult
remarkResultFuture wmOrderAdditionInfoThriftService.
findOrderCancelledRemarkByOrderIdAsync(orderId);// 业务方法内部会发起异步rpc 调用return remarkResultFuture.exceptionally(err - {// 通过 exceptionally 捕获异常打印日志并返回默认值log.error(“WmOrderRemarkService.getCancelTypeAsync Exception
orderId{}”, orderId, err);
return 0;});
}
3.异步化收益
通过异步化改造API 系统的性能得到明显提升与改造前对比的收益
如下
● 核心接口吞吐量大幅提升其中订单轮询接口改造前 TP99 为 754ms改造后降为 408ms。
● 服务器数量减少 1/3。 参考文献 《2022年美团技术年货-合辑》