Spring Boot 4 后台任务(定时 + 异步)是系统的"隐形发动机"。本文将系统讲解
@Scheduled定时任务、@Async异步线程池、CompletableFuture 并行编排、任务异常处理,并给出分布式环境下防重复执行的实战方案,帮你写出既准又稳的后台任务。
一、为什么需要定时任务与异步线程池?
1️⃣ 典型后台任务场景
| 场景 | 例子 |
|---|---|
| 定时任务 | 每日报表、凌晨对账、缓存预热、日志清理 |
| 异步任务 | 发送短信 / 邮件、导出大文件、调用第三方接口 |
| 并行任务 | 批量查询多个接口后聚合结果 |
2️⃣ 不用线程池的代价
// ❌ 错误示例:每次 new Thread
new Thread(() -> doSomething()).start();
问题:
- 线程创建销毁开销大
- 无法控制并发数
- 容易耗尽系统资源
- 线程无法复用
✅ 正确姿势:统一交给线程池管理
二、Spring Boot 4 定时任务(@Scheduled)
1️⃣ 开启定时任务
@SpringBootApplication
@EnableScheduling
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
2️⃣ 基础用法(三种方式)
@Component
public class ScheduledTask {
// 1️⃣ 固定频率(每 5 秒)
@Scheduled(fixedRate = 5000)
public void fixedRateTask() {
System.out.println("fixedRate: " + LocalDateTime.now());
}
// 2️⃣ 固定延迟(上次结束 → 延迟 3 秒 → 下次开始)
@Scheduled(fixedDelay = 3000)
public void fixedDelayTask() {
System.out.println("fixedDelay: " + LocalDateTime.now());
}
// 3️⃣ 初始延迟 + 固定频率
@Scheduled(initialDelay = 10000, fixedRate = 5000)
public void initialDelayTask() {
System.out.println("initialDelay + fixedRate");
}
}
📌 fixedRate vs fixedDelay 区别:
fixedRate: |---5s---|---5s---|---5s---|
↑开始 ↑开始 ↑开始
fixedDelay: |---3s---| 延迟3s |---3s---|
↑结束 ↑开始 ↑结束
3️⃣ Cron 表达式(最常用 ⭐)
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点
public void dailyReport() {
System.out.println("生成每日报表");
}
Cron 格式(6 位,Spring 支持 7 位)
秒 分 时 日 月 星期 [年]
| 字段 | 允许值 | 允许通配 |
|---|---|---|
| 秒 | 0--59 | , - * / |
| 分 | 0--59 | , - * / |
| 时 | 0--23 | , - * / |
| 日 | 1--31 | , - * ? / L W |
| 月 | 1--12 | , - * / |
| 星期 | 1--7(1=周日) | , - * ? / L # |
常用 Cron 示例
| 需求 | 表达式 |
|---|---|
| 每 5 秒 | */5 * * * * ? |
| 每分钟 | 0 * * * * ? |
| 每小时 | 0 0 * * * ? |
| 每天凌晨 2 点 | 0 0 2 * * ? |
| 每周一 3 点 | 0 0 3 ? * MON |
| 每月 1 号 | 0 0 0 1 * ? |
| 工作日 9 点 | 0 0 9 ? * MON-FRI |
📌 在线 Cron 生成器推荐: cron.qqe2.com
4️⃣ 并行执行定时任务(关键!)
默认情况下,所有 @Scheduled 任务在同一个线程串行执行。
// ❌ 一个任务阻塞,其他任务全部卡住
@Scheduled(fixedRate = 5000)
public void taskA() throws InterruptedException {
Thread.sleep(10000); // 阻塞 10 秒
}
@Scheduled(fixedRate = 5000)
public void taskB() {
System.out.println("taskB"); // 会被 taskA 阻塞
}
✅ 解决方案:配置定时任务线程池
@Configuration
public class SchedulingConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 10 个线程
scheduler.setThreadNamePrefix("scheduled-");
scheduler.setAwaitTerminationSeconds(60);
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.initialize();
return scheduler;
}
}
✅ 效果:多个定时任务并行执行,互不干扰。
三、Spring Boot 4 异步线程池(@Async)
1️⃣ 开启异步支持
@SpringBootApplication
@EnableAsync
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
2️⃣ 基础用法
@Service
public class EmailService {
@Async
public CompletableFuture<Void> sendEmail(String to, String content) {
Thread.sleep(3000); // 模拟耗时
System.out.println("邮件发送完成: " + to);
return CompletableFuture.completedFuture(null);
}
}
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private EmailService emailService;
@GetMapping("/email")
public Result<?> send() {
emailService.sendEmail("test@test.com", "Hello");
return Result.success("邮件发送中...");
}
}
📌 效果:接口立即返回,邮件在后台异步发送。
3️⃣ 自定义异步线程池(生产必配 ⭐)
默认线程池:
- 核心线程数:8
- 队列:无界(可能 OOM)
✅ 自定义线程池:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 核心线程数
executor.setMaxPoolSize(50); // 最大线程数
executor.setQueueCapacity(200); // 队列容量
executor.setKeepAliveSeconds(60); // 空闲线程存活时间
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
指定使用线程池:
@Async("taskExecutor")
public CompletableFuture<Void> sendEmail(...) { ... }
📌 拒绝策略对比:
| 策略 | 行为 |
|---|---|
AbortPolicy(默认) |
抛异常 |
CallerRunsPolicy |
由调用线程执行(推荐) |
DiscardPolicy |
直接丢弃 |
DiscardOldestPolicy |
丢弃队列最老任务 |
4️⃣ 异步任务返回值
@Async
public CompletableFuture<Integer> calculate() {
Thread.sleep(2000);
return CompletableFuture.completedFuture(100);
}
// 调用
CompletableFuture<Integer> future = calculate();
Integer result = future.get(); // 阻塞获取
四、CompletableFuture 并行编排(高阶)
1️⃣ 并行执行多个任务
@Service
public class AggregateService {
@Async("taskExecutor")
public CompletableFuture<User> getUser() { ... }
@Async("taskExecutor")
public CompletableFuture<Order> getOrder() { ... }
@Async("taskExecutor")
public CompletableFuture<Score> getScore() { ... }
public Result<?> aggregate() throws Exception {
CompletableFuture<User> userFuture = getUser();
CompletableFuture<Order> orderFuture = getOrder();
CompletableFuture<Score> scoreFuture = getScore();
// 等待所有完成
CompletableFuture.allOf(userFuture, orderFuture, scoreFuture).join();
User user = userFuture.get();
Order order = orderFuture.get();
Score score = scoreFuture.get();
return Result.success(Map.of(
"user", user,
"order", order,
"score", score
));
}
}
✅ 3 个任务并行执行,总耗时 ≈ 最慢的那个,而不是累加。
2️⃣ 异常处理
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("error");
}).exceptionally(ex -> {
System.out.println("异常: " + ex.getMessage());
return null;
});
五、定时任务 + 异步:黄金组合
1️⃣ 定时触发,异步执行
@Component
public class DailyReportTask {
@Autowired
private ReportService reportService;
@Scheduled(cron = "0 0 2 * * ?")
@Async("taskExecutor")
public void generateReport() {
reportService.generate(); // 耗时任务异步执行
}
}
✅ 好处:
- 定时任务线程不阻塞
- 可并行执行多个定时任务
- 系统资源利用率高
六、分布式环境下的定时任务(防重复执行)
❌ 问题:多实例部署时,每个实例都会执行
实例 A ──→ 执行定时任务
实例 B ──→ 执行定时任务(重复!)
实例 C ──→ 执行定时任务(重复!)
✅ 解决方案 1:Redis 分布式锁(推荐)
@Component
public class DistributedScheduler {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Scheduled(cron = "0 0 2 * * ?")
public void task() {
String lockKey = "task:dailyReport";
String requestId = UUID.randomUUID().toString();
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, Duration.ofMinutes(5));
if (locked != null && locked) {
try {
// 执行任务
doBusiness();
} finally {
// 释放锁(Lua 脚本保证原子性)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey), requestId);
}
}
}
}
✅ 解决方案 2:数据库唯一约束
CREATE TABLE scheduled_lock (
task_name VARCHAR(50) PRIMARY KEY,
lock_time DATETIME
);
@Scheduled(cron = "0 0 2 * * ?")
public void task() {
int rows = lockMapper.tryLock("dailyReport", LocalDateTime.now());
if (rows == 1) {
doBusiness();
}
}
✅ 解决方案 3:XXL-JOB / PowerJob(生产级)
- 任务调度中心
- 分片广播
- 失败重试
- 任务监控
📌 企业级项目最终都会走向调度中心。
七、Spring Boot 4 新变化
-
虚拟线程(
@Async可配合VirtualThreadTaskExecutor) -
定时任务与 AOT 编译兼容更好
-
任务执行指标暴露(Micrometer)
@Bean
public Executor taskExecutor() {
return Executors.newVirtualThreadPerTaskExecutor(); // Java 21+
}
八、常见坑位总结
| 坑 | 解决 |
|---|---|
| 定时任务串行执行 | 配置 TaskScheduler 线程池 |
| 异步任务线程无限增长 | 自定义 ThreadPoolTaskExecutor |
| 任务重复执行 | Redis 锁 / 调度中心 |
| 任务异常无声失败 | 全局异常处理器 / exceptionally |
| 任务阻塞导致系统卡死 | 合理设置超时 + 监控 |
| 定时任务时间不准 | 用 cron + 分布式锁 |
九、本篇总结
@Scheduled默认串行,必须配置线程池@Async必须自定义线程池,拒绝默认配置- 定时 + 异步是黄金组合,避免阻塞调度线程
- 并行任务用
CompletableFuture,别用Future - 分布式环境定时任务必须防重(Redis 锁)
- 生产环境最终走向调度中心(XXL-JOB / PowerJob)