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

Quartz如何实现分布式调度

系列文章目录

任务调度管理——Quartz入门


在这里插入图片描述

我们都说Quartz是个分布式调度框架,那么在分布式环境上,如何使得各个服务器上的定时任务能够做到有效分配?这就是我们今天的内容

📕作者简介:战斧,从事金融IT行业,有着多年一线开发、架构经验;爱好广泛,乐于分享,致力于创作更多高质量内容
📗本文收录于 Quartz专栏 ,有需要者,可直接订阅专栏实时获取更新
📘高质量专栏 云原生、RabbitMQ、Spring全家桶、 GIT 等仍在更新,欢迎指导
📙Zookeeper Redis kafka docker netty等诸多框架,以及架构与分布式专题即将上线,敬请期待


一、持久化

如果看过我们之前的入门文章 :任务调度管理——Quartz入门 ,应该还没有忘记,Quartz 的三个组件:

  • Scheduler 调度器
  • Job 作业
  • Trigger 触发器

在我们的示例中,我们并没有指定其保存在哪,而 Quartz 其实已经提供了存储的SPI。(不知道什么是SPI的可以看这篇:迷迷糊糊?似懂非懂?一文让你从此对SPI了如指掌)

在这里插入图片描述

我们从持久化角度来划分,一种是像 RAMJobStore 这样把任务存在内存中,一旦重启,关于任务执行的情况就丢失了。一种是如JobStoreTX,把任务执行情况及时记录在数据库中,进行持久化。

Quartz 默认使用的是 RAMJobStore, 即内存存储,需要特殊指定的话,只要在配置文件中指定类名即可,比如我们想用JobStoreTX:

org.quartz.jobstore.class=org.quartz.impl.jdbcjobstore.JobStoreTX

而此时我们当然还需要为quartz配置数据库,此时使用 org.quartz.dataSource 来指定一个数据源,如下

org.quartz.dataSource=myDS

org.quartz.dataSource.myDS.driver=com.mysql.cj.jdbc.Driver
org.quartz.dataSource.myDS.URL=jdbc:mysql://localhost:3306/mydb
org.quartz.dataSource.myDS.user=username
org.quartz.dataSource.myDS.password=password

完成以上配置后,我们就能让任务调度信息持久化了,这也是我们实现分布式控制的基础,注意,此时还需要在数据库中增加对应的表,这些新增表的语句在jar包已经有了。

在这里插入图片描述

二、分布式调度

我们都知道分布式服务在现代的应用中是非常重要的,但是针对分布式进行任务调度该怎么控制呢?比如说我们需要每五分钟执行一个发邮件的任务,在分布式情况下,很多的服务上都有这个定时任务,但我们只需要其中一个服务来发送就好了。

因此我们很自然地想到,可以使用这种竞争执行的方式,来保证到了某个特定的时刻,有且仅有一个服务能执行发邮件的操作。如果要达到这样的效果,首先就得存储着一个共享资源,然后还要支持排他锁,这样才能产生预期的竞争
在这里插入图片描述

1. 表信息

想要实现竞争,首先就得要有一个可供竞争的排他锁,因此我们就得先分析各个表的作用。

表名表作用
QRTZ_JOB_DETAILS存储每个Job的详细信息,包括Job的名称、所属组、描述、Job类的名称等
QRTZ_TRIGGERS存储每个触发器的详细信息,包括触发器的名称、所属组、描述、触发器的类型、触发器的表达式、触发时间等
QRTZ_SIMPLE_TRIGGERS存储使用SimpleTrigger触发器的相关信息,包括触发器的名称、所属组、重复次数、间隔时间等
QRTZ_CRON_TRIGGERS存储使用CronTrigger触发器的相关信息,包括触发器的名称、所属组、Cron表达式等
QRTZ_JOB_LISTENERS存储与Job相关的监听器信息
QRTZ_TRIGGER_LISTENERS存储与触发器相关的监听器信息
QRTZ_CALENDARS存储Quartz框架中定义的日历信息
QRTZ_LOCKS用于实现集群环境下的锁机制,确保只有一个服务器在执行调度任务

其中 QRTZ_TRIGGERS、QRTZ_JOB_DETAILS 存储的就是我们之前提到过的三要素的两个:触发器以及作业。而我们主要关注两张表的信息。

QRTZ_LOCKS 表里的字段
SCHED_NAME:调度器名称。
LOCK_NAME:为调度器上的锁的名字,如果要获取TRIGGER,就得先上”TRIGGER_ACCESS“

QRTZ_TRIGGERS 表里和调度紧密相关的有下面几个核心字段:

TRIGGER_NAME:触发器的名称。
JOB_NAME:与触发器关联的作业的名称。
NEXT_FIRE_TIME:下一次触发的时间,以毫秒为单位。
PREV_FIRE_TIME:上一次触发的时间,以毫秒为单位。
TRIGGER_STATE:触发器的状态,例如已暂停(PAUSED)、待执行(WAITING)、已完成(COMPLETE)等。

2. 调度器的竞争

调度器竞争会利用到 QRTZ_LOCKS 表, 比如服务器试图获取某个调度器下的触发器,那就得先获取到调度器的锁,每个调度器有两把锁。一个是STATE_ACCESS, 一个是TRIGGER_ACCESS

  • STATE_ACCESS:当一个线程要访问或修改作业状态时,它会尝试获取STATE_ACCESS字段的锁。
  • TRIGGER_ACCESS:当一个线程要访问或修改触发器状态时,它会尝试获取TRIGGER_ACCESS字段的锁。

在这里插入图片描述
而抢占锁使用的语句为:

SELECT * FROM QRTZ_LOCKS WHERE SCHED_NAME = 'MyScheduler' AND LOCK_NAME = ? FOR UPDATE

即一个select … for update 的句式,这种悲观锁可以保证了仅有先拿到了锁,才有资格进行后面的动作。

3. 触发器的分配

而对于每一台服务器而言,获得了调度去的TRIGGER的使用权后,接下来就是触发器的分配,即各个分布式服务去竞争触发器。那我们就来看看其具体的过程。

在这里插入图片描述

简单来说,就是Quarts内置两个列表,一个是当前可用线程的列表,一个是马上要触发的触发器列表。然后两边在循环体中完成匹配,以达到为线程分配触发器,进而执行其任务的目的。

我们看到,在整个逻辑中,似乎并没有对于触发器的竞争。其实这里是利用了更新该条触发器状态为获得状态这条SQL语句来实现的排他锁。其原始语句如下

UPDATE QRTZ_TRIGGERS SET TRIGGER_STATE = ? WHERE SCHED_NAME = {1} AND TRIGGER_NAME = ? AND TRIGGER_GROUP = ? AND TRIGGER_STATE = ?

可以看到where 后的筛选条件里,除了触发器名称,还带了触发器的状态。这意味着如果某条触发器已经被其他人修改(意味着被别人抢到了),这次更新条数就为0了,而如果更新条数为0,代码上就会跳过该触发器。说白了,就是利用了数据库UPDATE的互斥特性

三、 总结

上面我们介绍了 Quartz 在分布式环境下,如何保持一个触发器,仅有一个服务器的一个线程能获取到,服务器级别使用了QRTZ_LOCKS里面的行悲观锁来竞争。而对于一台服务器内的触发器-线程的分配则通过 QRTZ_TRIGGERS 的更新语句是否执行成功来判断是否获取触发器成功。


http://www.kler.cn/a/471616.html

相关文章:

  • 【形式篇】年终总结怎么写:PPT如何将内容更好地表现出来
  • 【竞技宝】CS2:HLTV2024职业选手排名TOP8-broky
  • 外驱功率管电流型PWM控制芯片CRE6281B1
  • 搭建企业AI助理的创新应用与案例分析
  • 怎么管理电脑usb接口,分享四种USB端口管理方法
  • HTML+CSS+JS制作中华传统文化主题网站(内附源码,含5个页面)
  • 4. 多线程(2)---线程的状态和多线程带来的风险
  • 如何用代码提交spark任务并且获取任务权柄
  • 大数据技术(八)—— HBase数据读写流程和Api的使用
  • uniapp打包到宝塔并发布
  • 使用python将自己的程序封装成API
  • 使用Python实现医疗物联网设备:构建高效医疗监测系统
  • 快速排序进阶版(加入插入排序提高其性能)
  • 【代码随想录】刷题记录(93)-无重叠区间
  • Requests-数据解析bs4+xpath
  • UWB实操:用信号分析仪(频谱分析仪)抓取UWB频域的图像
  • 【JMeter】多接口关联
  • es 3期 第22节-Bucket特殊分桶聚合实战
  • 【往届已EI检索】第五届智慧城市工程与公共交通国际学术会议(SCEPT 2025)
  • 在 PhpStorm 中配置命令行直接运行 PHP 的步骤
  • 后端开发入门超完整速成路线(算法篇)
  • 计算机网络:无线网络
  • 矩阵和向量点乘叉乘元素乘
  • ue5 替换角色的骨骼网格体和动画蓝图
  • 计算机网络之---计算机网络的性能评估
  • Redis中的主从/Redis八股