目录
一、自定义异步任务线程池,异步任务异常捕获处理器。
二、异步任务枚举。
三、异步服务接口。
四、接口实现类。
五、异步任务执行管理器。
六、异步任务执行切面。
七、调用异步任务、暴露接口。
/**
* 自定义异步任务线程池, 异步任务异常捕获处理器
*/
@Slf4j
@EnableAsync // 开启 Spring 异步任务支持
@Configuration
public class AsyncPoolConfig implements AsyncConfigurer {
/**
* 将自定义的线程池注入到 Spring 容器中
* */
@Bean
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(20);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("Qinyi-Async-"); // 这个非常重要
// 等待所有任务结果候再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
// 定义拒绝策略
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 初始化线程池, 初始化 core 线程
executor.initialize();
return executor;
}
/**
* 指定系统中的异步任务在出现异常时使用到的处理器
* */
@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 发送邮件或者是短信, 做进一步的报警处理
}
}
}
/**
* 异步任务状态枚举
* */
@Getter
@AllArgsConstructor
public enum AsyncTaskStatusEnum {
STARTED(0, "已经启动"),
RUNNING(1, "正在运行"),
SUCCESS(2, "执行成功"),
FAILED(3, "执行失败"),
;
/** 执行状态编码 */
private final int state;
/** 执行状态描述 */
private final String stateInfo;
}
/**
* 异步服务接口定义
* */
public interface IAsyncService {
/**
* 异步将商品信息保存下来
* */
void asyncImportGoods(List goodsInfos, String taskId);
/**
* 异步服务接口实现
* */
@Slf4j
@Service
@Transactional
public class AsyncServiceImpl implements IAsyncService {
private final EcommerceGoodsDao ecommerceGoodsDao;
private final StringRedisTemplate redisTemplate;
public AsyncServiceImpl(EcommerceGoodsDao ecommerceGoodsDao,
StringRedisTemplate redisTemplate) {
this.ecommerceGoodsDao = ecommerceGoodsDao;
this.redisTemplate = redisTemplate;
}
/**
* 异步任务需要加上注解, 并指定使用的线程池
* 异步任务处理两件事:
* 1. 将商品信息保存到数据表
* 2. 更新商品缓存
* */
@Async("getAsyncExecutor")
@Override
public void asyncImportGoods(List goodsInfos, String taskId) {
log.info("async task running taskId: [{}]", taskId);
StopWatch watch = StopWatch.createStarted();
// 1. 如果是 goodsInfo 中存在重复的商品, 不保存; 直接返回, 记录错误日志
// 请求数据是否合法的标记
boolean isIllegal = false;
// 将商品信息字段 joint 在一起, 用来判断是否存在重复
Set goodsJointInfos = new HashSet<>(goodsInfos.size());
// 过滤出来的, 可以入库的商品信息(规则按照自己的业务需求自定义即可)
List filteredGoodsInfo = new ArrayList<>(goodsInfos.size());
// 走一遍循环, 过滤非法参数与判定当前请求是否合法
for (GoodsInfo goods : goodsInfos) {
// 基本条件不满足的, 直接过滤器
if (goods.getPrice() <= 0 || goods.getSupply() <= 0) {
log.info("goods info is invalid: [{}]", JSON.toJSONString(goods));
continue;
}
// 组合商品信息
String jointInfo = String.format(
"%s,%s,%s",
goods.getGoodsCategory(), goods.getBrandCategory(),
goods.getGoodsName()
);
if (goodsJointInfos.contains(jointInfo)) {
isIllegal = true;
}
// 加入到两个容器中
goodsJointInfos.add(jointInfo);
filteredGoodsInfo.add(goods);
}
// 如果存在重复商品或者是没有需要入库的商品, 直接打印日志返回
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 = filteredGoodsInfo.stream()
.map(EcommerceGoods::to)
.collect(Collectors.toList());
List targetGoods = new ArrayList<>(ecommerceGoods.size());
// 2. 保存 goodsInfo 之前先判断下是否存在重复商品
ecommerceGoods.forEach(g -> {
// limit 1
if (null != ecommerceGoodsDao
.findFirst1ByGoodsCategoryAndBrandCategoryAndGoodsName(
g.getGoodsCategory(), g.getBrandCategory(),
g.getGoodsName()
).orElse(null)) {
return;
}
targetGoods.add(g);
});
// 商品信息入库
List savedGoods = IterableUtils.toList(
ecommerceGoodsDao.saveAll(targetGoods)
);
// 将入库商品信息同步到 Redis 中
saveNewGoodsInfoToRedis(savedGoods);
log.info("save goods info to db and redis: [{}]", savedGoods.size());
watch.stop();
log.info("check and import goods success: [{}ms]",
watch.getTime(TimeUnit.MILLISECONDS));
}
/**
* 将保存到数据表中的数据缓存到 Redis 中
* dict: key ->
* */
private void saveNewGoodsInfoToRedis(List savedGoods) {
// 由于 Redis 是内存存储, 只存储简单商品信息
List simpleGoodsInfos = savedGoods.stream()
.map(EcommerceGoods::toSimple)
.collect(Collectors.toList());
Map id2JsonObject = new HashMap<>(simpleGoodsInfos.size());
simpleGoodsInfos.forEach(
g -> id2JsonObject.put(g.getId().toString(), JSON.toJSONString(g))
);
// 保存到 Redis 中
redisTemplate.opsForHash().putAll(
GoodsConstant.ECOMMERCE_GOODS_DICT_KEY,
id2JsonObject
);
}
}
/**
* 异步任务执行管理器
* 对异步任务进行包装管理, 记录并塞入异步任务执行信息
* */
@Slf4j
@Component
public class AsyncTaskManager {
/** 异步任务执行信息容器 */
private final Map taskContainer =
new HashMap<>(16);
private final IAsyncService asyncService;
public AsyncTaskManager(IAsyncService asyncService) {
this.asyncService = asyncService;
}
/**
* 初始化异步任务
* */
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;
}
/**
* 提交异步任务
* */
public AsyncTaskInfo submit(List goodsInfos) {
// 初始化一个异步任务的监控信息
AsyncTaskInfo taskInfo = initTask();
asyncService.asyncImportGoods(goodsInfos, taskInfo.getTaskId());
return taskInfo;
}
/**
* 设置异步任务执行状态信息
* */
public void setTaskInfo(AsyncTaskInfo taskInfo) {
taskContainer.put(taskInfo.getTaskId(), taskInfo);
}
/**
* 获取异步任务执行状态信息
* */
public AsyncTaskInfo getTaskInfo(String taskId) {
return taskContainer.get(taskId);
}
}
/**
* 异步任务执行监控切面
*/
@Slf4j
@Aspect
@Component
public class AsyncTaskMonitor {
/** 注入异步任务管理器 */
private final AsyncTaskManager asyncTaskManager;
public AsyncTaskMonitor(AsyncTaskManager asyncTaskManager) {
this.asyncTaskManager = asyncTaskManager;
}
/**
* 异步任务执行的环绕切面
* 环绕切面让我们可以在方法执行之前和执行之后做一些 "额外" 的操作
* */
@Around("execution(* com.imooc.ecommerce.service.async.AsyncServiceImpl.*(..))")
public Object taskHandle(ProceedingJoinPoint proceedingJoinPoint) {
// 获取 taskId, 调用异步任务传入的第二个参数
String taskId = proceedingJoinPoint.getArgs()[1].toString();
// 获取任务信息, 在提交任务的时候就已经放入到容器中了
AsyncTaskInfo taskInfo = asyncTaskManager.getTaskInfo(taskId);
log.info("AsyncTaskMonitor is monitoring async task: [{}]", taskId);
taskInfo.setStatus(AsyncTaskStatusEnum.RUNNING);
asyncTaskManager.setTaskInfo(taskInfo); // 设置为运行状态, 并重新放入容器
AsyncTaskStatusEnum status;
Object result;
try {
// 执行异步任务
result = proceedingJoinPoint.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;
}
}
/**
* 异步任务服务对外提供的
* */
@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 goodsInfos) {
return asyncTaskManager.submit(goodsInfos);
}
@ApiOperation(value = "查询状态", notes = "查询异步任务的执行状态", httpMethod = "GET")
@GetMapping("/task-info")
public AsyncTaskInfo getTaskInfo(@RequestParam String taskId) {
return asyncTaskManager.getTaskInfo(taskId);
}
}