在实际开发中,经常会遇到 “需要异步轮询等待文件生成,生成后立即发送” 的场景(比如报表图片生成、文件导出后推送)。本文基于 Spring 的
TaskScheduler 实现了一套生产级的异步轮询方案,包含重试机制、异常防护、资源管控等核心设计,分享给大家。一、场景背景
需求:
- 异步等待指定的 ReportRecord 记录生成(关联图片文件);
- 轮询检查文件是否生成,最多重试 20 次,每次间隔 30 秒;
- 图片生成后拼接 URL 并安全发送,防止发送过程中 Native 崩溃导致线程异常;
- 所有轮询任务使用独立线程池管控,保证应用优雅关闭。
二、核心实现代码
1. 第一步:配置专用的 TaskScheduler 线程池
首先创建独立的线程池配置类,避免轮询任务与其他任务抢占资源:
package com.push.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* 异步轮询任务调度器配置
* 为轮询任务提供独立线程池,避免资源竞争
*/
@Configuration
public class AsyncConfig {
/**
* 轮询任务专用调度器
* @return 定制化的 ThreadPoolTaskScheduler
*/
@Bean("pollingTaskScheduler")
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
// 核心线程数:根据并发轮询任务数调整,本例设置5
scheduler.setPoolSize(5);
// 线程名称前缀:便于日志排查问题
scheduler.setThreadNamePrefix("polling-task-");
// 应用关闭时等待任务执行完成
scheduler.setWaitForTasksToCompleteOnShutdown(true);
// 等待任务完成的超时时间:防止无限阻塞,最多等60秒
scheduler.setAwaitTerminationSeconds(60);
return scheduler;
}
}
2. 第二步:核心轮询与发送逻辑
封装轮询、URL 构建、安全发送等核心方法,重点处理异常和重试:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Component;
import java.io.File;
import java.time.Instant;
/**
* 图片异步获取与发送核心逻辑
*/
@Slf4j
@Component
public class ImageSendService {
// 注入自定义的轮询任务调度器
@Autowired
@Qualifier("pollingTaskScheduler")
private TaskScheduler taskScheduler;
// 依赖的Mapper和工具类(根据实际业务替换)
@Autowired
private ReportRecordMapper reportRecordMapper;
@Autowired
private GetJREPathHelper getJREPathHelper;
// 业务配置项(可抽离到配置文件)
private static final int MAX_RETRIES = 20; // 最大重试次数
private static final long RETRY_DELAY_MS = 30000; // 重试间隔(30秒)
private static final long MAX_FILE_SIZE = 20 * 1024 * 1024; // 图片最大限制20MB
private boolean testSwitch = false; // 测试环境开关
private int sendTimes = 1; // 发送次数(根据业务调整)
/**
* 异步触发图片获取与发送(入口方法)
* @param eqDto 业务DTO
* @param thematicKey 主题标识
*/
private void getImgAsync(RealAnalogEQDto eqDto, String thematicKey) {
// 提交第一个轮询任务,立即执行
taskScheduler.schedule(() -> pollForFile(eqDto, thematicKey, 1), Instant.now());
}
/**
* 核心轮询逻辑:检查文件是否生成,失败则重试
* @param eqDto 业务DTO
* @param thematicKey 主题标识
* @param attempt 当前重试次数
*/
private void pollForFile(RealAnalogEQDto eqDto, String thematicKey, int attempt) {
try {
// 查询图片关联的记录
ReportRecord record = reportRecordMapper.getThematic(eqDto.getId(), thematicKey);
if (record != null) {
// 1. 记录存在:拼接URL并发送
log.info("第 {} 次尝试:成功获取到 ReportRecord", attempt);
String imgUrl = buildImgUrl(record);
log.info("准备发送图片: {}", imgUrl);
safeSendPicture(imgUrl);
} else if (attempt < MAX_RETRIES) {
// 2. 记录不存在且未达最大重试次数:延迟重试
log.warn("第 {} 次尝试:文件未生成,{}秒后重试", attempt, RETRY_DELAY_MS / 1000);
taskScheduler.schedule(
() -> pollForFile(eqDto, thematicKey, attempt + 1),
Instant.now().plusMillis(RETRY_DELAY_MS)
);
} else {
// 3. 达到最大重试次数:终止任务
log.error("经过 {} 次重试,文件仍未生成,任务终止。", MAX_RETRIES);
}
} catch (Exception e) {
// 4. 执行异常:记录并重试(防止单次异常终止任务)
log.error("轮询过程发生异常", e);
if (attempt < MAX_RETRIES) {
taskScheduler.schedule(
() -> pollForFile(eqDto, thematicKey, attempt + 1),
Instant.now().plusMillis(RETRY_DELAY_MS)
);
}
}
}
/**
* 构建图片完整URL(抽离路径拼接逻辑,符合单一职责)
* @param record 图片关联记录
* @return 完整图片URL
*/
private String buildImgUrl(ReportRecord record) {
// 获取基础路径并格式化(统一分隔符为/)
String jrePath = getJREPathHelper.getJREPath()
.replace("/target", "")
.replaceAll("\\\\", "/");
// 拼接相对路径
String relativePath = "/" + record.getFilePath();
return jrePath + relativePath;
}
/**
* 安全发送图片:防OOM、防Native崩溃、全量异常捕获
* @param imgUrl 图片完整URL
*/
private void safeSendPicture(String imgUrl) {
try {
// 前置检查:防止超大文件导致OOM
File file = new File(imgUrl);
if (file.length() > MAX_FILE_SIZE) {
log.warn("图片过大,跳过发送: {} 大小: {}MB", imgUrl, file.length() / (1024 * 1024));
return;
}
// 正式发送图片
sendPicture(imgUrl, sendTimes);
log.info("【发送成功】文件大小: {} KB", file.length() / 1024);
// 测试环境额外处理
if (testSwitch) {
sendPictureTest(imgUrl, sendTimes);
}
} catch (Throwable t) { // 捕获Throwable(包含Error),防止Native崩溃
log.error("【严重】发送图片时发生致命错误(可能是Native内存问题): {}", imgUrl, t);
// 可选:添加告警、资源重启等兜底逻辑
}
}
// 以下为业务方法(示例,根据实际场景实现)
private void sendPicture(String imgUrl, int sendTimes) {
// 实际发送逻辑:如调用第三方推送接口、本地文件推送等
}
private void sendPictureTest(String imgUrl, int sendTimes) {
// 测试环境发送逻辑
}
}
三、关键设计思路与优化点
1. 线程池管控(生产级必备)
- 使用
ThreadPoolTaskScheduler而非默认调度器,独立线程池避免资源竞争; - 配置
waitForTasksToCompleteOnShutdown和awaitTerminationSeconds,保证应用关闭时任务优雅退出; - 线程名称前缀便于日志定位问题。
2. 重试机制设计
- 固定重试间隔(30 秒)+ 最大重试次数(20 次),总超时 10 分钟,避免无限轮询;
- 异常捕获后仍重试,防止数据库查询失败、网络抖动等临时问题导致任务终止;
- 轮询逻辑与业务逻辑解耦,重试仅针对 “文件未生成” 场景。
3. 安全防护措施
- 防 OOM:发送前检查文件大小,限制 20MB 以内;
- 防线程崩溃:
safeSendPicture捕获Throwable(包含Error),防止 JNI/Native 层崩溃导致整个调度线程死亡; - 路径格式化:统一文件路径分隔符为
/,兼容 Windows/Linux 系统。
4. 代码可读性优化
- 抽离
buildImgUrl方法,将路径拼接逻辑与轮询 / 发送逻辑解耦,符合单一职责原则; - 常量抽离(如
MAX_RETRIES),便于配置管理; - 日志分级(info/warn/error),便于问题排查。
四、使用注意事项
- 线程池大小调整:根据实际并发轮询任务数调整
setPoolSize,如果同时有 10 个轮询任务,建议设置核心线程数为 10; - 重试策略优化:可将固定间隔改为 “指数退避”(如 30s→60s→120s),减少服务器压力;
- 配置抽离:将
MAX_RETRIES、RETRY_DELAY_MS等常量抽离到application.yml,便于动态调整; - 监控告警:添加轮询失败、发送失败的告警(如钉钉 / 邮件通知),及时发现问题。
五、线程池(ThreadPoolTaskScheduler)是并发编程的核心载体
配置的
ThreadPoolTaskScheduler本质是 Spring 封装的线程池(底层基于 JDK 的ThreadPoolExecutor),这是并发编程中管理多线程的核心组件:setPoolSize(5):设置 5 个核心线程,意味着可以同时并发执行 5 个轮询任务(比如 5 个不同的eqDto对应的图片轮询);- 线程池会自动管理线程的创建、复用、销毁,避免手动创建线程的开销和风险(如线程泄露)。
六、异步任务调度实现 “并发执行”
getImgAsync方法通过taskScheduler.schedule()提交任务,当前线程不会阻塞,而是由线程池中的独立线程执行轮询逻辑;- 多个
getImgAsync调用会生成多个独立的轮询任务,这些任务会在线程池的不同线程中并发执行(比如同时轮询 3 个不同的图片文件)。
七、 轮询重试的 “异步并发” 特性
每次重试都是通过
taskScheduler.schedule()提交新的任务,而非单线程sleep等待:- 即使某个轮询任务在重试等待(30 秒),线程池中的其他线程仍可处理其他轮询任务;
- 单个轮询任务的多次重试,也会被调度到线程池的不同线程(或复用线程)执行,避免单线程阻塞。
八、与 “传统底层并发编程” 的区别
虽然属于并发编程,但和直接使用 JDK 底层 API(如
Thread、Runnable、Lock、CountDownLatch)的 “底层并发编程” 有明显区别:| 维度 | 我的代码(Spring 封装) | 传统底层并发编程 |
|---|---|---|
| 核心 API | Spring TaskScheduler/ 线程池 |
JDK Thread/Lock/Callable |
| 关注点 | 任务调度、业务逻辑 | 线程创建、锁机制、资源竞争 |
| 复杂度 | 低(Spring 封装了并发细节) | 高(需手动处理线程安全、死锁) |
| 适用场景 | 业务层异步任务、定时 / 轮询任务 | 框架层、底层组件开发 |
简单来说:
- 该代码是业务层的并发编程实践(Spring 屏蔽了底层并发的复杂细节);
- 无需手动处理
synchronized、Lock等线程安全问题(因为每个轮询任务处理的是独立的业务数据,无共享资源竞争); - 核心目标是 “提升任务执行的并发性和响应性”,而非 “解决底层线程安全问题”。
总结
这套方案基于 Spring
TaskScheduler 实现了生产级的异步轮询逻辑,核心亮点是:- 独立线程池管控,保证任务隔离和优雅退出;
- 完善的重试与异常防护机制,适配各种异常场景;
- 代码分层清晰,符合面向对象设计原则,便于维护和扩展。适用于文件生成、报表推送、异步回调等需要 “轮询等待 + 重试” 的业务场景,可直接复用或根据实际需求调整。
- 这套代码属于并发编程,核心特征是使用线程池管理多任务的异步调度与并发执行;
- 它是 Spring 生态下高易用性的并发编程实践,无需关注底层线程安全细节,适合业务层异步任务场景;
- 区别于底层并发编程(如手动创建线程、处理锁),更聚焦于 “任务调度” 和 “业务逻辑”,是生产环境中最常用的并发编程方式之一;
浙公网安备 33010602011771号