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. 自检

  1. 默认调度器有几个线程?实测怎么证明?
  2. fixedRate 与 fixedDelay 的区别?
  3. @Async 默认线程池的问题?
  4. 多实例部署时定时任务怎么防重复?
  5. 重试三要素是什么?哪条最容易被忽略?

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/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。