【Quartz】Quartz集群原理及其实现

众所周知,Quartz是springboot的御用定时框架,在实际的应用开发中,如果任务数量比较多,单机版的quartz无法满足业务需求,这时可以搭建集群,让更多的节点来分担任务,提供系统的吞吐能力。

但是集群里面的quartz节点之间是无法通信的,如何通信呢?可以通过数据库,让节点访问同一个数据库,数据库中记录集群的信息,来保证节点直接互相通信。搭建集群需要配置文件spring-quartz.propertites。以及数据库sql。下面我们来介绍以下Quartz集群原理及其实现。

Quartz集群架构

Quartz集群每个节点是一个独立的Quartz微服务应用,独立的Quartz节点并不与另一个节点通信。Quartz应用是通过共有相同数据库表来感知到另一应用。 也就是说只有使用持久化JobStore存储Job和Trigger才能完成Quartz集群,简单来说就是,在集群环境下,通过数据库锁机制来实现定时任务的执行。

Quartz集群原理

Quartz的集群部署方案是分布式的,没有负责集中管理的节点,而是利用数据库行锁的方式来实现集群环境下的并发控制。

向Mysql获取行锁的语句:

sql 复制代码
select * from {0}LOCKS where sched_name = ? and lock_name = ? for update

{0}会替换为配置文件默认配置的**QRTZ_**。sched_name为应用集群的实例名,lock_name就是行级锁名。Quartz主要由两个行级锁。

lock_name desc
STATE_ACCESS 状态访问锁
TRIGGER_ACCESS 触发器访问锁

同一集群下,instanceName必须相同,instanceId可自动生成,isClustered为true,持久化存储。

总结:

  1. 数据库存储:Quartz集群使用一个共享的数据库来存储任务和调度信息。每个Quartz实例都连接到同一个数据库,并共享这些任务和调度信息,以确保任务的一致性和可靠性。
  2. 选举机制:在Quartz集群中,每个实例都有一个唯一的标识符,称为实例ID。当一个Quartz实例启动时,它会尝试成为集群的主节点。如果没有当前的主节点,那么该实例将成为主节点。如果已经有主节点存在,那么该实例将成为备用节点,并等待主节点故障时接管。
  3. 心跳检测:每个Quartz实例都定期发送心跳信号给其他实例,以保持集群的健康状态。如果一个实例在一段时间内没有收到其他实例的心跳信号,那么它会认为主节点已经故障,并尝试成为新的主节点。

Quartz数据库表简介

Quartz集群依赖于数据库,所以必须首先创建Quartz数据库表。Quartz发布包中包括了所有被支持的数据库平台的SQL脚本,版本:2.3.0, SQL文件是在这个路径下:quartz-2.3.0-SNAPSHOT\src\org\quartz\impl\jdbcjobstore\tables_mysql.sql,总共11张表。

|--------------------------|----------------------------------------------------|
| 表名 | 表描述 |
| QRTZ_JOB_DETAILS | 存储作业详细信息,包括作业名称、作业组、描述、作业类名等。 |
| QRTZ_TRIGGERS | 存储触发器详细信息,包括触发器名称、触发器组、开始时间、结束时间、重复间隔等 |
| QRTZ_SIMPLE_TRIGGERS | 存储简单触发器的额外信息 |
| QRTZ_CRON_TRIGGERS | 存储Cron触发器的额外信息 |
| QRTZ_BLOB_TRIGGERS | 存储BLOB触发器的额外信息 |
| QRTZ_CALENDARS | 存储日历信息,用于作业调度的时间约束 |
| QRTZ_PAUSED_TRIGGER_GRPS | 存储暂停的触发器组 |
| QRTZ_FIRED_TRIGGERS | 存储已触发的触发器信息 |
| QRTZ_SCHEDULER_STATE | 存储调度器状态信息,包括当前的主节点信息等 |
| QRTZ_LOCKS | 存储锁信息,用于保证数据一致性和防止竞态条件 |
| QRTZ_SIMPROP_TRIGGERS | 存储CalendarIntervalTrigger和DailyTimeIntervalTrigger |

Quartz集群实现方式

引入依赖

XML 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>

定义job

java 复制代码
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.PersistJobDataAfterExecution;
import org.quartz.SchedulerException;
import org.springframework.scheduling.quartz.QuartzJobBean;

import java.util.Date;

/**
 * @description:
 * @author: 黎剑
 * @create: 2024-04-13 22:58
 **/
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class QuartzJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        try {
            Thread.sleep(2000);
            System.out.println(context.getScheduler().getSchedulerInstanceId());
            System.out.println("taskName=" + context.getJobDetail().getKey().getName());
            System.out.println("执行时间:" + new Date());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}

配置scheduler

java 复制代码
import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

import javax.sql.DataSource;
import java.io.IOException;
import java.util.Properties;
import java.util.concurrent.Executor;

/**
 * @description: 调度器配置
 * @author: 黎剑
 * @create: 2024-04-13 23:04
 **/
@Configuration
public class SchedulerConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public Scheduler scheduler() throws IOException {
        return schedulerFactoryBean().getScheduler();
    }

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setSchedulerName("cluster_scheduler");
        // 注入数据源
        factory.setDataSource(dataSource);
        // 选填
        factory.setApplicationContextSchedulerContextKey("application");
        factory.setQuartzProperties(quartzProperties());
        // 读线程池配置
        factory.setTaskExecutor(schedulerThreadPool());
        // 等待设置其他属性
        return factory;
    }

    @Bean
    public Properties quartzProperties() throws IOException {
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        propertiesFactoryBean.setLocation(new ClassPathResource("/spring-quartz.properties"));
        propertiesFactoryBean.afterPropertiesSet();
        return propertiesFactoryBean.getObject();
    }

    @Bean
    public Executor schedulerThreadPool() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        // 处理器的核心数
        taskExecutor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
        // 最大线程数
        taskExecutor.setMaxPoolSize(Runtime.getRuntime().availableProcessors());
        // 容量
        taskExecutor.setQueueCapacity(Runtime.getRuntime().availableProcessors());
        return taskExecutor;
    }
}

监听

java 复制代码
import org.quartz.CronScheduleBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

/**
 * @description:
 * @author: 黎剑
 * @create: 2024-04-13 23:23
 **/
@Component
public class StartApplicationListener implements ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    private Scheduler scheduler;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 开启调度
        // 可以先判断触发器是否存在
        /**
         * JobDetail有一个类型为JobKey的重要属性key,相当于是该任务的键值,JobDetail注册到任务调度器Schedule中的时候,key值不允许重复。***整个任务调度过程中,Quartz都是通过Jobkey来唯一识别JobDetail的。试图将重复键值的JobDetail注册到任务调度器中而不指定覆盖的话,是不被允许的。***
         *
         * JobKey可以通过JobBuiler的withIdentity方法指定,该方法接收name或name+group参数,从而唯一确定一个任务JobDetail。
         *
         * 如果在JobDetail创建过程中不指定JobKey的话,Quartz会通过UUID的方式为该任务生成一个唯一的key值。
         *
         * ***所以,同一个Job实现类(也就是同一个任务),可以通过不同的JobKey值注册到任务调度器中、绑定不同的触发器执行!
         */
        TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", "group1");
        try {
            Trigger trigger = scheduler.getTrigger(triggerKey);
            if (trigger == null) {
                // 触发器
                TriggerBuilder.newTrigger().withIdentity(triggerKey)
                        .withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?")).build();
                //
                JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class)
                        .withIdentity("job1", "group1")
                        .build();
                scheduler.scheduleJob(jobDetail, trigger);
                scheduler.start();
            }
        } catch (SchedulerException e) {
            e.printStackTrace();
        }


    }
}

application.properties配置:

XML 复制代码
# 应用服务 WEB 访问端口
server.port=8080

spring.datasource.url=jdbc:mysql://localhost:3306/db_quartz?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC

spring.datasource.username=root
spring.datasource.password=lijian
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.max-active=1000
spring.datasource.max-idle=20
spring.datasource.min-idle=5
spring.datasource.initial-size=10

spring-quartz.properties配置:

XML 复制代码
#============================================================================
# 配置JobStore
#============================================================================
# JobDataMaps是否都为String类型,默认false
# 若是true的话,便可不用让更复杂的对象以序列化的形式保存到BLOB列中
org.quartz.jobStore.useProperties=false

# 存储相关信息表的前缀,默认QRTZ_
org.quartz.jobStore.tablePrefix = QRTZ_

# 是否加入集群
#是否是应用在集群中,当应用在集群中时必须设置为TRUE,否则会出错。
#如果有多个Quartz实例在用同一套数据库时,必须设置为true。
org.quartz.jobStore.isClustered = true

# 调度实例失效的检查时间间隔 ms
#只用于设置了isClustered为true的时候,设置一个频度(毫秒),用于实例报告给集群中的其他实例。
#这会影响到侦测失败实例的敏捷度。
org.quartz.jobStore.clusterCheckinInterval = 5000

# 当设置为"true"时,此属性告诉Quartz 在非托管JDBC连接上调用setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED)。
org.quartz.jobStore.txIsolationLevelReadCommitted = true

# 数据保存方式为数据库持久化,选择JDBC的存储方式
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

# 数据库代理类,一般org.quartz.impl.jdbcjobstore.StdJDBCDelegate可以满足大部分数据库
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

#这个值必须datasource的配置信息
org.quartz.jobStore.dataSource = myDataSource

#最大能忍受的触发超时时间,如果超时则认为"失误"
org.quartz.jobStore.misfireThreshold = 60000

#这是JobStore能处理的错过触发的Trigger的最大数量。处理太多(2打)很快就会导致数据库表被锁定够长的时间,
#这样会妨碍别的(还未错过触发)trigger执行的性能。
org.quartz.jobStore.maxMisfiresToHandleAtATime=20

#设置这个参数为true会告诉Quartz从数据源获取连接后不要调用它的setAutoCommit(false)方法。
#在少数情况下是有用的,比如有一个驱动本来是关闭的,但是又调用这个关闭的方法。但是大部分情况下驱动都要求调用setAutoCommit(false)
org.quartz.jobStore.dontSetAutoCommitFalse=false

#这必须是一个从LOCKS表查询一行并对这行记录加锁的SQL。假设没有设置,默认值如下。
#{0}会在运行期间被前面配置的TABLE_PREFIX所代替
org.quartz.jobStore.selectWithLockSQL=SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE

#值为true时告知Quartz(当使用JobStoreTX或CMT)调用JDBC连接的setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) 方法。这有助于某些数据库在高负载和长时间事务时锁的超时。
org.quartz.jobStore.txIsolationLevelSerializable=false

#============================================================================
# Scheduler 调度器属性配置
#============================================================================
# 调度标识名 集群中每一个实例都必须使用相同的名称,可以为任意字符串,对于scheduler来说此值没有意义,但是可以区分同一系统中多个不同的实例
org.quartz.scheduler.instanceName = ClusterQuartz
# ID设置为自动获取 每一个必须不同
org.quartz.scheduler.instanceId= AUTO

org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false

# 默认false,若是在执行Job之前Quartz开启UserTransaction,此属性应该为true。
#Job执行完毕,JobDataMap更新完(如果是StatefulJob)事务就会提交。默认值是false,可以在job类上使用@ExecuteInJTATransaction注解,
# 以便在各自的job上决定是否开启JTA事务。
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
#一个scheduler节点允许接收的trigger的最大数,默认是1,这个值越大,定时任务执行的越多,但代价是集群节点之间的不均衡。
org.quartz.scheduler.batchTriggerAcquisitionMaxCount=1


#============================================================================
# 配置ThreadPool
#============================================================================
# 线程池的实现类(一般使用SimpleThreadPool即可满足几乎所有用户的需求)
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool

# 指定线程数,一般设置为1-100直接的整数,根据系统资源配置
org.quartz.threadPool.threadCount = 10

# 设置线程的优先级(可以是Thread.MIN_PRIORITY(即1)和Thread.MAX_PRIORITY(这是10)之间的任何int 。默认值为Thread.NORM_PRIORITY(5)。)
org.quartz.threadPool.threadPriority = 5

#加载任务代码的ClassLoader是否从外部继承
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread= true

#是否设置调度器线程为守护线程
org.quartz.scheduler.makeSchedulerThreadDaemon=true
相关推荐
Ai 编码助手4 小时前
MySQL中distinct与group by之间的性能进行比较
数据库·mysql
陈燚_重生之又为程序员5 小时前
基于梧桐数据库的实时数据分析解决方案
数据库·数据挖掘·数据分析
caridle5 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
白云如幻5 小时前
MySQL排序查询
数据库·mysql
萧鼎5 小时前
Python并发编程库:Asyncio的异步编程实战
开发语言·数据库·python·异步
^velpro^5 小时前
数据库连接池的创建
java·开发语言·数据库
荒川之神5 小时前
ORACLE _11G_R2_ASM 常用命令
数据库·oracle
IT培训中心-竺老师5 小时前
Oracle 23AI创建示例库
数据库·oracle
小白学大数据5 小时前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
time never ceases6 小时前
使用docker方式进行Oracle数据库的物理迁移(helowin/oracle_11g)
数据库·docker·oracle