springboot 的 @Scheduled cron 延迟 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
frank1256
V2EX    程序员

springboot 的 @Scheduled cron 延迟

  •  
  •   frank1256 1 天前 233 次点击

    生产出现奇怪的事情

    代码里配置 @Scheduled(cron="0 0 2 * * ?") 生产 k8s 部署了 4 副本,但是 apm 发现有一个副本是 4 点钟执行任务。并且任务执行时会有数据库更新,库里都更新成了 4 点。

    进入容器内部,检查了 date 时区 ,代码版本,请求了接口的配置。4 个副本都一致。

    没啥头绪,有大佬指点下吗

    第 1 条附言    1 天前
    而且,更新数据库时间的值,是 LocalDateTime.now()也不是数据库时间,说明当时 jvm ,时间就是 4 点,但 2 点的 cron 怎么才触发。。懵了
    NelsonZhao
        1
    NelsonZhao  
       1 天前
    Gemini 的回答
    这是一个非常经典且具有迷惑性的问题!你排查的方向(时区、代码、配置)是正确的,但既然它们都一致,那么问题很可能出在你看不到的地方:Spring 的任务调度线程池。

    你遇到的“诡异现象”最可能的原因是:Spring TaskScheduler 线程池饥饿( Thread Pool Starvation )。

    问题分析:为什么 2 点的 Cron 会在 4 点执行?
    你对 LocalDateTime.now() 的分析非常关键,它证明了“任务执行时,JVM 时间确实是 4 点”。

    这并不是 2 点的 Cron 延迟触发了,而是 2 点的 Cron 准时触发了,但任务被“排队”了,直到 4 点钟才“轮到”它执行。

    详细的执行链条推测如下:

    Spring @Scheduled 的默认配置: 默认情况下,Spring Boot 用于 @Scheduled 的任务调度线程池( TaskScheduler )大小为 1 。这意味着,在同一时间,一个应用实例中只能有一个 @Scheduled 任务在运行。

    2:00 AM 准时触发: 在 2:00:00 ,4 个副本的调度器都准时醒来,说:“好了,该执行 cron="0 0 2 * * ?" 这个任务了。”

    提交任务到线程池:

    副本 1, 2, 3: 它们的调度线程池是空闲的。任务被提交,立刻被线程池中的那 1 个线程取出并立即开始执行。此时 LocalDateTime.now() 是 2:00 AM ,数据库更新为 2 点。

    副本 4 (问题副本): 它的调度器也在 2:00:00 提交了任务。但是,它的那 1 个调度线程正在忙!

    副本 4 为何忙碌?

    最可能的原因是:你代码中还有另一个 @Scheduled 任务(比如一个 cron="0 0 1 * * ?" 在 1 点钟触发的任务,或者一个 @Scheduled(fixedDelay=...) 的任务)。

    在副本 4 上,这个“更早的任务”由于某种原因(如 I/O 阻塞、数据库死锁、调用第三方接口超时、或者进入了 bug 循环)执行了非常长的时间。

    假设这个“长任务”从 1:00 AM 开始执行,一直执行到了 4:00 AM 才结束。

    任务排队与执行:

    在 2:00 AM ,你这个 2 点的任务被提交到副本 4 的队列中,它在排队。

    在 4:00 AM ,“长任务”终于执行完毕,释放了线程。

    线程池立刻从队列中取出下一个任务也就是你这个“本应 2 点执行”的任务,并开始执行它。

    结果:

    任务在 4:00 AM 开始执行。

    APM 监控到任务在 4:00 AM 开始。

    代码中的 LocalDateTime.now() 获取到的当前 JVM 时间就是 4:00 AM 。

    数据库被更新为 4:00 AM 。

    这个理论完美地符合了你的所有观察:时区没错,代码没错,配置也没错,错的是运行时的线程状态。

    如何验证和解决?
    如何验证
    检查 APM: 这是最快的方法。去 APM 查看那个“问题副本 4”,在 2:00 AM 到 4:00 AM 之间,是不是有另一个 @Scheduled 任务正在运行?你会在 APM 上看到一个从 1 点(或更早)开始,到 4 点才结束的超长执行链路。

    检查日志: 仔细翻看问题副本在 2:00 AM 之前的日志。看看有没有其他定时任务的“开始”日志,但迟迟没有打印“结束”日志。

    检查代码: 审查你项目中所有其他的 @Scheduled 方法。评估一下:

    它们有没有可能执行时间超过 1-2 个小时?

    它们有没有可能因为网络或数据库问题而卡住?

    jstack (如果能复现): 如果问题能复现,在 2:00 AM 刚过的时候,立即 ssh/exec 到问题容器,对 Java 进程执行 jstack 。你很可能会在线程 dump 中看到一个名为 scheduling-1 (或类似的) 线程,它正 RUNNABLE 或 BLOCKED 在某个长时间运行的任务代码上。

    如何解决
    你有两个层面的问题需要解决:

    问题一:调度线程池饥饿(你当前遇到的问题)

    方案 A (简单修复): 增加调度线程池的大小。这样即使一个任务卡住了,也不会阻塞其他任务。 在 application.yml (或 properties) 中配置:

    YAML

    spring:
    task:
    scheduling:
    pool:
    size: 10 # 默认是 1 ,改成 10 或更高
    方案 B (最佳实践): 如果你确认某个任务就是会执行很久,应该将它与普通的短任务隔离。

    让这个“长任务”的 @Scheduled 方法本身只做一件事:调用一个 @Async 异步方法。

    为这个 @Async 方法配置一个单独的、专用的线程池。

    这样,@Scheduled 线程(来自 scheduling-pool )会“秒级”执行完(仅仅是提交了一个异步任务),不阻塞其他定时任务。真正的耗时工作会在另一个线程池( async-pool )中进行。

    问题二:4 个副本重复执行(你潜在的更大问题)

    你提到 K8s 部署了 4 个副本。这意味着,这个 2:00 AM 的任务,每天都会被执行 4 次(副本 1 、2 、3 在 2 点执行,副本 4 在 4 点执行)。

    对于“更新数据库”这样的操作,这通常是一个严重的问题,会导致数据不一致或重复处理。

    解决方案 (强烈推荐): 引入分布式调度锁,例如 ShedLock 。

    ShedLock 是一个轻量级的库,它利用数据库(或 Redis/ZooKeeper 等)来确保一个带注解的 @Scheduled 任务在同一时间只会被一个集群实例执行。

    使用方法很简单:

    引入 ShedLock 依赖。

    配置一个 LockProvider (比如指向你的数据库)。

    在你的定时任务上加注解:

    Java

    import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;

    @Scheduled(cron="0 0 2 * * ?")
    @SchedulerLock(name = "myDailyTask", lockAtMostFor = "1h", lockAtLeastFor = "5m")
    public void yourTask() {
    // ... 数据库更新 ...
    }
    这样配置后,在 2:00 AM ,4 个副本都会尝试获取 myDailyTask 这个锁。只有一个副本能成功,然后执行任务。其他 3 个副本获取锁失败,会直接跳过当次的执行。

    总结
    你的时区/时钟 99% 没问题。

    问题出在 Pod 4 的 Spring 调度线程池(默认大小为 1 )被另一个长任务占满了。

    导致 2:00 AM 的任务在队列中排队,直到 4:00 AM 才轮到它执行。

    请立刻检查 APM 和日志,找到那个“超长任务”并优化它。

    请立刻配置 spring.task.scheduling.pool.size > 1 来避免阻塞。

    请立刻使用 ShedLock 来防止 4 个副本重复执行任务。
    shiny
        2
    shiny  
       1 天前 via iPhone   1
    @NelsonZhao 不要在这里直接贴 AI 的答案
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2604 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 34ms UTC 14:50 PVG 22:50 LAX 07:50 JFK 10:50
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86