前言
24年初开始做国产化开始,用了quartz
定时任务框架开始,就经常遇到定时任务执行失败的问题,执行一段时间就停止执行了。直到现在问题也没有完全得到解决💀
最开始也排查过因为架构不合理,多个项目共用一个库,定时任务信息没有进行隔离,导致A项目里面跑B项目的定时任务肯定是找不到任务类信息导致定时任务报错等问题。中途还出现过由于发版、数据迁移导致任务信息产生脏数据导致定时任务出问题。
这些问题我都解决了。今年换了迁移到信创环境之后,有个问题一直没有解决😭,直到今天我才定位到问题。
先说一下最终定位到的问题吧,就是Quartz框架在维护任务状态的时候,提交事务的时候,出现
Read timed out
,但是数据库里面的数据却显示修改成功了。导致状态一直卡在这儿了。
Quartz 执行一段时间就中断
如果要看懂本篇文章,首先还是需要 了解过 Quartz
框架的基础知识的。
推荐阅读:
问题描述
定时任务经常执行一段时间就不再执行了,因为我们重新编辑任务之后,任务又能重新运行一段时间,执行频率越高的任务停止运行的频率就越高!
所以首先排除我们业务代码的问题,并不是执行过程中我们的业务代码出错。而是框架内部自己维护的状态出了问题,导致某个任务,就一直不在执行了。
解决过程
所以首先排除我们业务代码的问题之后,可以猜测是定时任务框架内部的状态维护出了问题。接下来就是确认我们的猜测,找到关键日志,定位问题所在,然后就是给出解决方案。
一:确定内部问题状态
Quartz定时任务框架维护内部状态,有多种方式,我们采用的是使用数据库存储的方式。Quartz内部维护任务状态的主要在QRTZ_TRIGGERS
、QRTZ_FIRED_TRIGGERS
这两个表中TRIGGER_STATE
、STATE
字段。
找到任务停止的定时任务数据:
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
方法的主要逻辑如下:
-
查询待触发触发器(30s内要执行的任务)
从QRTZ_TRIGGERS
表中筛选状态为WAITING
且 ** 下次触发时间 ≤noLaterThan + timeWindow
** 的触发器。 -
状态变更
将选中的触发器状态从WAITING
更新为ACQUIRED
,并记录到QRTZ_FIRED_TRIGGERS
表中。 -
返回结果
返回OperableTrigger
列表,包含触发器的详细信息(如 Cron 表达式、JobDataMap 等)。
3.1验证结果
✅也就是acquireNextTriggers
在将 QRTZ_TRIGGERS
和 QRTZ_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);
}
}
}
解决方案
- 监听schedulerError实现
SchedulerListener.schedulerError()
接口,对ACQUIRED
状态的数据进行回滚或者删除后重新添加;并且启动的时候也执行一下。 - 使用spring的方式,写一个检查的任务状态的任务,对任务状态有误的数据,做回滚处理,或者删除后重新添加。
- 因为出问题的根本原因,还是和数据简历连接的问题,可以将状态维护的方式,改成内存维护。
🚫上面的解决方案都是为了弥补任务状态出错的补偿修复机制,没从根本上解决数据库
read time out
的问题。并且这个问题,出现在任何和数据库交互的地方。现在被国产环境搞得有点难受🏃♂️🤶大佬们对数据库偶尔出现
read time out
的问题,有好的解决思路吗?欢迎留言评论👏🏋️♀️后面我也会对数据库这个问题,持续跟进,解决之后也会有博客更新 欢迎各位大佬的持续关注