943 字
约 3 分钟
6
S07-J04 · 定时任务与异步:@Scheduled / 线程池 / 重试
适用读者:写过多定时任务、结果它们互相阻塞的人。
前置:S07-B05。
实测环境:Spring Boot 3.3.6 / JDK 17(2026-11-05)。
覆盖旧文:388(131) 1753(86) 1752(85) 382(292) 977(69) = 663 点。
覆盖扩展:E-S07-016(单线程调度器的坑)。
上一篇:S07-J03 · Spring Event:事件发布订阅
下一篇:S07-J05 · 接口幂等。
1. 一句话结论
@Scheduled 默认只有一个调度线程(scheduling-1)。所有定时任务串行执行——一个卡住,全部延迟。
实测(两个任务按 1.5s 周期触发):
[sched] 定时任务触发 线程=scheduling-1
[sched] 定时任务触发 线程=scheduling-1
...
线程名始终是 scheduling-1 ——这就是"单线程调度器"的直接证据。
2. 三种调度写法
| 写法 | 含义 | 注意 |
|---|---|---|
fixedRate |
按固定频率(上次开始算) | 任务超时会堆积 |
fixedDelay |
上次结束后延迟 | 更安全(不重叠) |
cron |
表达式 | 时区要显式指定 |
判据:"任务会超时吗?" 会 → fixedDelay(避免重叠);不会 → fixedRate。
3. 单线程的坑与解法(E-S07-016)
| 问题 | 现象 |
|---|---|
| 任务互相阻塞 | 长任务导致其他任务延迟 |
| 定时任务与业务争抢 | 同一线程池被打满 |
解法:
@Bean
public TaskScheduler taskScheduler() {
var s = new ThreadPoolTaskScheduler();
s.setPoolSize(5); // 多线程调度
s.setThreadNamePrefix("sched-");
return s;
}
@Async 是另一个池:默认 SimpleAsyncTaskExecutor(每次新建线程!)——必须自定义 ThreadPoolTaskExecutor(否则高并发下线程爆炸,S05-B05 同源)。
4. 分布式下的重复执行(A-1752/A-1753)
单机多实例时,每个实例都会跑定时任务 → 重复执行。
| 方案 | 说明 |
|---|---|
| 分布式锁(Redis/ZK) | 抢到锁的实例执行 |
| 调度中心(Quartz 集群 / XXL-JOB) | 由中心分配 |
| 单实例部署 | 最简单(但无高可用) |
判据:"这个任务重复执行会怎样?" 会出错(如重复扣款)→ 必须加锁。
5. 重试组件(A-382)
| 方案 | 说明 |
|---|---|
Spring Retry(@Retryable) |
注解式,支持退避 |
| 手写循环 + 退避 | 可控,代码多 |
| 消息队列重试 | 跨进程,最可靠 |
重试三要素:
① 什么条件重试(异常类型)
② 重试几次、间隔多少(指数退避 + 抖动)
③ 重试仍失败怎么办(告警/死信/补偿)
第 ③ 条最常被忽略——重试不是"最终会成功",必须有兜底。
6. 五个误区
| 误区 | 真相 |
|---|---|
| "定时任务天然并发" | 默认单线程(实测 scheduling-1) |
"@Async 开箱即用" |
默认线程池每次新建线程 |
| "多实例各自跑没影响" | 会重复执行 |
"fixedRate 更准时" |
任务超时会堆积 |
| "重试一定会成功" | 必须设计"重试失败"分支 |
7. 自检
- 默认调度器有几个线程?实测怎么证明?
fixedRate与fixedDelay的区别?@Async默认线程池的问题?- 多实例部署时定时任务怎么防重复?
- 重试三要素是什么?哪条最容易被忽略?
8. 下一篇
S07-J05 · 接口幂等:六种方案与选型
定时任务讲完,下一篇讲"重复请求"这个更普遍的问题:幂等键、去重表、状态机。
本篇属于系列《S07 · Spring 框架》。旧文来源:A-388/1753/1752/382/977;扩展点 E-S07-016。调度线程名为 Spring Boot 3.3.6 实测。
S07-J04 · 定时任务与异步:@Scheduled / 线程池 / 重试
https://www.clxhxhhr.top/posts/5277/ 评论
0 条
还没有评论,先写一条吧。