Java Spring中如何让@Scheduled定时任务内的耗时逻辑异步执行?
Spring @Scheduled定时任务异步执行耗时FCM推送方案
问题背景
现有Spring应用配置了fixedDelay = 10000的定时拍卖启动任务,遍历待启动拍卖场次执行核心启动逻辑时,两段FCM推送代码耗时过长,会阻塞主任务遍历流程,导致整体任务执行延迟。需要将两段推送逻辑改为异步执行,不阻塞拍卖启动核心流程。
原任务实现代码如下:
@Scheduled(fixedDelay = 10000) public void startAuction() throws Exception { List<SchGoodsAuctionStartListRes> list = schedulerService.schGoodsAuctionStartList(); for (SchGoodsAuctionStartListRes item : list) { schedulerService.schGoodsAuctionStart(item); // 第一段耗时逻辑:给卖家推送拍卖启动通知 PushInfo pushInfo = pushMapper.pushGoodsSeller(item.getGoodsIdx()); pushInfo.setTitle("Start"); pushInfo.setBody("[" + pushInfo.getBrand() + "] started."); pushInfo.setPushGrp("001"); pushInfo.setPushCode("003"); fcmPushUtil.sendPush(pushInfo); // 第二段耗时逻辑:给所有竞拍用户推送拍卖启动通知 List<PushInfo> pushInfos = pushMapper.pushGoodsAuctionAll(item.getIdx()); for (PushInfo pushInfoItem : pushInfos) { pushInfoItem.setTitle("\uD83D\uDD14 open"); pushInfoItem.setBody("[" + pushInfo.getBrand() + "] started. \uD83D\uDC5C"); pushInfoItem.setPushGrp("002"); pushInfoItem.setPushCode("008"); fcmPushUtil.sendPush(pushInfoItem); } } }
实现方案
直接用Spring原生@Async注解实现异步,不需要手动编写线程提交逻辑,侵入性最低。
具体步骤
- 开启异步支持
在Spring Boot启动类或者任意配置类上添加@EnableAsync注解,打开Spring异步方法执行开关。 - 自定义FCM推送专用线程池
Spring默认异步线程池每次调用都会新建线程,高并发下容易耗尽服务器资源,强烈建议自定义独立线程池,隔离推送任务和其他业务线程。@Configuration public class AsyncPoolConfig { @Bean("fcmPushExecutor") public Executor fcmPushExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(200); executor.setThreadNamePrefix("fcm-push-task-"); // 队列满时由调用线程执行任务,避免消息丢失 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } } - 封装异步推送逻辑
把两段耗时推送逻辑抽到独立的Service类中,给方法加上@Async注解,指定使用刚才定义的推送专用线程池。
注意:异步方法必须放在独立的Bean中,不能和定时任务写在同一个类,否则异步不生效@Service public class AsyncPushTaskService { @Resource private PushMapper pushMapper; @Resource private FcmPushUtil fcmPushUtil; @Async("fcmPushExecutor") public void asyncSendAuctionStartPush(SchGoodsAuctionStartListRes auctionItem) { // 卖家推送逻辑 PushInfo sellerPush = pushMapper.pushGoodsSeller(auctionItem.getGoodsIdx()); sellerPush.setTitle("Start"); sellerPush.setBody("[" + sellerPush.getBrand() + "] started."); sellerPush.setPushGrp("001"); sellerPush.setPushCode("003"); fcmPushUtil.sendPush(sellerPush); // 竞拍用户推送逻辑 List<PushInfo> userPushList = pushMapper.pushGoodsAuctionAll(auctionItem.getIdx()); for (PushInfo userPush : userPushList) { userPush.setTitle("\uD83D\uDD14 open"); userPush.setBody("[" + sellerPush.getBrand() + "] started. \uD83D\uDC5C"); userPush.setPushGrp("002"); userPush.setPushCode("008"); fcmPushUtil.sendPush(userPush); } } } - 改造原定时任务
删除原任务中同步执行的推送代码,替换为调用异步Service的方法,核心拍卖启动逻辑执行完成后立刻触发异步推送,不需要等待推送执行完成,直接进入下一个场次的处理,完全不会阻塞主流程。@Resource private AsyncPushTaskService asyncPushTaskService; @Scheduled(fixedDelay = 10000) public void startAuction() throws Exception { List<SchGoodsAuctionStartListRes> list = schedulerService.schGoodsAuctionStartList(); for (SchGoodsAuctionStartListRes item : list) { // 核心启动逻辑同步执行,保证拍卖状态一致性 schedulerService.schGoodsAuctionStart(item); // 异步触发推送,不阻塞主循环 asyncPushTaskService.asyncSendAuctionStartPush(item); } }
踩坑提醒
- 不要在定时任务所在类直接定义异步方法然后自调用,Spring AOP代理会失效,异步配置完全不生效,还是会同步阻塞。
- 线程池参数不要直接抄示例,根据自身服务器配置、推送峰值QPS调整,核心线程数不要设置过高,避免并发请求FCM接口被限流。
- 异常处理、失败重试、推送日志统计这些逻辑直接写在异步方法里即可,不会影响定时任务主流程的执行效率。
- 如果业务允许少量推送丢失,可以把拒绝策略换成
DiscardPolicy,极端情况下直接丢弃溢出的推送任务,完全不会阻塞定时任务;如果不允许丢消息就保留示例里的CallerRunsPolicy。
内容的提问来源于stack exchange,提问作者Adam Lee
相关产品推荐
相关产品推荐

