工作中常用异步并行任务

我在平时做东西的时候,总能发现一些这样的问题

需求点:业务上常常有这样一个需求:从多个数据源取得,合并成一个结果。

这个操作,假设有3个数据源,同步处理,需要queryData1,queryData2,queryData3。执行时间会是3个时间之和。一般的异步异步设计方案为:起一个业务的线程池,并发执行业务,然后一个守护的线程等各个业务结束(时间为业务执行最长的时间),获取所有数据,这样明显执行时间会小于3个业务时间之和。而是用了执行最长的业务时间,加上守护线程的消耗。那么这时候怎么解决呢?这里就记录下我平时解决的两种方式!

Java8新特性CompletableFuture

现在java8提供了一个很好的CompletableFuture工具

package com.maoxs.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**
* 并行获取各个数据源的数据合并成一个数据组合
*/
@Service
@Slf4j
public class AsyncTest1 {
private String getProductBaseInfo(String productId) {
log.info("获取基本信息");
return productId + "商品基础信息";
}

private String getProductDetailInfo(String productId) {
log.info("获取详情信息");
return productId + "商品详情信息";
}

private String getProductSkuInfo(String productId) {
log.info(" 获取sku信息");
return productId + "商品sku信息";
}

public String getAllInfoByProductId(String productId) {
//取得一个商品的所有信息(基础、详情、sku)
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> getProductBaseInfo(productId));
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> getProductDetailInfo(productId));
CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> getProductSkuInfo(productId));
//等待三个数据源都返回后,再组装数据。这里会有一个线程阻塞
CompletableFuture.allOf(f1, f2, f3).join();
try {
String baseInfo = f1.get();
String detailInfo = f2.get();
String skuInfo = f3.get();
return baseInfo + "" + detailInfo + skuInfo;
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return null;
}
}

创建一个测试用例

@Test
public void contextLoads() {
String allInfoByProductId = asyncTest1.getAllInfoByProductId("1001");
System.out.println(allInfoByProductId);
}

查看下日志

2018-12-02 22:06:36.679 INFO 18544 --- [onPool-worker-1] com.maoxs.service.AsyncTest1: 获取基本信息
2018-12-02 22:06:36.679 INFO 18544 --- [onPool-worker-1] com.maoxs.service.AsyncTest1: 获取sku信息
2018-12-02 22:06:36.679 INFO 18544 --- [onPool-worker-2] com.maoxs.service.AsyncTest1: 获取详情信息
1001商品基础信息1001商品详情信息1001商品sku信息

可以看到子线程是同时执行了三个方法,allOf是等待所有任务完成,接触阻塞,获取各个数据源的数据。

对于上面的例子,使用了默认的线程池,线程数为cpu核数-1。这个并不能很好地利用资源。下面为线程数计算的公式:

服务器端最佳线程数量= ( (线程等待时间+线程cpu时间) /  线程cpu时间)  * cpu数量

那就改进一下将executor线程池暴露出来,方便配置线程数和做一些其他处理。

ExecutorService executor = Executors.newFixedThreadPool(100);
public String getAllInfoByProductId(String productId) {
//取得一个商品的所有信息(基础、详情、sku)
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> getProductBaseInfo(productId),executor);
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> getProductDetailInfo(productId),executor);
CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> getProductSkuInfo(productId),executor);
//等待三个数据源都返回后,再组装数据。这里会有一个线程阻塞
try {
String baseInfo = f1.get();
String detailInfo = f2.get();
String skuInfo = f3.get();
return baseInfo + "" + detailInfo + skuInfo;
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return null;
}

Spring @Async

在spring 3.x之后,就已经内置了@Async来完美解决这个问题,本文将介绍在springboot中如何使用@Async。

使用

首先呢建一个异步任务的类

package com.maoxs.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;

import java.util.concurrent.Future;
/**
* 并行获取各个数据源的数据合并成一个数据组合
**/
@Component
@Slf4j
@EnableAsync
public class AsyncTask {
@Async
public Future<String> getProductBaseInfo(String productId) {
log.info("获取基本信息");
Future<String> future;
future = new AsyncResult<>(productId + "商品基础信息");
return future;
}

@Async
public Future<String> getProductDetailInfo(String productId) {
log.info("获取详情信息");
Future<String> future;
future = new AsyncResult<>(productId + "商品详情信息");
return future;
}

@Async
public Future<String> getProductSkuInfo(String productId) {
log.info(" 获取sku信息");
Future<String> future;
future = new AsyncResult<>(productId + "商品sku信息");
return future;
}

}

这里要说明下@EnableAsync 这个注解的作用是表示开启异步,为什么要建一个任务类呢?

不得不提到 Spring在调用注解方法的时候是生成一个代理类,由代理类去执行! 而在同一个类中,方法调用是在类体内执行的,spring无法截获这个方法调用。

然后呢怎么使用呢

package com.maoxs.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

@Service
public class AsyncTest2 {

@Autowired
private AsyncTask asyncTask;

public String getAllInfoByProductId(String productId) {
//取得一个商品的所有信息(基础、详情、sku)
Future<String> f1 = asyncTask.getProductBaseInfo(productId);
Future<String> f2 = asyncTask.getProductDetailInfo(productId);
Future<String> f3 = asyncTask.getProductSkuInfo(productId);
try {
String baseInfo = f1.get();
String detailInfo = f2.get();
String skuInfo = f3.get();
return baseInfo + "" + detailInfo + skuInfo;
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return null;
}

}

然后同样的来一个测试类

@Test
public void contextLoads() {
String allInfoByProductId = asyncTest2.getAllInfoByProductId("1001");
System.out.println(allInfoByProductId);
}

这里是日志输出,可以看到三个子线程是同时去执行了。

2018-12-02 22:08:36.709  INFO 14124 --- [         task-1] com.maoxs.service.AsyncTask   : 获取基本信息
2018-12-02 22:08:36.709 INFO 14124 --- [ task-3] com.maoxs.service.AsyncTask : 获取sku信息
2018-12-02 22:08:36.709 INFO 14124 --- [ task-2] com.maoxs.service.AsyncTask : 获取详情信息
1001商品基础信息1001商品详情信息1001商品sku信息

自定义线程池

这里呢异步调用使用的是系统默认的线程池可以通过自定义线程池来达资源的最大的利用

这时只需定义一个bean

@Bean("FulinTask")
public Executor myTaskAsyncPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("FulinExecutor-");
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}

怎么使用呢?只需要在注解上加入bean名字即可

@Async(value = "FulinTask")

调配默认线程池

如果我们想使用默认的线程池,但是只是想修改默认线程池的配置,那怎么做了,此时我们需要实现AsyncConfigurer类即可

package com.maoxs.conf;

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;


@Slf4j
@Configuration
public class AsyncTaskExecutePool implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("FulinExecutor-");
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
// 异步任务中异常处理
return (arg0, arg1, arg2) -> {
log.error("=========" + arg0.getMessage() + "=========", arg0);
log.error("exception method:" + arg1.getName());
};
}
}