半年都还没解决的定时任务问题

首页 编程分享 PHP丨JAVA丨OTHER 正文

提前退休的java猿 转载 编程分享 2025-06-28 22:02:12

简介 前言 24年初开始做国产化开始,用了quartz定时任务框架开始,就经常遇到定时任务执行失败的问题,执行一段时间就停止执行了。直到现在问题也没有完全得到解决💀 最开始也排查过因为架构不合理,多个项目共


前言

24年初开始做国产化开始,用了quartz定时任务框架开始,就经常遇到定时任务执行失败的问题,执行一段时间就停止执行了。直到现在问题也没有完全得到解决💀

最开始也排查过因为架构不合理,多个项目共用一个库,定时任务信息没有进行隔离,导致A项目里面跑B项目的定时任务肯定是找不到任务类信息导致定时任务报错等问题。中途还出现过由于发版、数据迁移导致任务信息产生脏数据导致定时任务出问题。

这些问题我都解决了。今年换了迁移到信创环境之后,有个问题一直没有解决😭,直到今天我才定位到问题。

先说一下最终定位到的问题吧,就是Quartz框架在维护任务状态的时候,提交事务的时候,出现Read timed out,但是数据库里面的数据却显示修改成功了。导致状态一直卡在这儿了。

Quartz 执行一段时间就中断

如果要看懂本篇文章,首先还是需要 了解过 Quartz 框架的基础知识的。
推荐阅读:

问题描述

定时任务经常执行一段时间就不再执行了,因为我们重新编辑任务之后,任务又能重新运行一段时间,执行频率越高的任务停止运行的频率就越高!

所以首先排除我们业务代码的问题,并不是执行过程中我们的业务代码出错。而是框架内部自己维护的状态出了问题,导致某个任务,就一直不在执行了。

解决过程

所以首先排除我们业务代码的问题之后,可以猜测是定时任务框架内部的状态维护出了问题。接下来就是确认我们的猜测,找到关键日志,定位问题所在,然后就是给出解决方案。

一:确定内部问题状态

Quartz定时任务框架维护内部状态,有多种方式,我们采用的是使用数据库存储的方式。Quartz内部维护任务状态的主要在QRTZ_TRIGGERSQRTZ_FIRED_TRIGGERS 这两个表中TRIGGER_STATESTATE字段。

找到任务停止的定时任务数据

QRTZ_TRIGGERS表部分字段数据

TRIGGER_NAME NEXT_FIRE_TIME PREV_FIRE_TIME TRIGGER_STATE
13023696897 1750726680000(2025-06-24 08:58:00) 1750726620000(2025-06-24 08:57:00) ACQUIRED

QRTZ_FIRED_TRIGGERS表部分字段数据:

TRIGGER_NAME FIRED_TIME SCHED_TIME STATE
13023696897 1750726670204(2025-06-24 08:57:50) 1750726680000(2025-06-24 08:58:00) ACQUIRED

ACQUIRED:触发器已被调度器获取,正在执行或准备执行任务。

看到上面的状态信息就能够确认,任务执行的最后时间点,找出日志文件

二:找出日志,定位问题

上面我们已经确认任务,是在卡在ACQUIRED状态,并且拿到了具体时间点。我们就找出此时的日志如下:

 .......................  
 org.springframework.scheduling.quartz.LocalDataSourceJobStore.closeConnection(LocalDataSourceJobStore.java:174)
	at org.quartz.impl.jdbcjobstore.JobStoreSupport.cleanupConnection(JobStoreSupport.java:3678)
	at org.quartz.impl.jdbcjobstore.JobStoreSupport.executeInNonManagedTXLock(JobStoreSupport.java:3896)
	at org.quartz.impl.jdbcjobstore.JobStoreSupport.acquireNextTriggers(JobStoreSupport.java:2802)
	at org.quartz.core.QuartzSchedulerThread.run(QuartzSchedulerThread.java:287)
2025-06-24 08:58:00.269 [MyScheduler_QuartzSchedulerThread] [1;31mERROR[0;39m [36morg.quartz.core.ErrorLogger:2407[0;39m - An error occurred while scanning for the next triggers to fire.
org.quartz.JobPersistenceException: Couldn't commit jdbc connection. An I/O error occurred while sending to the backend.
	at org.quartz.impl.jdbcjobstore.JobStoreSupport.commitConnection(JobStoreSupport.java:3749)
	at org.quartz.impl.jdbcjobstore.JobStoreSupport.executeInNonManagedTXLock(JobStoreSupport.java:3866)
	at org.quartz.impl.jdbcjobstore.JobStoreSupport.acquireNextTriggers(JobStoreSupport.java:2802)
	at org.quartz.core.QuartzSchedulerThread.run(QuartzSchedulerThread.java:287)
Caused by: com.kingbase8.util.KSQLException: An I/O error occurred while sending to the backend.
..........
Caused by: java.net.SocketTimeoutException: Read timed out

❗ 看到这个日志,其实我们已经能够确认引起定时任务状态问题的所在了,就是数据库的问题read timed out

三、验证问题

2025-06-24 08:58:00.269 [MyScheduler_QuartzSchedulerThread] [1;31mERROR[0;39m [36morg.quartz.core.ErrorLogger:2407[0;39m
- An error occurred while scanning for the next triggers to fire.
org.quartz.JobPersistenceException: 
Couldn't commit jdbc connection. An I/O error occurred while sending to the backend. 
at org.quartz.impl.jdbcjobstore.JobStoreSupport.commitConnection(JobStoreSupport.java:3749) 
at org.quartz.impl.jdbcjobstore.JobStoreSupport.executeInNonManagedTXLock(JobStoreSupport.java:3866) 
at org.quartz.impl.jdbcjobstore.JobStoreSupport.acquireNextTriggers(JobStoreSupport.java:2802) at org.quartz.core.QuartzSchedulerThread.run(QuartzSchedulerThread.java:287)

从上述日志,我们得出了,是在执行org.quartz.impl.jdbcjobstore.JobStoreSupport.acquireNextTriggers(JobStoreSupport.java:2802)方法时候,数据库连接出现了问题。接下来我们就需要搞清楚该方法的逻辑以及上下文。

QuartzSchedulerThread.run() 方法:

try {
    // 查询30s内要执行的触发器、修改trigger中状态为`ACQUIRED`,在fired_trigger 中插入一条记录,状态也为`ACQUIRED`
    triggers = qsRsrcs.getJobStore().acquireNextTriggers(
            now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
    acquiresFailed = 0;
    if (log.isDebugEnabled())
        log.debug("batch acquisition of " + (triggers == null ? 0 : triggers.size()) + " triggers");
} catch (JobPersistenceException jpe) {
    if (acquiresFailed == 0) {
    //------------------------------------------------------------捕捉到日志,调用shcduler error 的监听器---------
        qs.notifySchedulerListenersError(
            "An error occurred while scanning for the next triggers to fire.",
            jpe);
    }
    if (acquiresFailed < Integer.MAX_VALUE)
        acquiresFailed++;
    continue;
} catch (RuntimeException e) {
    if (acquiresFailed == 0) {
        getLog().error("quartzSchedulerThreadLoop: RuntimeException "
                +e.getMessage(), e);
    }
    if (acquiresFailed < Integer.MAX_VALUE)
        acquiresFailed++;
    continue;
}

acquireNextTriggers方法的主要逻辑如下:

  1. 查询待触发触发器(30s内要执行的任务)
    QRTZ_TRIGGERS表中筛选状态为WAITING且 ** 下次触发时间 ≤ noLaterThan + timeWindow** 的触发器。
  2. 状态变更
    将选中的触发器状态从WAITING更新为ACQUIRED,并记录到QRTZ_FIRED_TRIGGERS表中。
  3. 返回结果
    返回OperableTrigger列表,包含触发器的详细信息(如 Cron 表达式、JobDataMap 等)。
3.1验证结果

✅也就是acquireNextTriggers在将 QRTZ_TRIGGERSQRTZ_FIRED_TRIGGERS 状态改成ACQUIRED 之后,手动提交事务的时候,conn 就失去了连接。 如果没有手动的事务被提交了,那么就刚好印证了这个问题。

❓但是我们看下面的commitConnection 如果提交失败,是会被捕捉然后,回滚。如果回滚成功则不会出现我们上面的问题,如果回滚失败,则应该有回滚失败的日志。

根据日志发现是commit的时候报错了,但是数据已经生效了。commit失败之后代码有回滚策略。所以到这儿作者也脑壳疼了,是日志的问题???还是因为数据库连接断开之后,自动提交了。还是连接池的问题?

protected <T> T executeInNonManagedTXLock(
        String lockName, 
        TransactionCallback<T> txCallback, final TransactionValidator<T> txValidator) throws JobPersistenceException {
    boolean transOwner = false;
    Connection conn = null;
    try {
        ....
        conn = getNonManagedTXConnection();
        .......
        // txcallback 更新trigger 相关的状态方法---------------
        final T result = txCallback.execute(conn);
        try {
            commitConnection(conn);
        } catch (JobPersistenceException e) {
         // conn 不为null的话就 rollback
         rollbackConnection(conn);
        ..........
        return result;
        }
    } catch (JobPersistenceException e) {
        rollbackConnection(conn);
        throw e;
    } catch (RuntimeException e) {
        rollbackConnection(conn);
        throw new JobPersistenceException("Unexpected runtime exception: "
                + e.getMessage(), e);
    } finally {
        // 下面也在报错 ``
        try {
            releaseLock(lockName, transOwner);
        } finally {
            cleanupConnection(conn);
        }
    }
}

解决方案

  1. 监听schedulerError实现SchedulerListener.schedulerError()接口,对ACQUIRED状态的数据进行回滚或者删除后重新添加;并且启动的时候也执行一下。
  2. 使用spring的方式,写一个检查的任务状态的任务,对任务状态有误的数据,做回滚处理,或者删除后重新添加。
  3. 因为出问题的根本原因,还是和数据简历连接的问题,可以将状态维护的方式,改成内存维护。

🚫上面的解决方案都是为了弥补任务状态出错的补偿修复机制,没从根本上解决数据库read time out的问题。并且这个问题,出现在任何和数据库交互的地方。现在被国产环境搞得有点难受🏃‍♂️

🤶大佬们对数据库偶尔出现read time out的问题,有好的解决思路吗?欢迎留言评论👏

🏋️‍♀️后面我也会对数据库这个问题,持续跟进,解决之后也会有博客更新 欢迎各位大佬的持续关注

转载链接:https://juejin.cn/post/7520204324770627625


Tags:


本篇评论 —— 揽流光,涤眉霜,清露烈酒一口话苍茫。


    声明:参照站内规则,不文明言论将会删除,谢谢合作。


      最新评论




ABOUT ME

Blogger:袅袅牧童 | Arkin

Ido:PHP攻城狮

WeChat:nnmutong

Email:nnmutong@icloud.com

标签云