当前位置: 首页 > news >正文

Spring @Scheduled 定时任务突然停跑、不再执行全场景分析

Spring @Scheduled 定时任务突然停跑、不再执行全场景分析

先记住核心底层:Spring @Scheduled 底层是ScheduledThreadPoolExecutor,Spring 会对任务加 try-catch 吞异常,单纯业务抛异常不会停任务;一旦任务永久卡住、线程耗尽、调度器关闭,才会彻底不再执行。

一、最常见:任务执行时间过长,线程池耗尽(生产最高发)

原理

Spring 默认定时线程池核心线程数只有 1(单线程调度池)。

  1. 单线程串行执行所有@Scheduled任务;
  2. 某一个任务执行耗时极长(大量IO、同步HTTP、大事务、大批量数据循环无分页、死锁、慢SQL);
  3. 调度线程被永久占用,无空闲线程处理下一轮调度;
  4. 等到下一个 cron/fixedRate 触发点,没有可用线程,任务直接被丢弃,不再执行。

举例复现

// 每5秒执行,但内部sleep 20秒@Scheduled(fixedRate=5000)publicvoidlongTask()throwsInterruptedException{System.out.println("开始执行");Thread.sleep(20000);// 长时间阻塞调度线程}

现象:执行几次后彻底看不到日志,定时卡死。

区分两个配置

  • fixedRate:按固定间隔发起调度,线程忙则堆积丢弃;
  • fixedDelay:等上一次完全执行完,再间隔N毫秒执行,不会堆积,但长时间阻塞依然会卡线程。

解决

自定义定时线程池,增大核心线程:

@ConfigurationpublicclassScheduleConfigimplementsSchedulingConfigurer{@OverridepublicvoidconfigureTasks(ScheduledTaskRegistrartaskRegistrar){taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));}}

二、任务内部出现永久阻塞(线程卡死不释放)

Spring 只会捕获Exception,但阻塞不会抛异常,线程一直占用:

  1. 无限等待锁、分布式锁未释放(Redisson lock 忘记 unlock、异常分支没释放锁);
  2. 阻塞IO:http 调用无超时、数据库连接不释放、阻塞队列 take();
  3. 死锁(多表更新顺序不一致、多线程互等锁);
  4. 外部接口无限等待,未设置 connect/read 超时。

典型坑:分布式锁漏释放

@Scheduled(cron="0/10 * * * * ?")publicvoidtask(){RLocklock=redissonClient.getLock("task_lock");lock.lock();// 业务抛异常,没有finally解锁dbBiz();lock.unlock();}

一旦dbBiz()抛异常,unlock不执行,锁永久持有,下一次进来lock()永久阻塞调度线程。

三、调度器实例被销毁/上下文关闭

  1. Spring 容器关闭:应用正常/异常停机、kill -15,调度池 shutdown,所有定时停止;
  2. 动态删除定时任务:代码中手动ScheduledTaskRegistrar移除任务、销毁触发器;
  3. 热更新/动态刷新上下文:部分配置中心刷新 Bean、销毁重建定时相关 Bean,旧调度被关闭;
  4. 多实例部署 + 分布式锁单点抢占:单实例抢到锁后长期阻塞,其他实例本身不执行,看起来“任务没跑”。

四、线程内部抛出 Error(非 Exception,Spring 无法捕获)

Spring 包装任务只 catchExceptionError 不会捕获,会直接终止调度线程

  • OOMOutOfMemoryError
  • StackOverflowError
  • NoClassDefFoundError
    一旦抛出 Error,调度线程直接死亡,默认线程池不会自动重建线程,该定时永久停止。

示例:循环递归无终止条件,栈溢出,定时直接停。

五、cron表达式配置错误 / 时区问题(看起来“停了”)

  1. cron 写错:比如0 0 25 * * ?不存在25点,永远不触发;
  2. 时区不匹配:Spring 定时默认服务器时区,集群时区不一致、配置文件指定错误时区,触发时间偏移到凌晨/不存在的时刻;
  3. 项目重启后 cron 下一次触发间隔极长,误以为任务挂了。

六、第三方中间件资源耗尽阻塞任务

  1. MySQL 连接池耗尽:定时批量查询/更新不释放连接,getConnection()阻塞;
  2. Redis 连接打满,Jedis/Redisson 获取连接阻塞;
  3. MQ 消费者阻塞,同步拉取消息无限等待。

七、异步/事务注解干扰,间接阻塞调度线程

  1. @Transactional长事务,锁表、占用数据库连接;
  2. 错误在定时方法上加@Async,大量异步任务打满应用线程池,间接导致数据库/IO资源耗尽,下次定时执行阻塞;
  3. 事务死锁,线程卡死。

八、特殊:JDK 版本 bug / ScheduledThreadPool 线程失效

低版本 JDKScheduledThreadPoolExecutor存在已知 bug:
当任务抛出 Error、线程异常退出后,线程池不会补充新工作线程,核心线程数永久少1,单线程池直接报废。

九、区分假象:不是任务停了,是日志没打印

  1. 日志级别调整为 ERROR,INFO 打印被屏蔽;
  2. 日志输出缓冲、磁盘满,日志落盘失败,看不到执行记录;
  3. 日志切面拦截异常吞掉执行日志,误以为未执行。

快速排查步骤(生产定位流程)

  1. 查看服务器线程堆栈jstack pid
    • 搜索scheduled调度线程,看是否处于WAITING/BLOCKED(锁阻塞);
    • 查看线程执行栈,定位卡在哪个代码行(数据库、锁、HTTP调用)。
  2. 查看 GC 日志,判断是否频繁 FullGC、OOM。
  3. 核对定时线程池核心大小,确认是否为默认1。
  4. 检查任务内锁、数据库、HTTP 是否全部设置超时、finally释放资源。
  5. 查看系统 error 日志,是否存在 OOM、StackOverflow 等 Error 堆栈。
  6. 核对 cron 时区与表达式正确性。

生产强制规范,杜绝定时停跑

  1. 自定义定时线程池,核心线程数 ≥5;
  2. 所有外部调用(HTTP、Redis、DB)强制设置超时时间;
  3. 分布式锁、数据库连接必须 finally 释放;
  4. 定时方法最外层包裹 try-catch(Throwable),捕获 Exception + Error,防止线程死亡;
  5. 大批量操作分页处理,避免单次任务长时间执行;
  6. 定时执行增加监控埋点:执行次数、耗时、失败告警;
  7. 禁止无限循环、无超时阻塞代码写在定时内。

兜底安全模板(推荐所有定时统一使用)

@Scheduled(fixedRate=10000)publicvoidsafeTask(){try{// 业务逻辑doBiz();}catch(Throwablet){log.error("定时任务执行异常",t);// 告警推送}}

Java定时任务抛未捕获异常后任务是否继续执行?分三种主流定时框架分别说明

核心结论先概括:普通单线程定时,未捕获异常会终止当前任务线程,后续调度是否执行取决于框架实现;ScheduledExecutorService、Spring Task、Quartz行为各不相同

一、JDK原生:ScheduledExecutorService(最基础定时)

1. 代码示例

ScheduledExecutorServicepool=Executors.newSingleThreadScheduledExecutor();// 每隔1秒执行一次pool.scheduleAtFixedRate(()->{System.out.println("任务执行");thrownewRuntimeException("未捕获异常");},0,1,TimeUnit.SECONDS);

2. 现象:任务只会执行1次,之后永久不再调度

3. 底层原因

  1. scheduleAtFixedRate/scheduleWithFixedDelay提交的是循环任务Runnable
  2. 任务内部抛出未捕获异常,线程池执行时会捕获异常并标记任务执行失败
  3. 单线程调度池(newSingleThreadScheduledExecutor)
    任务抛出异常后,该循环任务直接被丢弃,调度器不会再重试、不会再触发下一次;
  4. 多线程Scheduled池同理:单个失败的循环任务永久终止,其他互不影响
  5. 底层源码逻辑:ScheduledFutureTask.run() 中,异常抛出会进入cancel(false),循环标记置为false,调度终止。

补充:如果是一次性任务schedule(),抛异常只是单次执行失败,不存在后续调度问题。

二、Spring 自带定时 @Scheduled(业务最常用)

1. 现象:本次任务终止,下一次调度依旧正常执行

2. 原因

Spring定时底层封装了ScheduledExecutorService,但做了全局异常捕获兜底

  1. Spring会给用户的任务方法套一层代理包装;
  2. 任务方法抛出未捕获异常时,Spring内部try-catch捕获异常、打印错误日志;
  3. 异常不会向上抛给ScheduledExecutor,底层调度线程不会感知异常;
  4. 调度计时器不受影响,到下一个触发时间点,会再次执行该定时方法。

关键区别对比JDK原生

  • JDK原生裸写Runnable抛异常 → 任务永久停;
  • Spring @Scheduled方法抛异常 → 仅本次失败,下次照常跑。

验证示例

@ComponentpublicclassDemoTask{privateintcount=0;@Scheduled(fixedRate=1000)publicvoidtask(){count++;System.out.println("第"+count+"次执行");if(count==2){thrownewRuntimeException("报错");}}}

输出:

第1次执行 第2次执行 ERROR 异常堆栈 第3次执行 第4次执行 ...持续循环

三、Quartz 定时框架(复杂分布式定时)

行为由JobDetail、Trigger、异常策略控制,分两种场景:

  1. Job执行抛未捕获异常,抛出 JobExecutionException
    • 若设置setRefireImmediately(true):立刻重试本次任务;
    • 若不设置重试:本次失败,等待下一个 cron/间隔周期正常执行;
  2. 若直接抛出普通RuntimeException(未包装JobExecutionException):
    Quartz会捕获,标记本次执行失败,不影响后续调度,到点继续执行
    特殊:配置故障恢复、错过任务补偿时会有额外重试逻辑。

四、拓展:Timer(老旧废弃定时类,不推荐使用)

Timertimer=newTimer();timer.schedule(newTimerTask(){@Overridepublicvoidrun(){thrownewRuntimeException();}},0,1000);

现象:整个Timer线程直接崩溃,所有定时任务全部停止
原因:Timer只有单个守护线程,无异常捕获,一旦抛异常线程直接终止,整个Timer报废。

统一总结表格

定时实现方式未捕获异常后果后续是否继续执行
JDK Timer(废弃)唯一执行线程崩溃,全部任务停否,全部终止
JDK ScheduledExecutor 原生循环任务当前任务永久终止,其他任务不受影响否,该任务不再跑
Spring @Scheduled仅本次执行失败,日志打印异常,调度不受影响是,下次正常执行
Quartz本次失败,默认等待周期重新执行,可配置重试策略是,默认继续

生产环境规范建议

  1. 所有定时任务方法必须手动try-catch捕获全部异常,打印业务日志+异常堆栈;
  2. JDK原生Scheduled场景,兜底捕获防止任务彻底卡死;
  3. 异常时增加告警(短信/钉钉),避免任务静默失败无人感知;
  4. 禁止使用Timer,统一使用Scheduled线程池或Spring Task。
http://www.cnnetsun.cn/news/3154298.html

相关文章:

  • 系统动力学驱动的钢铁行业碳排放预测:从模型构建到情景仿真全流程复现
  • 为什么18KV绝缘鞋越来越受欢迎?真正原因曝光!
  • 如何永久保存QQ空间青春记忆?这个免费工具让你轻松备份完整数据
  • 嵌入式系统硬件去抖动矩阵键盘设计
  • 5分钟掌握抖音下载神器:如何免费批量下载直播回放和用户主页视频
  • 抖音直播回放下载终极指南:简单三步搞定无水印批量保存
  • api-guarder vs 其他接口工具:为什么它是最佳选择?
  • 3分钟学会OBS多平台直播:免费高效的多路推流终极解决方案
  • Thorium浏览器:终极性能优化的Chromium分支实战指南
  • 盛最多水的容器 — AI 写了两版,第一版差点把面试官气走
  • WWDC 视频批量下载:一个 Swift 脚本搞定所有资源
  • Steam创意工坊下载终极指南:5分钟学会用WorkshopDL免费下载模组
  • 养好猫,趣闯关!《喵呜乐消消》承包你的碎片时间
  • 终极指南:3分钟掌握BetterNCM插件管理器,彻底改造网易云音乐
  • ppInk屏幕标注工具:从新手到专家的完整Windows演示指南
  • Deepin Boot Maker完全指南:5分钟制作专业启动盘的免费开源方案
  • Beyond Compare 5永久激活终极指南:开源密钥生成器完整教程
  • Beyond Compare 5永久激活终极指南:开源密钥生成器完整使用教程
  • Locale-Emulator:智能解决Windows非Unicode程序区域兼容性难题
  • Android Keymaster/KeyMint:硬件级密钥管理与认证原理与NPI实践
  • 终极文档下载解决方案kill-doc:如何免费获取全网文档资源
  • 【信息科学与工程学】【制造工程】第三十四篇 3D TSV制造工程01
  • 3个步骤快速掌握Minecraft PCL启动器:终极免费解决方案
  • Topit:终极macOS窗口置顶解决方案,5分钟彻底告别窗口遮挡烦恼
  • StreamCap终极指南:3步掌握开源直播录制工具,轻松录制40+平台直播内容
  • B站缓存视频合并教程:3步将零散分段变成完整视频
  • 2026年6月GESP真题及题解(C++五级):晚宴
  • Bilibili-Old:现代化技术栈重构经典B站界面解决方案
  • 国产大模型价格战复盘 2024-2026:24 个月里,谁在裸泳,谁在赚安静的钱
  • 从零开始掌握ColabFold:让蛋白质结构预测变得触手可及