问题背景
最近遇到一个场景,简化之后即:一个接口要实现对多个服务的聚合查询,多个服务之间不存在依赖,如何能保证在指定的时间内返回查询到的数据,其它的请求按超时丢弃处理
问题分析
这种聚合的查询接口,性能和稳定性要求都比较高,并且要能够支持返回特定时间内部分查询结果的功能,也就意味着不能阻塞等待所有请求都结束才返回。
这个问题有个很关键的前提,即这些服务之间不存在依赖,根据这个信息可以想到这些服务请求都可以单起一个线程发送,所以可以使用线程池,而且都是网络请求,所以可以使用异步发送避免阻塞,那如何解决超时这个异步请求还没有反回的话就失败呢?想了一下其实可以借助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