Spring Boot: 定时任务与异步处理
最后更新:2026-08-26
定时任务和异步处理是后台效率的加速器——该定时的定时,该并行的并行,不浪费用户等待时间。
1. 你将学到
@EnableScheduling与 Cron / FixedRate / FixedDelay 表达式@EnableAsync与@Async异步方法执行- 自定义线程池
ThreadPoolTaskExecutor配置 - 异步异常处理
AsyncUncaughtExceptionHandler - Alice 实现超时 30 分钟未支付订单的自动取消定时任务
2. 一个业务开发者的真实故事
(1) 痛点:超时订单积压
Alice 发现 OrderFlow 中大量未支付订单堆积——用户下单后不付款,库存被锁定,其他用户无法购买。Charlie 报告说每天约有 2 thousand 个超时订单,累计锁定了价值 50 thousand USD 的库存。之前靠人工每 30 分钟查一次数据库取消订单,效率低下且容易遗漏。
(2) @Scheduled 的解法
Spring Boot 内置定时任务,一条注解即可实现:
JAVA
@Scheduled(fixedRate = 5 * 60 * 1000) // Every 5 minutes
public void cancelExpiredOrders() {
List<Order> expired = orderRepository.findByStatusAndCreatedAtBefore(
"PENDING", Instant.now().minus(30, ChronoUnit.MINUTES));
expired.forEach(order -> cancelOrder(order));
log.info("Cancelled {} expired orders", expired.size());
}
(3) 收益
Alice 实现定时任务后,超时订单每 5 分钟自动清理,库存自动释放,超时积压从 2 thousand/天降到 0。同时用 @Async 将通知邮件改为异步发送,下单接口响应时间从 800ms 降到 50ms。
3. @Scheduled 定时任务
(1) 三种调度方式
| 方式 | 注解参数 | 含义 | 示例 |
|---|---|---|---|
| FixedRate | fixedRate |
固定间隔(从上次开始计时) | 每 5 分钟执行 |
| FixedDelay | fixedDelay |
固定延迟(从上次结束计时) | 上次结束后等 5 分钟 |
| Cron | cron |
Cron 表达式 | 每天凌晨 2 点执行 |
gantt
title FixedRate vs FixedDelay Timing
dateFormat X
axisFormat %s
section FixedRate
Task A (5min) :a1, 0, 5
Task B (5min) :a2, 5, 8
Task C (5min) :a3, 10, 14
section FixedDelay
Task A (5min) :b1, 0, 5
wait 3min :b2, 5, 8
Task B (5min) :b3, 8, 13
wait 3min :b4, 13, 16
| 维度 | FixedRate | FixedDelay | Cron |
|---|---|---|---|
| 执行间隔 | 固定(不管上次耗时) | 固定(等上次结束) | 精确时间点 |
| 任务重叠 | 可能重叠 | 不会重叠 | 可能重叠 |
| 适用场景 | 轮询检查 | 数据处理 | 定时批处理 |
▶ 示例: FixedRate 定时任务
JAVA
@Component
@EnableScheduling
public class OrderScheduleTask {
private final OrderService orderService;
private static final Logger log = LoggerFactory.getLogger(OrderScheduleTask.class);
public OrderScheduleTask(OrderService orderService) {
this.orderService = orderService;
}
@Scheduled(fixedRate = 5 * 60 * 1000) // Every 5 minutes
public void cancelExpiredOrders() {
log.info("Starting expired order cleanup...");
int cancelled = orderService.cancelExpiredOrders(Duration.ofMinutes(30));
log.info("Cancelled {} expired orders", cancelled);
}
}
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例: Cron 表达式定时任务
JAVA
@Component
public class DailyReportTask {
@Scheduled(cron = "0 0 2 * * ?") // Every day at 2:00 AM
public void generateDailyReport() {
// Generate daily order statistics report
}
@Scheduled(cron = "0 0 0 1 * ?") // First day of every month at midnight
public void generateMonthlyReport() {
// Generate monthly report
}
}
输出:
TEXT
📖 仅展示
// 执行成功
| Cron 字段 | 含义 | 取值范围 |
|---|---|---|
| 秒 | Seconds | 0-59 |
| 分 | Minutes | 0-59 |
| 时 | Hours | 0-23 |
| 日 | Day of month | 1-31 |
| 月 | Month | 1-12 |
| 星期 | Day of week | 0-7 (0/7=Sun) |
| 常用 Cron | 含义 |
|---|---|
0 0 2 * * ? |
每天凌晨 2 点 |
0 */5 * * * ? |
每 5 分钟 |
0 0 0 1 * ? |
每月 1 号零点 |
0 0 9-17 * * MON-FRI |
工作日 9-17 点整点 |
4. @Async 异步处理
(1) 同步 vs 异步
▶ 示例: 同步发送通知(阻塞)
JAVA
@Service
public class OrderService {
public Order createOrder(CreateOrderRequest request) {
Order order = saveOrder(request);
emailService.sendConfirmation(order); // Blocks for 500ms
smsService.sendNotification(order); // Blocks for 200ms
return order; // Total: 700ms wasted
}
}
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例: 异步发送通知(非阻塞)
JAVA
@Service
@EnableAsync
public class NotificationService {
@Async
public void sendOrderConfirmation(Order order) {
// This runs in a separate thread
// Does not block the caller
emailService.sendConfirmation(order);
log.info("Confirmation email sent for order {}", order.getId());
}
@Async
public CompletableFuture<String> sendSmsNotification(Order order) {
smsService.sendNotification(order);
return CompletableFuture.completedFuture("SMS sent");
}
}
// In OrderService
@Service
public class OrderService {
private final NotificationService notificationService;
public Order createOrder(CreateOrderRequest request) {
Order order = saveOrder(request);
notificationService.sendOrderConfirmation(order); // Non-blocking
return order; // Returns immediately
}
}
输出:
TEXT
📖 仅展示
// 执行成功
| 维度 | 同步 | 异步 |
|---|---|---|
| 调用方式 | 阻塞等待结果 | 立即返回,后台执行 |
| 响应时间 | 累加所有操作耗时 | 仅核心业务耗时 |
| 错误处理 | try-catch 直接捕获 | AsyncUncaughtExceptionHandler |
| 适用场景 | 必须等待结果的操作 | 通知、日志、非关键操作 |
5. 自定义线程池
(1) 默认线程池的问题
Spring Boot 默认使用 SimpleAsyncTaskExecutor,它为每个任务创建新线程,不限制线程数,可能导致 OOM。
▶ 示例: 自定义 ThreadPoolTaskExecutor
JAVA
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("orderflow-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("Async exception in method {}: {}", method.getName(), ex.getMessage(), ex);
};
}
}
输出:
TEXT
📖 仅展示
// 执行成功
| 参数 | 含义 | 推荐值 |
|---|---|---|
corePoolSize |
核心线程数(常驻) | CPU 核数 |
maxPoolSize |
最大线程数 | CPU 核数 × 2 |
queueCapacity |
队列容量 | 100-1000 |
threadNamePrefix |
线程名前缀 | 项目名-async- |
rejectedExecutionHandler |
拒绝策略 | CallerRunsPolicy |
| 拒绝策略 | 行为 | 适用场景 |
|---|---|---|
AbortPolicy |
抛 RejectedExecutionException | 默认,快速失败 |
CallerRunsPolicy |
调用者线程执行 | 降级但不丢弃 |
DiscardPolicy |
静默丢弃 | 可容忍丢失 |
DiscardOldestPolicy |
丢弃最老任务 | 实时性优先 |
6. 异步异常处理
▶ 示例: AsyncUncaughtExceptionHandler
JAVA
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
private static final Logger log = LoggerFactory.getLogger(AsyncConfig.class);
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, objects) -> {
log.error("Async method [{}] threw exception: {}",
method.getName(), throwable.getMessage(), throwable);
// Could also publish to monitoring system
};
}
}
输出:
TEXT
📖 仅展示
// 执行成功
📌 重点:
@Async void 方法的异常不会传播到调用者,必须通过 AsyncUncaughtExceptionHandler 处理。返回 CompletableFuture 的方法可以通过 future.exceptionally() 处理。
7. 综合示例:OrderFlow 超时取消 + 异步通知
JAVA
// OrderScheduleTask.java
package com.orderflow.schedule;
import com.orderflow.service.OrderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
public class OrderScheduleTask {
private static final Logger log = LoggerFactory.getLogger(OrderScheduleTask.class);
private final OrderService orderService;
public OrderScheduleTask(OrderService orderService) {
this.orderService = orderService;
}
@Scheduled(fixedRateString = "${orderflow.order.expiry-check-interval:300000}")
public void cancelExpiredOrders() {
int timeoutMinutes = 30;
int cancelled = orderService.cancelExpiredOrders(Duration.ofMinutes(timeoutMinutes));
if (cancelled > 0) {
log.info("Cancelled {} orders expired for {} minutes", cancelled, timeoutMinutes);
}
}
@Scheduled(cron = "0 0 2 * * ?")
public void generateDailyReport() {
log.info("Generating daily order report...");
orderService.generateDailyReport();
}
}
// NotificationService.java
package com.orderflow.service;
import com.orderflow.model.Order;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class NotificationService {
private static final Logger log = LoggerFactory.getLogger(NotificationService.class);
@Async
public void sendOrderConfirmation(Order order) {
// Simulate email sending
log.info("Sending confirmation email for order {}", order.getId());
}
@Async
public void sendCancellationNotice(Order order) {
log.info("Sending cancellation notice for order {}", order.getId());
}
@Async
public CompletableFuture<String> sendSmsNotification(Order order) {
log.info("Sending SMS for order {}", order.getId());
return CompletableFuture.completedFuture("SMS sent for order " + order.getId());
}
}
// AsyncConfig.java
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfig implements AsyncConfigurer {
private static final Logger log = LoggerFactory.getLogger(AsyncConfig.class);
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(5);
exec.setMaxPoolSize(20);
exec.setQueueCapacity(100);
exec.setThreadNamePrefix("orderflow-async-");
exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
exec.setWaitForTasksToCompleteOnShutdown(true);
exec.setAwaitTerminationSeconds(60);
exec.initialize();
return exec;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("Async error in {}: {}", method.getName(), ex.getMessage(), ex);
}
}
❓ 常见问题
Q @Scheduled 只能用单线程执行吗?
A 默认是的。多个 @Scheduled 方法共用一个单线程调度器,长任务会阻塞其他任务。解决方案:配置
TaskScheduler 线程池,或把耗时操作用 @Async 委托给线程池。Q @Async 在同一个类内部调用会生效吗?
A 不会。和 @Transactional 一样,@Async 基于 AOP 代理,同类方法互调不走代理。解决方案:1)拆分到不同 Service;2)注入自身代理。
Q fixedRate 和 fixedDelay 在长任务时有什么区别?
A fixedRate 从上次开始时间计时,如果任务耗时超过间隔,任务会连续执行甚至重叠。fixedDelay 从上次结束时间计时,保证任务间有固定间隔,不会重叠。
Q 生产环境定时任务如何避免多实例重复执行?
A 1)数据库行锁(SELECT FOR UPDATE);2)分布式锁(Redis/ShedLock);3)只在一个实例上启用调度。推荐 ShedLock 库,专门解决此问题。
Q 如何动态修改定时任务的时间?
A 可以使用
ScheduledTaskRegistrar 编程式注册任务,或配合配置中心动态刷新。简单方案:fixedRateString = "${interval}" 通过外部配置控制。Q 异步方法返回 CompletableFuture 和 void 有什么区别?
A void 方法"fire-and-forget",调用者不关心结果,异常由 AsyncUncaughtExceptionHandler 处理。CompletableFuture 方法可以链式处理结果和异常,调用者有更多控制权。
📖 小节
@Scheduled三种调度:fixedRate(固定间隔)、fixedDelay(固定延迟)、cron(精确时间)@Async让耗时操作不阻塞调用者,适用于通知、日志等非关键操作- 默认线程池不限制线程数,生产环境必须自定义
ThreadPoolTaskExecutor AsyncUncaughtExceptionHandler处理 void 异步方法的异常- 定时任务多实例部署需用分布式锁(如 ShedLock)防止重复执行
@Async同类方法互调不生效,需拆分类或注入代理
📝 作业
-
基础题(难度⭐):为 OrderFlow 实现超时订单自动取消定时任务,每 5 分钟检查并取消 30 分钟未支付的 PENDING 订单。
-
进阶题(难度⭐⭐):将订单确认邮件和 SMS 通知改为 @Async 异步发送,自定义线程池(core=5, max=20),验证下单接口响应时间显著降低。
-
挑战题(难度⭐⭐⭐):集成 ShedLock 实现分布式定时任务锁,确保 OrderFlow 多实例部署时同一时刻只有一个实例执行定时任务,思考锁的粒度和超时设计。