Java 异步调用实践

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 本文介绍了线上业务中的一些异步调用实践经验,包含 IO 模型介绍、CompletableFuture 的基本使用、RPC 异步调用、异步 HTTP 客户端 Spring WebClient 的使用等。
本文介绍了线上业务中的一些异步调用实践经验,包含 IO 模型介绍、CompletableFuture 的基本使用、RPC 异步调用、异步 HTTP 客户端 Spring WebClient 的使用等。RPC 使用 前文介绍的手写 RPC 框架,该框架支持异步调用。

本文要点:

  • 为什么需要异步调用
  • CompletableFuture 基本使用
  • RPC 异步调用
  • HTTP 异步调用
  • 编排 CompletableFuture 提高吞吐量

为什么异步

BIO 模型

首先我们先回顾一下 BIO 模型:

BIO 模型

当用户进程调用了recvfrom 这个系统调用,kernel 就开始了 IO 的第一个阶段:准备数据。对于 network io 来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候 kernel 就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当 kernel 一直等到数据准备好了,它就会将数据从 kernel 中拷贝到用户内存,然后 kernel 返回结果,用户进程才解除 block 的状态,重新运行起来。所以,Blocking IO 的特点就是在 IO 执行的两个阶段都被 block 了。

同步调用

同步调用

在同步调用的场景下,依次请求多个接口,耗时长、性能差,接口响应时长 T > T1+T2+T3+……+Tn。

减少同步等待

一般这个时候为了减少同步等待时间,会使用线程池来同时处理多个任务,接口的响应时间就是 MAX(T1,T2,T3):

减少同步等待

大概代码如下:

Future<String> future = executorService.submit(() -> {
  Thread.sleep(2000);
  return "hello world";
});
while (true) {
  if (future.isDone()) {
    System.out.println(future.get());
    break;
  }
}

同步模型中使用线程池确实能实现异步调用的效果,也能压缩同步等待的时间,但是也有一些缺陷:

  • CPU 资源大量浪费在阻塞等待上,导致 CPU 资源利用率低。
  • 为了增加并发度,会引入更多额外的线程池,随着 CPU 调度线程数的增加,会导致更严重的资源争用,上下文切换占用 CPU 资源。
  • 线程池中的线程都是阻塞的,硬件资源无法充分利用,系统吞吐量容易达到瓶颈。

NIO 模型

为了解决 BIO 中的缺陷,引入 NIO 模型:

NIO 模型

当用户进程发出 read 操作时,如果 kernel 中的数据还没有准备好,那么它并不会 block 用户进程,而是立刻返回一个 error。从用户进程角度讲 ,它发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦 kernel 中的数据准备好了,并且又再次收到了用户进程的 system call,那么它马上就将数据拷贝到了用户内存,然后返回。所以,用户进程其实是需要不断的主动询问 kernel 数据好了没有。

异步优化思路

我们知道了 NIO 的调用方式比 BIO 好,那我们怎么能在业务编码中使用到 NIO 呢?自己动手将 BIO 替换成 NIO 肯定不现实,已有组件支持 NIO 的可以直接使用,不支持的继续使用自定义线程池。

  • 通过 RPC NIO 异步调用、 HTTP 异步调用的方式降低线程数,从而降低调度(上下文切换)开销。
  • 没有原生支持 NIO 异步调用的继续使用线程池。
  • 引入 CompletableFuture 对业务流程进行编排,降低依赖之间的阻塞。

简述CompletableFuture

CompletableFuture 是 java.util.concurrent 库在 java 8 中新增的主要工具,同传统的 Future 相比,其支持流式计算、函数式编程、完成通知、自定义异常处理等很多新的特性。

常用 API 举例

supplyAsync
CompletableFuture<String> future = CompletableFuture.supplyAsync(()->{    
  try{
    Thread.sleep(1000L);
    return "hello world";
  } catch (Exception e){
    return "failed";
  }
});
System.out.println(future.join());
// output
hello world

开启异步任务,到另一个线程执行。

complete
CompletableFuture<String> future1 = new CompletableFuture<>();
future.complete("hello world");              //异步线程执行
future.whenComplete((res, throwable) -> {
  System.out.println(res);
});
System.out.println(future1.join());
CompletableFuture<String> future2 = new CompletableFuture<>();
future.completeExceptionally(new Throwable("failed")); //异步线程执行
System.out.println(future2.join());
// output
hello world
hello world
  
Exception in thread "main" 
java.util.concurrent.CompletionException: 
java.lang.Throwable: failed

complete 正常完成该 CompletableFuture。

completeExceptionally 异常完成该 CompletableFuture。

thenApply
String original = "Message";
CompletableFuture<String> cf = 
    CompletableFuture.completedFuture(original).thenApply(String::toUpperCase);
System.out.println(cf.join());
// output
MESSAGE

任务后置处理。

图示:

thenApply

thenCombine
CompletableFuture<String> cf = 
    CompletableFuture.completedFuture("Message").thenApply(String::toUpperCase);
CompletableFuture<String> cf1 = 
    CompletableFuture.completedFuture("Message").thenApply(String::toLowerCase);
CompletableFuture<String> allCf = cf.thenCombine(cf1, (s1, s2) -> s1 + s2);
System.out.println(allCf.join());
// output
MSGmsg

合并任务,两个任务同时执行,结果由合并函数 BiFunction 返回。

图示:

thenCombine

allOf
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Message1");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Message2");
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> "Message3");
CompletableFuture<String> future = 
    CompletableFuture.allOf(future1, future2, future3).thenApply(v -> {
  String join1 = future1.join();
  String join2 = future2.join();
  String join3 = future3.join();
  return join1 + join2 + join3;});
System.out.println(future.join());
// output
Msg1Msg2Msg3

allOf 会阻塞等待所有异步线程任务结束。

allOf 里的 join 并不会阻塞,传给 thenApply 的函数是在 future1, future2, future3 全部完成时,才会执行 。

图示:

allOf

CF 执行线程

下面有两个小demo,可以先试着想想输出的结果:

String original = "Message";
CompletableFuture cf = CompletableFuture.supplyAsync(() -> {
  System.out.println("supplyAsync thread: " + Thread.currentThread().getName());
  return original;
}).thenApply(r -> {
  System.out.println("thenApply thread: " + Thread.currentThread().getName());
  return r;
});
System.out.println(cf.join());
// output
supplyAsync thread: ForkJoinPool.commonPool-worker-1
thenApply thread: main
Message
String original = "Message";
CompletableFuture cf = CompletableFuture.supplyAsync(() -> {
  System.out.println("supplyAsync thread: " + Thread.currentThread().getName());
  try {
    Thread.sleep(100);
  } catch (InterruptedException e) {
    throw new RuntimeException(e);
  }
  return original;
}).thenApply(r -> {
  System.out.println("thenApply thread: " + Thread.currentThread().getName());
  return r;
});
System.out.println(cf.join());
// output
supplyAsync thread: ForkJoinPool.commonPool-worker-1
thenApply thread: ForkJoinPool.commonPool-worker-1
Message

先看结论:

  • 执行 complete 的线程会执行当前调用链上的所有CF。
  • 如果 CF 提前 complete,后续 CF 由初始线程执行。

异步任务里没有 sleep 的时候,异步任务很快就会完成,意味着 JVM 执行到 thenApply 的时候,前置 CF 已经提前完成所以后续的 CF 会被 main 线程执行。

异步任务里有 sleep 的时候, JVM 执行到 thenApply 时,前置 CF 还没有完成,前置 CF complete 的线程会执行所有后续的 CF。

CF 嵌套join

ExecutorService executorService = Executors.newFixedThreadPool(2);
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
  Thread.sleep(3000);
  return 1;
}, executorService);
CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> {
  Thread.sleep(3000);
  return 2;
}, executorService);
Integer join1 = cf1.thenApply((cf1Val) -> {
  System.out.println("cf1 start value:" + cf1Val);
  Integer cf2Val = cf2.join();
  System.out.println("cf2 end value:" + cf2Val);
  return 3;
}).join();
//output
cf1 start value:1
cf2 end value:2

代码很简单,有一个线程数为 2 的线程池,cf1、cf2 都使用这个线程执行异步任务,特别的是在 cf1.thenApply 中会调用 cf2.join(),当线程数是2的时候可以顺利输出。

ExecutorService executorService = Executors.newFixedThreadPool(1);
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
  Thread.sleep(3000);
  return 1;
}, executorService);
CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> {
  Thread.sleep(3000);
  return 2;
}, executorService);
    Integer join1 = cf1.thenApply((cf1Val) -> {
  System.out.println("cf1 start value:" + cf1Val);
  Integer cf2Val = cf2.join();
  System.out.println("cf2 end value:" + cf2Val);
  return 3;
}).join();
//output
cf1 start value:1

这时候我们将线程池的线程数调整为 1,这时只会输出 cf1 start value:1,然后就一直阻塞。

使用 jstack -l pid 查看线程状态,发现是 WAITING,等待的地方正是我们在代码里调用的cf2.join()

"pool-1-thread-1" #11 prio=5 os_prio=31 tid=0x00000001429f5000 nid=0xa903 waiting on condition
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x000000076ba5f7d0> (a java.util.concurrent.CompletableFuture$Signaller)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.CompletableFuture$Signaller.block(CompletableFuture.java:1707)
    at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3323)
    at java.util.concurrent.CompletableFuture.waitingGet(CompletableFuture.java:1742)
    at java.util.concurrent.CompletableFuture.join(CompletableFuture.java:1947)
    at com.ppphuang.demo.threadPool.ExecutorsTest.lambda$main$2(ThreadPoolExecutorsTest.java:34)

原因是我们在唯一一个线程中调用 cf2.join(),阻塞等待 cf2 完成,但是 cf2 需要等待 cf1 完成之后才有空闲线程去执行。这就类似于你右手正拿着一个水杯,然后等待右手拿水壶倒满水,这是不可能完成的。所以尽量不要嵌套join,不注意隔离线程池的话很容易造成’死锁‘(线程阻塞)。

CF 常用 API

API 描述
supplyAsync 开启异步任务,到另一个线程执行,异步任务有返回值。
complete 完成任务。
completeExceptionally 异常结束任务。
thenCombine 合并任务,两个任务同时执行,结果由合并函数 BiFunction 返回。
thenApply 任务后置处理。
applyToEither 会取两个任务最先完成的任务,上个任务和这个任务同时进行,哪个先结束,先用哪个结果。
handle 后续处理。
whenComplete 完成后的处理。
allOf 等待所有异步线程任务结束。
join 获取返回值,没有complete的 CF 对象调用join时,会等待complete再返回,已经 complete的 CF 对象调用join时,会立刻返回结果。

优化过程

异步 RPC 客户端

我们手写的这个 RPC 框架支持异步调用,如果你想看具体的实现,可以在文末找到源码链接。异步调用之前会设置一个 CallBack 方法,异步调用时会直接返回 null,不会等待服务端返回接果,服务端返回结果之后会通过 RPC 客户端自带的线程池执行设置的 CallBack 方法。

RPC 异步调用图示:

RPC 异步调用

包装异步RPC Client

通过 AsyncExecutor 包装 RPC的客户端,AsyncExecutor 类中的 client 属性值为创建的某个 RPC 服务的异步客户端代理类,这个代理类在构造方法中创建并赋值给 client 属性。

类中的 async 方法接受 Function 类型的参数 function,可以通过 function.apply(client) 来通过 client 执行真正的 RPC 调用。

在 async 方法中实例化一个 CompletableFuture, 并将 CompletableFuture 作为异步回调的上下文设置到 RPC 的异步回调中,之后将该 CompletableFuture 返回给调用者。

public class AsyncExecutor<C> {

    private C client;

    public AsyncExecutor(ClientProxyFactory clientProxyFactory, Class<C> clazz, String group, String version) {
        this.client = clientProxyFactory.getProxy(clazz, group, version, true);
    }

    public <R> CompletableFuture<R> async(Function<C, R> function) {
        CompletableFuture<R> future = new CompletableFuture<>();
        ClientProxyFactory.setLocalAsyncContextAndAsyncReceiveHandler(future, CompletableFutureAsyncCallBack.instance());
        try {
            function.apply(client);
        } catch (Exception e) {
            future.completeExceptionally(e);
        }
        return future;
    }
}
异步回调类
public class CompletableFutureAsyncCallBack extends AsyncReceiveHandler {
    private static volatile CompletableFutureAsyncCallBack INSTANCE;

    private CompletableFutureAsyncCallBack() {
    }

    @Override
    public void callBack(Object context, Object result) {
        if (!(context instanceof CompletableFuture)) {
            throw new IllegalStateException("the context must be CompletableFuture");
        }
        CompletableFuture future = (CompletableFuture) context;
        if (result instanceof Throwable) {
            future.completeExceptionally((Throwable) result);
            return;
        }
        log.info("result:{}", result);
        future.complete(result);
    }
}

AsyncReceiveHandler 是 RPC 的异步回调抽象类,类中的 callBack、onException 抽象方法需要子类实现。

CompletableFutureAsyncCallBack 实现了这个 callBack 抽象方法,第一个参数是我们在包装异步 RPC Client 时设置的 CompletableFuture 上下文,第二个参数是 RPC 返回的结果。方法中判断 RPC 返回的结果是否异常,若异常通过 completeExceptionally 异常结束这个 CompletableFuture,若正常通过 complete 正常结束这个 CompletableFuture。

注册异步客户端Bean
@Component
public class AsyncExecutorConfig {
    @Autowired
    ClientProxyFactory clientProxyFactory;

    @Bean
    public AsyncExecutor<DemoService> demoServiceAsyncExecutor() {
        return new AsyncExecutor<>(clientProxyFactory, DemoService.class, "", "");
    }
}
异步 RPC 调用
@Autowired
AsyncExecutor<DemoService> demoServiceAsyncExecutor;

CompletableFuture<String> pppName = demoServiceAsyncExecutor.async(service -> service.hello("ppp"));

String name = pppName.join();

异步HTTP WebClient

WebClient 是从 Spring WebFlux 5.0 版本开始提供的一个非阻塞的基于响应式编程的进行 HTTP 请求的客户端工具。它的响应式编程的基于 Reactor 的。

WebClient VS RestTemplate

WebClient的优势在于:

  • 非阻塞响应式 IO,单位时间内有限资源下支持更高的并发量。
  • 支持使用 Java8 Lambda 表达式函数。
  • 支持同步、异步、Stream 流式传输。
WebClient 使用
public CompletableFuture<String> asyncHttp(String url) {
        WebClient localhostWebClient = WebClient.builder().baseUrl("http://localhost:8080").build();
        Mono<HttpResult<String>> userMono = localhostWebClient.method(HttpMethod.GET).uri(url)
                .retrieve()
                .bodyToMono(new ParameterizedTypeReference<HttpResult<String>>() {})
                //异常处理 有onErrorReturn时doOnError不会触发,所以不需要后续在CompletableFuture中handle处理异常
                //如果不使用onErrorReturn,建议在后续CompletableFuture中handle处理异常
                .onErrorReturn(new HttpResult<>(201, "default", "default hello"))
                //超时处理
                .timeout(Duration.ofSeconds(3))
                //返回值过滤
                .filter(httpResult -> httpResult.code == 200)
                //默认值
                .defaultIfEmpty(new HttpResult<>(201, "defaultIfEmpty", "defaultIfEmpty hello"))
                //失败重试
                .retryWhen(Retry.backoff(1, Duration.ofSeconds(1)));
        CompletableFuture<HttpResult<String>> stringCompletableFuture = WebClientFutureFactory.getCompletableFuture(userMono);
        return stringCompletableFuture.thenApply(HttpResult::getData);
    }
WebClient 整合 CF

WebClientFutureFactory.getCompletableFuture 方法会把 WebClient 返回的结果组装成 CompletableFuture ,使用的是 Mono 类的 doOnError 和 subscribe 方法,当正常返回时通过 subscribe 来调用 completableFuture.complete,当异常时通过 doOnError 来调用 completableFuture.completeExceptionally:

public class WebClientFutureFactory {
    public static <T> CompletableFuture<T> getCompletableFuture(Mono<T> mono) {
        CompletableFuture<T> completableFuture = new CompletableFuture<>();
        mono.doOnError(throwable -> {
            completableFuture.completeExceptionally(throwable);
            log.error("mono.doOnError throwable:{}", throwable.getMessage());
        }).subscribe(result -> {
            completableFuture.complete(result);
            log.debug("mono.subscribe execute thread: {}", Thread.currentThread().getName());
        });
        return completableFuture;
    }
}

WebClient 对同一服务的多次调用:

public Flux<User> fetchUsers(List<Integer> userIds) {
    return Flux.fromIterable(userIds)
        .parallel()
        .flatMap(this::getUser)
        .ordered((u1, u2) -> u2.id() - u1.id());
}

对返回相同类型的不同服务进行多次调用:

public Flux<User> fetchUserAndOtherUser(int id) {
    return Flux.merge(getUser(id), getOtherUser(id))
        .parallel()
        .runOn(Schedulers.elastic())
        .ordered((u1, u2) -> u2.id() - u1.id());
}

对不同类型的不同服务的多次调用:

public Mono fetchUserAndItem(int userId, int itemId) {
    Mono<User> user = getUser(userId).subscribeOn(Schedulers.elastic());
    Mono<Item> item = getItem(itemId).subscribeOn(Schedulers.elastic());
 
    return Mono.zip(user, item, UserWithItem::new);
}

异步数据库调用

使用 CompletableFuture.supplyAsync 执行异步任务时,必须指定成自己的线程池,否则 CompletableFuture 会使用默认的线程池 ForkJoinPool,默认线程池数量为 cpus - 1:

WebClient<Boolean> dbFuture = CompletableFuture.supplyAsync(() -> getDb(id), ThreadPoolConfig.ASYNC_TASK_EXECUTOR);

编排 CF

构造了所有需要异步执行的 CompletableFuture 之后,使用 allOf 方法阻塞等待所有的 CompletableFuture 结果,allOf 响应之后可以通过 join 获取各个 CompletableFuture 的响应接口,这里的 join 是会立刻返回的,不会阻塞:

//RPC 的 CompletableFuture
CompletableFuture<String> pppName = demoServiceAsyncExecutor.async(service -> service.hello("ppp"));

//RPC 的 CompletableFuture
CompletableFuture<String> huangName = demoServiceAsyncExecutor.async(service -> service.hello("huang"));

//DB 操作的 CompletableFuture
WebClient<Boolean> dbFuture = CompletableFuture.supplyAsync(() -> getDb(id), ThreadPoolConfig.ASYNC_TASK_EXECUTOR);

//allOf 方法阻塞等待所有的 CompletableFuture 结果     
return CompletableFuture.allOf(pppName, huangName, dbFuture)
              //组装结果返回
        .thenApply(r -> pppName.join() && huangName.join() && dbFuture.join()).join();

超时处理

java9 中 CompletableFuture 才有超时处理,使用方法如下:

CompletableFuture.supplyAsync(() -> 6 / 3).orTimeout(1, TimeUnit.SECONDS);

java8 中需要配合 ScheduledExecutorService + applyToEither:

public class TimeoutUtils {
    private static final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1);

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(scheduledExecutor::shutdownNow));
    }

    public static <T> CompletableFuture<T> timeout(CompletableFuture<T> cf, long timeout, TimeUnit unit) {
        CompletableFuture<T> result = new CompletableFuture<>();
        scheduledExecutor.schedule(() -> result.completeExceptionally(new TimeoutException()), timeout, unit);
        return cf.applyToEither(result, Function.identity());
    }
}

TimeoutUtils 类与有一个静态属性,值为初始化的一个 ScheduledExecutorService ,还有一个静态方法 timeout ,这个方法将传入的 cf 用 applyToEither 接口与一个调度计时的 CompletableFuture 组合,哪个 CompletableFuture 先执行完成,就返回哪个的结果。

具体使用如下:

CompletableFuture<Integer> future = demoServiceAsyncExecutor.async(service -> service.getAge(18));
CompletableFuture<Integer> futureWithTimeout = TimeoutUtils.timeout(future, 3, TimeUnit.SECONDS);
futureWithTimeout.join();

异常与默认值处理

CompletableFuture 中可以处理异常有下面三个 API :

public <V> CompletableFuture<V> handle(java.util.function.BiFunction<? super T, Throwable, ? extends V> fn)

handle 接口不论 CompletableFuture 执行成功还是异常都会被处罚,handle 接受一个 BiFunction 参数,BiFunction 中的第一个参数为 CompletableFuture 的结果,另一个参数为 CompletableFuture 执行过程中的异常,Handle可以返回任意类型的值。可以给 handle 传入自定义函数,根据结果跟执行异常返回最终数据。

public CompletableFuture<T> whenComplete(java.util.function.BiConsumer<? super T, ? super Throwable> action)

whenComplete 接口与 handle 类似,whenComplete 接受一个 BiConsumer 参数,BiConsumer 中的第一个参数为 CompletableFuture 的结果,另一个参数为 CompletableFuture 执行过程中的异常,但是没有返回值。

public CompletableFuture<T> exceptionally(java.util.function.Function<Throwable, ? extends T> fn)

exceptionally 接口只有在执行异常的时候才会被触发,接受一个 Function 参会, Function 只有一个参数为 CompletableFuture 执行过程中的异常,可以有一个任意返回值。

下表是三个接口的对比:

handle() whenComplete() exceptionly()
访问成功 Yes Yes No
访问失败 Yes Yes Yes
能从失败中恢复 Yes No Yes
能转换结果从T 到 U Yes No No
成功时触发 Yes Yes No
失败时触发 Yes Yes Yes
有异步版本 Yes Yes Yes

我们使用 handle 接口来处理异常与默认值,下面是封装的一个 handle 接口入参:

public class DefaultValueHandle<R> extends AbstractLogAction<R> implements BiFunction<R, Throwable, R> {
      public DefaultValueHandle(boolean isNullToDefault, R defaultValue, String methodName, Object... args) {
        super(methodName, args);
        this.defaultValue = defaultValue;
        this.isNullToDefault = isNullToDefault;
    }
      @Override
    public R apply(R result, Throwable throwable) {
        logResult(result, throwable);
        if (throwable != null) {
            return defaultValue;
        }
        if (result == null && isNullToDefault) {
            return defaultValue;
        }
        return result;
    }
}

这个类实现了 handle 接口需要的 BiFunction 类型,在构造方法中有四个参数 boolean isNullToDefault, R defaultValue, String methodName, Object... args 第一个参数是决定执行结果为空值时,是否将我们传进来的第二个参数作为默认值返回。当异常时也会将第二个参数作为默认返回值。最后两个参数一个是方法名称,一个是调用参数,可以给父类用作日志记录。

与 CompletableFuture 配合使用如下:

CompletableFuture<String> pppName = demoServiceAsyncExecutor.async(service -> service.hello("ppp"))
        .handle(new DefaultValueHandle<>(true, "name", "service.hello", "ppp"));

日志

封装了一个实现 BiConsumer 的 LogErrorAction 类,父类有个抽象类 AbstractLogAction 这个类就是简单使用 logReslut 方法记录日志,可以自己随意实现:

public class LogErrorAction<R> extends AbstractLogAction<R> implements BiConsumer<R, Throwable>{
  @Override
    public void accept(R result, Throwable throwable) {
        logResult(result, throwable);
    }
}

与 CompletableFuture 配合使用如下:

CompletableFuture<String> pppName = demoServiceAsyncExecutor.async(service -> service.hello("ppp"))
  .whenComplete(
        new LogErrorAction<>("hello", "ppp")
    );

优化效果

优化前接口平均响应耗时 350ms,优化后平均响应耗时 180ms,下降 49% 左右。

最佳实践

  • 禁止嵌套 join,避免“死锁”(线程阻塞)。
  • 多个 CompletableFuture 聚合时建议使用 allOf。
  • HTTP 使用无阻塞的 Spring webclient,避免自定义线程池线程阻塞。
  • 使用 RPC 或者 HTTP 异步调用生成的 CompletableFuture, 后续的 thenAppply,handle 等禁止耗时操作,避免阻塞异步框架线程池。
  • 禁止使用 CompletableFuture 的默认线程池,不同任务自定义线程池,不同级别业务线程池隔离,根据测试情况设置线程数,队列长度,拒绝策略。
  • 异步执行的操作都加上超时,CF 超时后不会终止线程中的超时任务,不设置超时可能导致线程长时间阻塞。
  • 建议使用异常、默认值、空值替换、错误日志等工具记录信息,方便排查问题。

示例代码

RPC 框架代码:https://github.com/PPPHUANG/rpc-spring-starter

异步调用demo:https://github.com/PPPHUANG/rpc-spring-starter-demo

总结

  • 为什么需要异步调用
  • CompletableFuture 基本使用
  • RPC 异步调用
  • HTTP 异步调用
  • 编排 CompletableFuture 提高吞吐量

公众号:DailyHappy 一位后端写码师,一位黑暗料理制造者。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
15天前
|
设计模式 安全 Java
Java编程中的单例模式:理解与实践
【10月更文挑战第31天】在Java的世界里,单例模式是一种优雅的解决方案,它确保一个类只有一个实例,并提供一个全局访问点。本文将深入探讨单例模式的实现方式、使用场景及其优缺点,同时提供代码示例以加深理解。无论你是Java新手还是有经验的开发者,掌握单例模式都将是你技能库中的宝贵财富。
20 2
|
11天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
8天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
12天前
|
SQL Java 数据库连接
从理论到实践:Hibernate与JPA在Java项目中的实际应用
本文介绍了Java持久层框架Hibernate和JPA的基本概念及其在具体项目中的应用。通过一个在线书店系统的实例,展示了如何使用@Entity注解定义实体类、通过Spring Data JPA定义仓库接口、在服务层调用方法进行数据库操作,以及使用JPQL编写自定义查询和管理事务。这些技术不仅简化了数据库操作,还显著提升了开发效率。
26 3
|
11天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
17天前
|
Java 程序员 数据库连接
Java中的异常处理:理解与实践
【10月更文挑战第29天】在Java编程的世界里,异常像是不请自来的客人,它们可能在任何时候闯入我们的程序宴会。了解如何妥善处理这些意外访客,不仅能够保持我们程序的优雅和稳健,还能确保它不会因为一个小小的失误而全盘崩溃。本文将通过浅显易懂的方式,带领读者深入异常处理的核心概念,并通过实际示例展现如何在Java代码中实现有效的异常管理策略。
|
21天前
|
缓存 Java 调度
Java中的多线程编程:从基础到实践
【10月更文挑战第24天】 本文旨在为读者提供一个关于Java多线程编程的全面指南。我们将从多线程的基本概念开始,逐步深入到Java中实现多线程的方法,包括继承Thread类、实现Runnable接口以及使用Executor框架。此外,我们还将探讨多线程编程中的常见问题和最佳实践,帮助读者在实际项目中更好地应用多线程技术。
23 3
|
23天前
|
监控 安全 Java
Java多线程编程的艺术与实践
【10月更文挑战第22天】 在现代软件开发中,多线程编程是一项不可或缺的技能。本文将深入探讨Java多线程编程的核心概念、常见问题以及最佳实践,帮助开发者掌握这一强大的工具。我们将从基础概念入手,逐步深入到高级主题,包括线程的创建与管理、同步机制、线程池的使用等。通过实际案例分析,本文旨在提供一种系统化的学习方法,使读者能够在实际项目中灵活运用多线程技术。
|
21天前
|
缓存 安全 Java
Java中的多线程编程:从基础到实践
【10月更文挑战第24天】 本文将深入探讨Java中的多线程编程,包括其基本原理、实现方式以及常见问题。我们将从简单的线程创建开始,逐步深入了解线程的生命周期、同步机制、并发工具类等高级主题。通过实际案例和代码示例,帮助读者掌握多线程编程的核心概念和技术,提高程序的性能和可靠性。
13 2
|
22天前
|
Java
Java中的多线程编程:从基础到实践
本文深入探讨Java多线程编程,首先介绍多线程的基本概念和重要性,接着详细讲解如何在Java中创建和管理线程,最后通过实例演示多线程的实际应用。文章旨在帮助读者理解多线程的核心原理,掌握基本的多线程操作,并能够在实际项目中灵活运用多线程技术。