利用 CompletableFuture的applyToEither 优雅的实现快速失败

问题背景

最近遇到一个场景,简化之后即:一个接口要实现对多个服务的聚合查询,多个服务之间不存在依赖,如何能保证在指定的时间内返回查询到的数据,其它的请求按超时丢弃处理

问题分析

这种聚合的查询接口,性能和稳定性要求都比较高,并且要能够支持返回特定时间内部分查询结果的功能,也就意味着不能阻塞等待所有请求都结束才返回。

这个问题有个很关键的前提,即这些服务之间不存在依赖,根据这个信息可以想到这些服务请求都可以单起一个线程发送,所以可以使用线程池,而且都是网络请求,所以可以使用异步发送避免阻塞,那如何解决超时这个异步请求还没有反回的话就失败呢?想了一下其实可以借助CompletableFuture.applyToEither来实现,下面来看下这个场景的一个解决示例

示例演示

package demo.assemble;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.apache.commons.lang3.concurrent.BasicThreadFactory;

class Test {
    private final static int AVALIABLE_PROCESSORS = Runtime.getRuntime().availableProcessors();
    // 自定义一个线程池
    private final static ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(
            AVALIABLE_PROCESSORS,
            AVALIABLE_PROCESSORS * 2,
            5,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(5),
            new ThreadPoolExecutor.CallerRunsPolicy()
    );

    /**
     * 定时器线程,专门给fastFail使用
     */
    private static final ScheduledExecutorService SCHEDULED_EXECUTOR = Executors.newScheduledThreadPool(
            1,
            new BasicThreadFactory.Builder().namingPattern("ScheduleFuture-Thread-%d")
                    .daemon(true) // 这里需要设置成守护线程
                    .build());

    /**
     * 构造一个指定时间后抛异常的future
     */
    private static <T> CompletableFuture<T> fastFail(long timeout, TimeUnit timeUnit) {
        final CompletableFuture<T> future = new CompletableFuture<>();// 承载超时异常的future
        SCHEDULED_EXECUTOR.schedule(() -> {
            final TimeoutException ex = new TimeoutException("超时啦... " + timeout);
            return future.completeExceptionally(ex);
        }, timeout, timeUnit);
        return future;
    }

    /**
     * 用于构造模拟查询三方服务的future
     */
    private static CompletableFuture<String> query3rdService(String serviceName) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                long t = ThreadLocalRandom.current().nextInt(5);
                System.out.println(Thread.currentThread().getName() + " 请求服务 " + serviceName + "需要时间" + t + "秒");
                TimeUnit.SECONDS.sleep(t);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return serviceName;
        }, POOL_EXECUTOR)
                // applyToEitherAsync: 最快返回输出的线程结果作为下一次任务的输入
                // 所以这里理解: 模拟三方服务为futureA, fastFail 为 futureB, 则这个方法会返回 futureA 和 futureB 更快的那一个的执行结果去执行下一跳任务
                // futureB 是必然的一个结果: timeout(这里是2s)时间之后抛一个超时异常
                // 则: futureA 如果在 timeout 之前返回,则 applyToEitherAsync 会返回 futureA 的值
                // 否则: futureA 如果在 timeout 之后返回,则 applyToEitherAsync 会返回 futureB 的值. 这里是超时异常, 返回 error
                .applyToEitherAsync(fastFail(2000, TimeUnit.MILLISECONDS), Function.identity()).exceptionally(e -> "error");
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 模拟6个请求三方服务的future
        List<CompletableFuture<String>> futureList = IntStream.range(0, 6).mapToObj(i -> query3rdService("Service" + i)).collect(Collectors.toList());

        // 并发执行
        futureList.forEach(CompletableFuture::join);

        // 查看执行结果
        for (CompletableFuture<String> future : futureList) {
            System.out.println(future.get());
        }
    }

}

该示例每次执行的结果都不一样,其中一个为下面,可以看到 service3 和 service5 时间超过了2s,所以抛出异常打印了error,其它的都正常返回了

pool-1-thread-1 请求服务 Service0需要时间2秒
pool-1-thread-2 请求服务 Service1需要时间2秒
pool-1-thread-3 请求服务 Service2需要时间0秒
pool-1-thread-4 请求服务 Service3需要时间4秒
pool-1-thread-5 请求服务 Service4需要时间1秒
pool-1-thread-6 请求服务 Service5需要时间3秒
Service0
Service1
Service2
error
Service4
error
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值