自定义异步线程池和异步任务异常捕获器
/**
* ProjectName xyc-commerce-springcloud
* 自定义异步任务线程池,异步任务异常捕获处理器
* @author xieyucan
* <br>CreateDate 2022/9/8 16:40
*/
@Slf4j
@EnableAsync //开启spring异步任务支持
@Configuration
public class AsyncPoolConfig implements AsyncConfigurer {
/**
* 将自定义的线程池注入到spring容器中
* @return
*/
@Bean
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
//阻塞队列
executor.setQueueCapacity(20);
//保存线程存活时间
executor.setKeepAliveSeconds(60);
//线程名称前缀
executor.setThreadNamePrefix("XieYuCan-Async-");
//等待所有任务执行结束在关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
//设置线程池任务等待时间,确保任务可以关闭
executor.setAwaitTerminationSeconds(60);
//定义饱和策略 当线程池线程数量到达max 阻塞队列也满了,此时触发拒绝策略
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy()
);
//初始化线程池 初始化core线程
executor.initialize();
return executor;
}
/**
* 指定系统中异步任务出现异常时使用到的处理器
* @return
*/
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncExceptionHandler();
}
/**
* 异步任务异常捕获处理器
*/
@SuppressWarnings("all")
class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler{
@Override
public void handleUncaughtException(Throwable throwable,
Method method, Object... objects) {
throwable.printStackTrace();
log.error("Async error:[{}],method:[{}],param:[{}]",
throwable.getMessage(),method.getName(), JSON.toJSONString(objects));
//TODO 发送邮件或者短信,做进一步的报警处理
}
}
}
定义异步任务接口
/**
* ProjectName xyc-commerce-springcloud
* 异步服务接口定义
* @author xieyucan
* <br>CreateDate 2022/9/8 16:30
*/
public interface IAsyncService {
/**
* 异步将商品信息保存下来
* @param goodsInfos
* @param taskId
*/
void asyncImportGoods(List<GoodsInfo> goodsInfos,String taskId);
}
定义异步接口实现类
/**
* ProjectName xyc-commerce-springcloud
* 异步任务接口实现
* @author xieyucan
* <br>CreateDate 2022/9/8 17:08
*/
@SuppressWarnings("all")
@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
public class AsyncServiceImpl implements IAsyncService{
private final EcommerceGoodsDao ecommerceGoodsDao;
private final StringRedisTemplate stringRedisTemplate;
public AsyncServiceImpl(EcommerceGoodsDao ecommerceGoodsDao,
StringRedisTemplate stringRedisTemplate) {
this.ecommerceGoodsDao = ecommerceGoodsDao;
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 异步任务需要加上注解,并且指定使用的线程池
* 异步任务处理两个事情:
* 1:将商品信息保存到数据库表
* 2:更新商品缓存
* @param goodsInfos
* @param taskId
*/
@Async("getAsyncExecutor")
@Override
public void asyncImportGoods(List<GoodsInfo> goodsInfos, String taskId) {
log.info("async task running taskId:[{}]",taskId);
/**
* 开始计时
*/
StopWatch watch = StopWatch.createStarted();
//1 如果goodsInfo 中存在重复的商品,不保存,直接返回,记录错误日志
//请求数据是否符合的标记
boolean isIllegal=false;
//将商品信息字段 join 在一起,用来判断是否重复
Set<String> goodsJointInfos=new HashSet<>(goodsInfos.size());
//过滤出来的,可以入库的商品信息(规则按照自己的业务需求自定义即可)
List<GoodsInfo> filteredGoodsInfo=new ArrayList<>(goodsInfos.size());
//走一遍循环,过滤非法参数去判定当前请求是否合法
for(GoodsInfo goodsInfo:goodsInfos)
{
//基本条件不满足,直接过滤掉
if(goodsInfo.getPrice()<=0||goodsInfo.getSupply()<=0)
{
log.info("goods info is invalid:[{]]", JSON.toJSONString(goodsInfo));
continue;
}
//组合每一个商品信息
String jointInfo=String.format(
"%s,%s,%s",
goodsInfo.getGoodsCategory(),
goodsInfo.getBrandCategory(),
goodsInfo.getGoodsName());
if(goodsJointInfos.contains(jointInfo))
{
isIllegal=true;
}
//加入到两个容器中
goodsJointInfos.add(jointInfo);
filteredGoodsInfo.add(goodsInfo);
}
//如果存在重复商品或者没有需要入库的商品,直接打印日志返回
if(isIllegal|| CollectionUtils.isEmpty(filteredGoodsInfo))
{
watch.stop();
log.warn("import nothing:[{}]",JSON.toJSONString(filteredGoodsInfo));
log.info("check and import goods done:[{}ms]",
watch.getTime(TimeUnit.MILLISECONDS));
return;
}
List<EcommerceGoods> ecommerceGoods=filteredGoodsInfo
.stream()
.map(EcommerceGoods::to)
.collect(Collectors.toList());
List<EcommerceGoods> targetGoods=new ArrayList<>(ecommerceGoods.size());
//保存goodsInfo之前判断下是否存在重复商品
ecommerceGoods.forEach(g->{
if(null!=ecommerceGoodsDao
.findFirst1ByGoodsCategoryAndBrandCategoryAndGoodsName(
g.getGoodsCategory(),g.getBrandCategory(),g.getGoodsName())
.orElse(null)) {
return;
}
targetGoods.add(g);
});
//商品信息入库
List<EcommerceGoods> saveGoods= IterableUtils.toList(
ecommerceGoodsDao.saveAll(targetGoods)
);
// 将入库的商品同步到redis中
saveNewGoodsInfo(saveGoods);
log.info("save goodsInfo to db and redis:[{}]",saveGoods.size());
watch.stop();
log.info("check and import goods success:[{}]",
watch.getTime(TimeUnit.MILLISECONDS));
}
/**
* 将保存到数据表中的的数据缓存到redis中
* dict:key-><id,simpleGoodsInfo(json)>
* @param saveGoods
*/
private void saveNewGoodsInfo(List<EcommerceGoods> saveGoods)
{
//由于redis是内存存储,所以只存储简单信息
List<SimpleGoodsInfo> simpleGoodsInfos = saveGoods
.stream()
.map(EcommerceGoods::toSimple)
.collect(Collectors.toList());
Map<String,String> id2JsonObject=new HashMap<>(simpleGoodsInfos.size());
simpleGoodsInfos.forEach(
g->id2JsonObject.put(g.getId().toString(),JSON.toJSONString(g)));
//保存到redis中
stringRedisTemplate.opsForHash()
.putAll(GoodsConstant.ECOMMERCE_GOODS_DICT_KEY,id2JsonObject);
}
}
异步任务执行信息类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AsyncTaskInfo {
/**
* 异步任务id
*/
private String taskId;
/**
* 异步任务执行状态
*/
private AsyncTaskStatusEnum status;
/**
* 异步任务执行开始时间
*/
private Date startTime;
/**
* 异步任务执行结束时间
*/
private Date endTime;
/**
* 异步任务总耗时
*/
private String totalTime;
}
异步任务执行器(提交任务,并且查看任务执行情况)
/**
* ProjectName xyc-commerce-springcloud
* 异步任务执行管理器,对异步任务进行包装管理,
* 记录并塞入异步任务执行信息
* @author xieyucan
* <br>CreateDate 2022/9/9 9:26
*/
@Slf4j
@Component
public class AsyncTaskManager {
/**
* 异步任务执行的容器(简单记录)
*/
private final Map<String, AsyncTaskInfo> taskContainer=new HashMap<>(16);
private final IAsyncService asyncService;
public AsyncTaskManager(IAsyncService asyncService) {
this.asyncService = asyncService;
}
/**
* 初始化异步任务
* @return
*/
public AsyncTaskInfo initTask()
{
AsyncTaskInfo taskInfo = new AsyncTaskInfo();
//设置一个唯一的异步id
taskInfo.setTaskId(UUID.randomUUID().toString());
taskInfo.setStatus(AsyncTaskStatusEnum.STARTED);
taskInfo.setStartTime(new Date());
//初始化的时候就要将异步任务执行信息放入到存储容器中
taskContainer.put(taskInfo.getTaskId(),taskInfo);
return taskInfo;
}
/**
* 提交异步任务
* @param goodsInfos
* @return
*/
public AsyncTaskInfo submit(List<GoodsInfo> goodsInfos)
{
//初始化一个异步任务的监控信息
AsyncTaskInfo taskInfo = initTask();
asyncService.asyncImportGoods(goodsInfos,taskInfo.getTaskId());
return taskInfo;
}
/**
* 设置异步任务执行状态信息
* @param taskInfo
*/
public void setTaskInfo(AsyncTaskInfo taskInfo)
{
taskContainer.put(taskInfo.getTaskId(),taskInfo);
}
/**
* 获取异步任务执行状态信息
* @param taskId
* @return
*/
public AsyncTaskInfo getTaskInfo(String taskId)
{
return taskContainer.get(taskId);
}
}
异步任务监控器(利用切面,补齐异步任务执行信息类信息内容)
/**
* ProjectName xyc-commerce-springcloud
* 异步任务执行监控切面
* @author xieyucan
* <br>CreateDate 2022/9/9 9:52
*/
@Slf4j
@Aspect
@Component
public class AsyncTaskMonitor {
private final AsyncTaskManager asyncTaskManager;
public AsyncTaskMonitor(AsyncTaskManager asyncTaskManager) {
this.asyncTaskManager = asyncTaskManager;
}
/**
* 异步任务执行的环绕切面
* 环绕切面让我们可以在方法执行之前之后做一些额外的操作
* @param joinPoint
* @return
*/
@Around("execution(* com.imooc.ecommerce.service.async.AsyncServiceImpl.*(..))")
public Object taskHandler(ProceedingJoinPoint joinPoint)
{
//获取taskId调用异步任务传递的第二个参数
String taskId=joinPoint.getArgs()[1].toString();
//获取任务信息,在提交任务的时候,已经放入容器中了
AsyncTaskInfo taskInfo = asyncTaskManager.getTaskInfo(taskId);
log.info("AsyncTaskMonitor is monitor async task:[{}]",taskId);
//设置为运行状态,并且重新放入容器中
taskInfo.setStatus(AsyncTaskStatusEnum.RUNNING);
asyncTaskManager.setTaskInfo(taskInfo);
AsyncTaskStatusEnum status;
Object result;
try {
result=joinPoint.proceed();
status=AsyncTaskStatusEnum.SUCCESS;
}catch (Throwable ex){
result=null;
status=AsyncTaskStatusEnum.FAILED;
log.error("AsyncTaskMonitor:async task [{}] is failed,error info: [{}]",taskId,ex.getMessage(),ex);
}
//设置异步任务其他信息,再次重新放入容器中
taskInfo.setEndTime(new Date());
taskInfo.setStatus(status);
taskInfo.setTotalTime(String.valueOf(taskInfo.getEndTime().getTime()
-taskInfo.getStartTime().getTime()));
asyncTaskManager.setTaskInfo(taskInfo);
return result;
}
}
最终controller调用
/**
* ProjectName xyc-commerce-springcloud
* 异步任务服务对外提供api接口
* @author xieyucan
* <br>CreateDate 2022/9/12 10:44
*/
@Api(tags = "商品异步入库服务")
@Slf4j
@RestController
@RequestMapping("/async-goods")
public class AsyncGoodsController {
//异步任务执行器
private final AsyncTaskManager asyncTaskManager;
public AsyncGoodsController(AsyncTaskManager asyncTaskManager) {
this.asyncTaskManager = asyncTaskManager;
}
@ApiOperation(value = "导入商品",notes = "导入商品到商品表",httpMethod = "POST")
@PostMapping("/import-goods")
public AsyncTaskInfo importGoods(@RequestBody List<GoodsInfo> goodsInfos)
{
return asyncTaskManager.submit(goodsInfos);
}
@ApiOperation(value = "查询状态",notes = "查询异步任务的执行状态",httpMethod = "GET")
@GetMapping("/task-info")
//taskId 为报错时,反馈给我们之前我们传递哪一个任务出错了,我们可以及时查看
public AsyncTaskInfo getTaskInfo(@RequestParam String taskId)
{
return asyncTaskManager.getTaskInfo(taskId);
}
}