坑爹!Quartz 重复调度问题,你遇到过么?(1)

简介: 坑爹!Quartz 重复调度问题,你遇到过么?

1. 引子

公司前期改用quartz做任务调度,一日的调度量均在两百万次以上。随着调度量的增加,突然开始出现job重复调度的情况,且没有规律可循。网上也没有说得较为清楚的解决办法,于是我们开始调试Quartz源码,并最终找到了问题所在。


如果没有耐性看完源码解析,可以直接拉到文章最末,有直接简单的解决办法。 注:本文中使用的quartz版本为2.3.0,且使用JDBC模式存储Job。


2. 准备

首先,因为本文是代码级别的分析文章,因而需要提前了解Quartz的用途和用法,网上还是有很多不错的文章,可以提前自行了解。


其次,在用法之外,我们还需要了解一些Quartz框架的基础概念:


1)Quartz把触发job,叫做fire。TRIGGER_STATE是当前trigger的状态,PREV_FIRE_TIME是上一次触发时间,NEXT_FIRE_TIME是下一次触发时间,misfire是指这个job在某一时刻要触发,却因为某些原因没有触发的情况。


2)Quartz在运行时,会起两类线程(不止两类),一类用于调度job的调度线程(单线程),一类是用于执行job具体业务的工作池。


3)Quartz自带的表里面,本文主要涉及以下3张表:


triggers表。triggers表里记录了,某个trigger的PREV_FIRE_TIME(上次触发时间),NEXT_FIRE_TIME(下一次触发时间),TRIGGER_STATE(当前状态)。虽未尽述,但是本文用到的只有这些。

locks表。Quartz支持分布式,也就是会存在多个线程同时抢占相同资源的情况,而Quartz正是依赖这张表,处理这种状况,至于如何做到,参见3.1。

fired_triggers表,记录正在触发的triggers信息。

4)TRIGGER_STATE,也就是trigger的状态,主要有以下几类:


image.png

trigger的初始状态是WAITING,处于WAITING状态的trigger等待被触发。调度线程会不停地扫triggers表,根据NEXT_FIRE_TIME提前拉取即将触发的trigger,如果这个trigger被该调度线程拉取到,它的状态就会变为ACQUIRED。


因为是提前拉取trigger,并未到达trigger真正的触发时刻,所以调度线程会等到真正触发的时刻,再将trigger状态由ACQUIRED改为EXECUTING。


如果这个trigger不再执行,就将状态改为COMPLETE,否则为WAITING,开始新的周期。如果这个周期中的任何环节抛出异常,trigger的状态会变成ERROR。如果手动暂停这个trigger,状态会变成PAUSED。


3. 开始排查

3.1分布式状态下的数据访问

前文提到,trigger的状态储存在数据库,Quartz支持分布式,所以如果起了多个quartz服务,会有多个调度线程来抢夺触发同一个trigger。mysql在默认情况下执行select 语句,是不上锁的,那么如果同时有1个以上的调度线程抢到同一个trigger,是否会导致这个trigger重复调度呢?我们来看看,Quartz是如何解决这个问题的。


首先,我们先来看下JobStoreSupport类的executeInNonManagedTXLock()方法:

image.png


这个方法的官方介绍:

/**
*Execute the given callback having acquired the given lock.
*Depending on the JobStore,the surrounding transaction maybe
*assumed to be already present(managed).
*
*@param lockName The name of the lock to acquire,for example
*"TRIGGER_ACCESS".If null, then no lock is acquired ,but the
*lockCallback is still executed in a transaction.
*/

也就是说,传入的callback方法在执行的过程中是携带了指定的锁,并开启了事务,注释也提到,lockName就是指定的锁的名字,如果lockName是空的,那么callback方法的执行不在锁的保护下,但依然在事务中。


这意味着,我们使用这个方法,不仅可以保证事务,还可以选择保证,callback方法的线程安全。


接下来,我们来看一下executeInNonManagedTXLock(…)中的obtainLock(conn,lockName)方法,即抢锁的过程。这个方法是在Semaphore接口中定义的,Semaphore接口通过锁住线程或者资源,来保护资源不被其他线程修改,由于我们的调度信息是存在数据库的,所以现在查看DBSemaphore.java中obtainLock方法的具体实现:


image.png


我们通过调试查看expandedSQL和expandedInsertSQL这两个变量:


image.png


图3-3可以看出,obtainLock方法通过locks表的一个行锁(lockName确定)来保证callback方法的事务和线程安全。拿到锁后,obtainLock方法将lockName写入threadlocal。当然在releaseLock的时候,会将lockName从threadlocal中删除。


总而言之,executeInNonManagedTXLock()方法,保证了在分布式的情况,同一时刻,只有一个线程可以执行这个方法。


相关文章
|
存储 Linux 数据安全/隐私保护
安装部署milvus单机版(快速体验)
安装部署milvus单机版(快速体验)
3387 0
|
5月前
|
开发框架 定位技术 API
AgentScope 与 MCP:实践、思考与展望
AgentScope 作为一款功能强大的开源多智能体开发框架,为开发者提供了智能体构建、工具使用、多智能体编排等全方位支持。
648 37
|
6月前
|
安全
区分ConnectTimeout错误和ConnectionError异常在使用中的不同。
总结一下,这两个问题就像是你试图拨打一个电话。ConnectTimeout错误就好比你拨通了电话,但另一端没有接听;ConnectionError异常则好比你的电话根本没法拨出去,或者你拨错了号码——甚至是你根本就没拿到电话。所以,在下一次遇到类似问题的时候,希望你能想起这个比喻,然后灵活应对。
289 34
|
11月前
|
Java Maven
Maven编译报错:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.13.0:compile 解决方案
在执行Maven项目中的`install`命令时,遇到编译插件版本不匹配的错误。具体报错为:`maven-compiler-plugin:3.13.0`要求Maven版本至少为3.6.3。解决方案是将Maven版本升级到3.6.3或降低插件版本。本文详细介绍了如何下载、解压并配置Maven 3.6.3,包括环境变量设置和IDEA中的Maven配置,确保项目顺利编译。
12181 5
Maven编译报错:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.13.0:compile 解决方案
|
消息中间件 NoSQL Java
Redis系列学习文章分享---第六篇(Redis实战篇--Redis分布式锁+实现思路+误删问题+原子性+lua脚本+Redisson功能介绍+可重入锁+WatchDog机制+multiLock)
Redis系列学习文章分享---第六篇(Redis实战篇--Redis分布式锁+实现思路+误删问题+原子性+lua脚本+Redisson功能介绍+可重入锁+WatchDog机制+multiLock)
509 0
|
运维 Linux Apache
如何使用`systemctl status`命令来查看服务状态?
如何使用`systemctl status`命令来查看服务状态?
1354 0
|
前端开发 小程序 Java
【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅
本文详细介绍了如何在SpringBoot项目中统一处理接口返回结果及全局异常。首先,通过封装`ResponseResult`类,实现了接口返回结果的规范化,包括状态码、状态信息、返回信息和数据等字段,提供了多种成功和失败的返回方法。其次,利用`@RestControllerAdvice`和`@ExceptionHandler`注解配置全局异常处理,捕获并友好地处理各种异常信息。
6343 0
【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅
|
IDE Java 编译器
lombok编译遇到“找不到符号的问题”
【9月更文挑战第18天】当使用 Lombok 遇到 “找不到符号” 的问题时,可能是由于 Lombok 未正确安装、编译器不支持、IDE 配置不当或项目构建工具配置错误。解决方法包括确认 Lombok 安装、编译器支持,配置 IDE 和检查构建工具配置。通过这些步骤通常可解决问题,若问题仍存在,建议检查项目配置和依赖,或查看日志获取更多信息。
5253 2
|
消息中间件 缓存 Kafka
图解Kafka:架构设计、消息可靠、数据持久、高性能背后的底层原理
【8月更文挑战第15天】在构建高吞吐量和高可靠性的消息系统时,Apache Kafka 成为了众多开发者和企业的首选。其独特的架构设计、消息可靠传输机制、数据持久化策略以及高性能实现方式,使得 Kafka 能够在分布式系统中大放异彩。本文将通过图解的方式,深入解析 Kafka 的这些核心特性,帮助读者更好地理解和应用这一强大的消息中间件。
704 0
|
消息中间件 数据采集 安全
Python教程:深入理解 Python multiprocessing.Queue
在 Python 编程中,多进程编程是利用计算机多核心资源的重要手段之一。然而,多进程之间的通信却是一个相对复杂的问题。在本文中,我们将深入探讨 Python 中的 multiprocessing.Queue 模块,它为多进程间的通信提供了便捷而高效的解决方案。
364 5