Quartz集群配置与原理解析

定时任务的Solution:  

(1)每天晚上的定时任务,通过sql脚本 + crontab方式执行 
  1. #crm  
  2. 0 2 * * * /opt/***/javafiles/***/shell/***_daily_stat.sql  
  3. 30 7 * * * /opt/***/javafiles/***/shell/***_data_fix  
  4. 30 0 * * * /opt/***/javafiles/***/shell/***_sync_log  
  5. 0 1 * * * /opt/***/javafiles/***/shell/***_clear_log  
  6. 20 8 * * * /opt/***/javafiles/***/shell/***_daily >> /var/***/logs/***_daily.log 2>&1  
  7. 40 1 * * * /opt/***/javafiles/***/shell/***_sync_account2  
  8. 0 2 * * 1 /opt/***/javafiles/***/shell/***_weekly >> /var/***/logs/***_weekly.log 2>&1  
存在的问题:当需要跨库或许数据的,sql无能为力,引入许多中间表,完成复杂统计需求。大范围对线上热表扫描,造成锁表,延迟严重 
(2)使用python(多数据源) + SQL的方式 
  1. def connectCRM():  
  2.     return MySQLdb.Connection("localhost", "***", "***", "***", 3306, charset="utf8")  
  3.   
  4. def connectTemp():  
  5.     return MySQLdb.Connection("localhost", "***", "***",  "***", 3306, charset="utf8")  
  6.   
  7. def connectOA():  
  8.     return MySQLdb.Connection("localhost", "***", "***",  "***", 3306, charset="utf8")  
  9.   
  10. def connectCore():  
  11.     return MySQLdb.Connection("localhost", "***", "***",  "***", 3306, charset="utf8")  
  12.   
  13. def connectCT():  
  14.     return MySQLdb.Connection("localhost", "***", "***", "***", 3306, charset="utf8")  
存在的问题:直接访问数据,需要理解各系统的数据结构,无法满足动态任务问题,各系统业务接口没有重用 
(3)使用spring + JDK timer方式调用接口完成定时任务 
  1. <bean id="accountStatusTaskScanner"  class="***.impl.AccountStatusTaskScanner" />  
  2.     <task:scheduler id="taskScheduler" pool-size="5" />  
  3.     <task:scheduled-tasks scheduler="taskScheduler">  
  4.     <task:scheduled ref="accountStatusTaskScanner" method="execute" cron="0 0 1 * * ?" />  
  5. </task:scheduled-tasks>  
使用写死服务器Host(srv23)的方式,控制只在一台服务器上执行task 
  1. public abstract class SingletonServerTaskScanner implements TaskScanner {  
  2.     private final Logger logger = LoggerFactory.getLogger(SingletonServerTaskScanner.class);  
  3.     @Override  
  4.     public void execute() {  
  5.         String hostname = "";  
  6.         try {  
  7.             hostname = InetAddress.getLocalHost().getHostName();  
  8.         } catch (UnknownHostException e) {  
  9.             logger.error(e.getMessage(), e);  
  10.         }  
  11.         //判断是否为当前可执行服务器  
  12.         if (ConfigUtil.getValueByKey("core.scan.server").equals(hostname)) {  
  13.             doScan();  
  14.         }  
  15.     }  
  16.     public abstract void doScan();  
  17. }  

//对于srv23的重启,保存在内存中的任务将丢失,每次重启srv23重新生成定时任务 

  1. public class CrmInitializer implements InitializingBean {  
  2.     private Logger logger = LoggerFactory.getLogger(CrmInitializer.class);  
  3.     @Override  
  4.     public void afterPropertiesSet() throws Exception {  
  5.         // 扫描商家状态,创建定时任务  
  6.         logger.info("扫描商家状态,创建定时任务");  
  7.         accountStatusTaskScanner.execute();  
  8.         // 扫描N天未拜访商家,创建定时任务  
  9.         logger.info("扫描N天未拜访商家,创建定时任务");  
  10.         nDaysActivityScanner.execute();  
  11.     }  
  12. }  
  1. //通过调用srv23的特定URL的方式,动态指定任务(如取消N天未拜访,私海进保护期,保护期进公海等)  
  2. public class SingletonServerTaskController {  
  3.         @Resource  
  4.         private AccountService accountService;  
  5.         @RequestMapping(value = "/reschedule")  
  6.         public @ResponseBody  
  7.             String checkAndRescheduleAccount(Integer accountId) {  
  8.             logger.debug("reschedule task for accountId:" + accountId);  
  9.             if (isCurrentServer()) {  
  10.                 accountService.checkAndRescheduleAccount(Arrays.asList(accountId));  
  11.             }  
  12.             return "ok";  
  13.         }  
  14.     private boolean isCurrentServer() {  
  15.         String hostname = "";  
  16.         try {  
  17.             hostname = InetAddress.getLocalHost().getHostName();  
  18.         } catch (UnknownHostException e) {  
  19.             logger.error(e.getMessage(), e);  
  20.         }  
  21.         if (ConfigUtil.getValueByKey("core.scan.server").equals(hostname)) {  
  22.             return true;  
  23.         } else {  
  24.             return false;  
  25.         }  
  26.     }  
  27. }  
存在的问题:实现步骤复杂,分散,任务调度不能恢复,严重依赖于srv23,回调URL时可能失败等情况。 

(4)Quartz:
1)完全由Java写成,设计用于J2SE和J2EE应用.方便集成:JVM,RMI. 
2)设计清晰简单:核心概念scheduler,trigger,job,jobDetail,listener,calendar 
3)支持集群:org.quartz.jobStore.isClustered 
4)支持任务恢复:requestsRecovery 

介绍Quartz集群

项目中使用分布式并发部署定时任务,多台跨JVM,按照常理逻辑每个JVM的定时任务会各自运行,这样就会存在问题,多台分布式JVM机器的应用服务同时干活,一个是加重服务负担,另外一个是存在严重的逻辑问题,比如需要回滚的数据,就回滚了多次,刚好quartz提供很好的解决方案。

集群分布式并发环境中使用QUARTZ定时任务调度,会在各个节点会上报任务,存到数据库中,执行时会从数据库中取出触发器来执行,如果触发器的名称和执行时间相同,则只有一个节点去执行此任务。

如果此节点执行失败,则此任务则会被分派到另一节点执行,中途也会自动检查失效的定时调度,发现不成功的,其他节点立马接过来继续完成定时任务。
Quartz是一个开放源码项目,专注于任务调度器,提供了极为广泛的特性如持久化任务,集群和分布式任务等。 Quartz核心是调度器,还采用多线程管理。

quartz本身事实上也是支持集群的。在这种方案下,cluster上的每一个node都在跑quartz,然后也是通过数据中记录的状态来判断这个操作是否正在执行,这就要求cluster上所有的node的时间应该是一样的。而且每一个node都跑应用就意味着每一个node都需要有自己的线程池来跑quartz。  quartz自身支持的集群方案,在架构上完全是分布式的,没有集中的管理,quratz通过数据库锁以及标识字段保证多个节点对任务不重复获取,并且有负载平衡机制和容错机制,用少量的冗余,换取了高可用性(high avilable HA)和高可靠性.(个人认为和git的机制有异曲同工之处,分布式的冗余设计,换取可靠性和速度).

1.持久化任务:当应用程序停止运行时,所有调度信息不被丢失,当你重新启动时,调度信息还存在,这就是持久化任务(保存到数据库表中)。

2.集群和分布式处理:当在集群环境下,当有配置Quartz的多个客户端时(节点),采用Quartz的集群和分布式处理时,我们要了解几点好处

1) 一个节点无法完成的任务,会被集群中拥有相同的任务的节点取代执行。

2) Quartz调度是通过触发器的类别来识别不同的任务,在不同的节点定义相同的触发器的类别,这样在集群下能稳定的运行,一个节点无法完成的任务,会被集群中拥有相同的任务的节点取代执行。

3)分布式体现在当相同的任务定时在一个时间点,在那个时间点,不会被两个节点同时执行。

Quartz的 Task(11 张表)实例化采用数据库存储,基于数据库引擎及 High-Available 的策略(集群的一种策略)自动协调每个节点的 Quartz。
1)学习Quartz 

 

图1 介绍了quartz关键的组件和简单流程 

quartz的分布式架构如上图,可以看到数据库是各节点上调度器的枢纽.各个节点并不感知其他节点的存在,只是通过数据库来进行间接的沟通.

实际上,quartz的分布式策略就是一种以数据库作为边界资源的并发策略.每个节点都遵守相同的操作规范,使得对数据库的操作可以串行执行.而不同名称的调度器又可以互不影响的并行运行.
简单地说,quartz的分布式调度策略是以数据库为边界资源的一种异步策略.各个调度器都遵守一个基于数据库锁的操作规则保证了操作的唯一性.同时多个节点的异步运行保证了服务的可靠. ,集群特性对于高cpu使用率的任务效果很好,但是对于大量的短任务,各个节点都会抢占数据库锁,这样就出现大量的线程等待资源.这种情况随着节点的增加会越来越严重.

组件间的通讯图如下:(*注:主要的sql语句附在文章最后)

quartz运行时由QuartzSchedulerThread类作为主体,循环执行调度流程。JobStore作为中间层,按照quartz的并发策略执行数据库操作,完成主要的调度逻辑。JobRunShellFactory负责实例化JobDetail对象,将其放入线程池运行。LockHandler负责获取LOCKS表中的数据库锁。

整个quartz对任务调度的时序大致如下:

梳理一下其中的流程,可以表示为:

0.调度器线程run()

1.获取待触发trigger

    1.1数据库LOCKS表TRIGGER_ACCESS行加锁

    1.2读取JobDetail信息

    1.3读取trigger表中触发器信息并标记为"已获取"

    1.4commit事务,释放锁

2.触发trigger

    2.1数据库LOCKS表STATE_ACCESS行加锁

    2.2确认trigger的状态

    2.3读取trigger的JobDetail信息

    2.4读取trigger的Calendar信息

    2.3更新trigger信息

    2.3commit事务,释放锁

3实例化并执行Job

    3.1从线程池获取线程执行JobRunShell的run方法

可以看到,这个过程中有两个相似的过程:同样是对数据表的更新操作,同样是在执行操作前获取锁 操作完成后释放锁.这一规则可以看做是quartz解决集群问题的核心思想.

规则流程图:

进一步解释这条规则就是:一个调度器实例在执行涉及到分布式问题的数据库操作前,首先要获取QUARTZ2_LOCKS表中对应当前调度器的行级锁,获取锁后即可执行其他表中的数据库操作,随着操作事务的提交,行级锁被释放,供其他调度器实例获取.

集群中的每一个调度器实例都遵循这样一种严格的操作规程,那么对于同一类调度器来说,每个实例对数据库的操作只能是串行的.而不同名的调度器之间却可以并行执行。

注意:实际使用时使用jobStoreTX/jobStoreCMT ,StatefulJob,大量的trigger对应一个JobDetail的情况下Mysql会产生锁超时问题. 

中断 Job 
Quartz 包括一个接口叫做 org.quartz.InterruptableJob,它扩展了普通的 Job 接口并提供了一个 interrupt() 方法:
没有深入研究,只知道 Scheduler会调用自定义的Job的 interrupt()方法。由用户决定 Job 决定如何中断.没有测试!!!

job的特性 
易失性 volatility 
一个易失性的 Job 是在程序关闭之后不会被持久化。一个 Job 是通过调用 JobDetail 的 setVolatility(true)被设置为易失. 
Job易失性的默认值是 false. 
注意:只有采用持久性JobStore时才有效 

Job 持久性 durability 
设置JobDetail 的 setDurability(false),在所有的触发器触发之后JobDetail将从 JobStore 中移出。 
Job持久性默认值是false. 
Scheduler将移除没有trigger关联的jobDetail 

Job 可恢复性 shuldRecover 
当一个Job在执行中,Scheduler非正常的关闭,设置JobDetail 的setRequestsRecovery(true) 在 Scheduler 重启之后可恢复的Job还会再次被执行。这个 
Job 会重新开始执行。注意job代码事务特性. 
Job可恢复性默认为false,Scheduler不会试着去恢复job操作。 

 

图为表述没有执行完成的job数据库记录 

quartz与线程 
主处理线程:QuartzSchedulerThread 
启动Scheduler时。QuartzScheduler被创建并创建一个org.quartz.core.QuartzSchedulerThread 类的实例。 
QuartzSchedulerThread 包含有决定何时下一个Job将被触发的处理循环。QuartzSchedulerThread 是一个 Java 线程。它作为一个非守护线程运行在正常优先级下。 

QuartzSchedulerThread 的主处理轮循步骤: 
1. 当 Scheduler 正在运行时: 
A. 检查是否有转换为 standby 模式的请求。 
1. 假如 standby 方法被调用,等待继续的信号 
B. 询问 JobStore 下次要被触发的 Trigger. 
1. 如果没有 Trigger 待触发,等候一小段时间后再次检查 
2. 假如有一个可用的 Trigger,等待触发它的确切时间的到来 
D. 时间到了,为 Trigger 获取到 triggerFiredBundle. 
E. 使用Scheduler和triggerFiredBundle 为 Job 创建一个JobRunShell实例 
F. 在ThreadPool 申请一个线程运行 JobRunShell 实例. 

quartz工作者线程 
Quartz 不会在主线程(QuartzSchedulerThread)中处理用户的Job。Quartz 把线程管理的职责委托给ThreadPool。
一般的设置使用org.quartz.simpl.SimpleThreadPool。SimpleThreadPool 创建了一定数量的 WorkerThread 实例来使得Job能够在线程中进行处理。 
WorkerThread 是定义在 SimpleThreadPool 类中的内部类,它实质上就是一个线程。 
要创建 WorkerThread 的数量以及配置他们的优先级是在文件quartz.properties中并传入工厂。 

spring properties 
  1. <prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPool</prop>  
  2. <prop key="org.quartz.threadPool.threadCount">20</prop>  
  3. <prop key="org.quartz.threadPool.threadPriority">5</prop>  

主线程(QuartzSchedulerThread)请求ThreadPool去运行 JobRunShell 实例,ThreadPool 就检查看是否有一个可用的工作者线 
程。假如所以已配置的工作者线程都是忙的,ThreadPool 就等待直到有一个变为可用。当一个工作者线程是可用的, 
并且有一个JobRunShell 等待执行,工作者线程就会调用 JobRunShell 类的 run() 方法。 


Quartz 数据库结构 
表名描述 
QRTZ_CALENDARS 以 Blob 类型存储 Quartz 的 Calendar 信息 
QRTZ_CRON_TRIGGERS 存储 Cron Trigger,包括 Cron 表达式和时区信息 
QRTZ_FIRED_TRIGGERS 存储与已触发的 Trigger 相关的状态信息,以及相联 Job 的执行信息 
QRTZ_PAUSED_TRIGGER_GRPS 存储已暂停的 Trigger 组的信息 
QRTZ_SCHEDULER_STATE 存储少量的有关 Scheduler 的状态信息,和别的 Scheduler 实例(假如是用于一个集群中) 
QRTZ_LOCKS 存储程序的非观锁的信息(假如使用了悲观锁) 
QRTZ_JOB_DETAILS 存储每一个已配置的 Job 的详细信息 
QRTZ_JOB_LISTENERS 存储有关已配置的 JobListener 的信息 
QRTZ_SIMPLE_TRIGGERS 存储简单的 Trigger,包括重复次数,间隔,以及已触的次数 
QRTZ_BLOG_TRIGGERS Trigger 作为 Blob 类型存储(用于 Quartz 用户用 JDBC 创建他们自己定制的 Trigger 类型,JobStore 并不知道如何存储实例的时候) 
QRTZ_TRIGGER_LISTENERS 存储已配置的 TriggerListener 的信息 
QRTZ_TRIGGERS 存储已配置的 Trigger 的信息 
所有的表默认以前缀QRTZ_开始。可以通过在 quartz.properties配置修改(org.quartz.jobStore.tablePrefix = QRTZ_)。 
可以对不同的Scheduler实例使用多套的表,通过改变前缀来实现。 
选Quartz的团队基本上是冲着Quartz本身实现的集群去的, 不然JDK自带Timer就可以实现相同的功能, 而Timer存在的单点故障是生产环境上所不能容忍的。 在自己造个有负载均衡和支持集群(高可用、伸缩性)的调度框架又影响项目的进度, 所以大多数团队都直接使用了Quartz来作为调度框架。

Quartz集群配置:
    <!-- 调度任务 -->
        <bean id="jobDetail"
                        class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
                        <property name="jobClass" value="全类名" />
                        <property name="durability" value="true"/>
                        <property name="targetMethod" value="execute" />
                        <property name="concurrent" value="true" /> -->
                        <!-- <property name="shouldRecover" value="true" /> -->
        </bean>
        
        <!-- 调度工厂 -->
        <bean id="scheduler" lazy-init="false" autowire="no"
                class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
                
                <!-- 注册JobDetails -->
                <property name="jobDetails">
                        <list>
                                <ref bean="jobDetail"/>
                        </list>
                </property>
                
                <!--可选,QuartzScheduler 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 -->
                <property name="overwriteExistingJobs" value="true"/>
                
                <!-- 属性 -->
                <property name="quartzProperties">
                        <props>
                                <!-- 集群要求必须使用持久化存储 -->
                                <prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreCMT</prop>
                                
                                <prop key="org.quartz.scheduler.instanceName">EventScheduler</prop>
                                <!-- 每个集群节点要有独立的instanceId -->
                                <prop key="org.quartz.scheduler.instanceId">AUTO</prop>
                                
                                <!-- Configure ThreadPool -->
                                <prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPool</prop>
                                <prop key="org.quartz.threadPool.threadCount">50</prop>
                                <prop key="org.quartz.threadPool.threadPriority">5</prop>
                                <prop key="org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread">true</prop>
                                <!-- Configure JobStore -->
                                <prop key="org.quartz.jobStore.misfireThreshold">60000</prop>
                                <prop key="org.quartz.jobStore.driverDelegateClass">org.quartz.impl.jdbcjobstore.StdJDBCDelegate</prop>
                                <prop key="org.quartz.jobStore.tablePrefix">SCHEDULER_</prop>
                                <prop key="org.quartz.jobStore.maxMisfiresToHandleAtATime">10</prop>
                                <!-- 开启集群 -->
                                <prop key="org.quartz.jobStore.isClustered">true</prop>
                                <prop key="org.quartz.jobStore.clusterCheckinInterval">20000</prop>
                                <prop key="org.quartz.jobStore.dontSetAutoCommitFalse">true</prop>
                                <prop key="org.quartz.jobStore.txIsolationLevelSerializable">false</prop>
                                <prop key="org.quartz.jobStore.dataSource">myDS</prop>
                                <prop key="org.quartz.jobStore.nonManagedTXDataSource">myDS</prop>
                                <prop key="org.quartz.jobStore.useProperties">false</prop>
                                <!-- Configure Datasources  -->
                                <prop key="org.quartz.dataSource.myDS.driver">com.mysql.jdbc.Driver</prop>
                                <prop key="org.quartz.dataSource.myDS.URL">${db.url}</prop>
                                <prop key="org.quartz.dataSource.myDS.user">${db.username}</prop>
                                <prop key="org.quartz.dataSource.myDS.password">${db.password}</prop>
                                <prop key="org.quartz.dataSource.myDS.maxConnections">10</prop>
                                <prop key="org.quartz.dataSource.myDS.validationQuery">select 0 from dual</prop>
                        </props>
                </property>
                <property name="applicationContextSchedulerContextKey" value="applicationContext" />
        </bean>



三、 集群源码分析

Quartz如何保证多个节点的应用只进行一次调度(即某一时刻的调度任务只由其中一台服务器执行)?


正如上面架构图所示, Quartz的集群是在同一个数据库下, 由数据库的数据来确定调度任务是否正在执行, 正在执行则其他服务器就不能去执行该行调度数据。 这个跟很多项目是用Zookeeper做集群不一样, 这些项目是靠Zookeeper选举出来的的服务器去执行, 可以理解为Quartz靠数据库选举一个服务器来执行。


如果之前看过这篇 Quartz按时启动原理就应该了解到Quartz最主要的一个类QuartzSchedulerThread职责是触发任务, 是一个不断运行的Quartz主线程, 还是从这里入手了解集群原理。 


集群配置里面有一个配置项:

<prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreCMT</prop>
源码可以看到JobStoreCMT extends JobStoreSupport, 在QuartzSchedulerThread的run方法里面调用的acquireNextTriggers、 triggersFired、 releaseAcquiredTrigger方法都进行了加锁处理。

以acquireNextTriggers为例:



而LOCK_TRIGGER_ACCESS其实就是一个JAVA常量

protected static final String LOCK_TRIGGER_ACCESS = "TRIGGER_ACCESS";


这个常量传入加锁的核心方法executeInNonManagedTXLock: 处理逻辑前获取锁, 处理完成后在finally里面释放锁(一种典型的同步处理方法)

 
protected <T> T executeInNonManagedTXLock(
            String lockName, 
            TransactionCallback<T> txCallback, final TransactionValidator<T> txValidator) throws JobPersistenceException {
        boolean transOwner = false;
        Connection conn = null;
        try {
            if (lockName != null) {
                // If we aren't using db locks, then delay getting DB connection 
                // until after acquiring the lock since it isn't needed.
                if (getLockHandler().requiresConnection()) {
                    conn = getNonManagedTXConnection();
                }
                // 获取锁
                transOwner = getLockHandler().obtainLock(conn, lockName);
            }
            
            if (conn == null) {
                conn = getNonManagedTXConnection();
            }
            
            final T result = txCallback.execute(conn);
            try {
                commitConnection(conn);
            } catch (JobPersistenceException e) {
                rollbackConnection(conn);
                if (txValidator == null || !retryExecuteInNonManagedTXLock(lockName, new TransactionCallback<Boolean>() {
                    @Override
                    public Boolean execute(Connection conn) throws JobPersistenceException {
                        return txValidator.validate(conn, result);
                    }
                })) {
                    throw e;
                }
            }

            Long sigTime = clearAndGetSignalSchedulingChangeOnTxCompletion();
            if(sigTime != null && sigTime >= 0) {
                signalSchedulingChangeImmediately(sigTime);
            }
            
            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);
            }
        }
    }


getLockHandler那么可以思考下这个LockHandler怎么来的?

最后发现在JobStoreSupport的initail方法赋值了:

public void initialize(ClassLoadHelper loadHelper,
            SchedulerSignaler signaler) throws SchedulerConfigException {

        ...
        

        // If the user hasn't specified an explicit lock handler, then 
        // choose one based on CMT/Clustered/UseDBLocks.
        if (getLockHandler() == null) {
            
            // If the user hasn't specified an explicit lock handler, 
            // then we *must* use DB locks with clustering
            if (isClustered()) {
                setUseDBLocks(true);
            }
            
            if (getUseDBLocks()) {
                ...
                                // 在初始化方法里面赋值了
                setLockHandler(new StdRowLockSemaphore(getTablePrefix(), getInstanceName(), getSelectWithLockSQL()));
            } else {
                getLog().info(
                    "Using thread monitor-based data access locking (synchronization).");
                setLockHandler(new SimpleSemaphore());
            }
        }

    }


可以在StdRowLockSemaphore里面看到:

public static final String SELECT_FOR_LOCK = "SELECT * FROM "
            + TABLE_PREFIX_SUBST + TABLE_LOCKS + " WHERE " + COL_SCHEDULER_NAME + " = " + SCHED_NAME_SUBST
            + " AND " + COL_LOCK_NAME + " = ? FOR UPDATE";

    public static final String INSERT_LOCK = "INSERT INTO "
        + TABLE_PREFIX_SUBST + TABLE_LOCKS + "(" + COL_SCHEDULER_NAME + ", " + COL_LOCK_NAME + ") VALUES (" 
        + SCHED_NAME_SUBST + ", ?)"; 

可以看出采用了悲观锁的方式对triggers表进行行加锁, 以保证任务同步的正确性。 

当线程使用上述的SQL对表中的数据执行操作时,数据库对该行进行行加锁; 于此同时, 另一个线程对该行数据执行操作前需要获取锁, 而此时已被占用, 那么这个线程就只能等待, 直到该行锁被释放。 


Quartz的锁存放在:

CREATE TABLE `scheduler_locks` (
  `SCHED_NAME` varchar(120) NOT NULL COMMENT '调度名',
  `LOCK_NAME` varchar(40) NOT NULL COMMENT '锁名',
  PRIMARY KEY (`SCHED_NAME`,`LOCK_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8


锁名和上述常量一一对应:

 

有可能你的任务不能支持并发执行(因为有可能任务还没执行完, 下一轮就trigger了, 如果没做同步处理可能造成严重的数据问题), 那么在任务类加上注解:

@DisallowConcurrentExecution 


设置@DisallowConcurrentExecution以后程序会等任务执行完毕以后再去执行 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值