Quartz如何实现分布式调度
系列文章目录
任务调度管理——Quartz入门
Quartz如何实现分布式控制
- 系列文章目录
- 一、持久化
- 二、分布式调度
- 1. 表信息
- 2. 调度器的竞争
- 3. 触发器的分配
- 三、 总结
我们都说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 的更新语句是否执行成功来判断是否获取触发器成功。