Quartz的负载均衡如何实现

    项目中使用Quartz来管理定时任务,配置将任务信息保存到关系型数据库中,并且使用的是集群模式。集群会保证同一个任务到达触发时间的时候,只有一台机器去执行该任务。我现在想了解的是:
    1.Quartz如何保证任务只在一个节点运行(即任务不会重复执行)?
    2.在集群的哪个节点上运行,Quartz是如何进行选取的?
    3.任务太多,而线程池中配置的线程太少时怎么办?
    带着这三个问题去探究下Quartz内部的实现机制。

 

Quartz的基本概念:

    简单介绍Quartz框架中的一些基本概念,详细的Quartz使用Demo另行百度把,一两句话也讲不清楚。
Scheduler:
    Quartz中的定时调度器,用户将任务注册到Scheduler中进行调度
Job:
    用户的定时任务
Trigger: 
    用户定时任务的触发策略
MisFire: 
    如果定时任务到了触发时间,但是由于一些其他意外原因没有触发,并且超过触发时间一定的时间范围(默认为60s),称之为MisFire。MisFire对应有多种处理策略,后续会讲到。
    能想到的意外原因有:没有足够的线程运行任务、Schedule崩溃了(例如服务器断电或者进程崩溃了)、Job时间误设置成了很久之前的时间。

 

Quartz中的线程模型:

      Quartz在运行时使用的是类似生产者和消费者模式,一个生产者去获取任务,然后将这些任务交给消费者去执行。Quartz中有如下几类线程:
    1.线程池SimpleThreadPool和内部的工作线程WorkThread
        Quartz内部自己实现的线程池,和JDK中的线程池很像,是运行具体任务用的。
    2.核心调度线程QuartzScheduleThread
        用于Trigger的获取、触发并放入到SimpleThreadPool线程池中去执行任务。
    3.集群管理线程ClusterManager和处理MisFire任务线程MisfireHandler
        开启cluster的话,前者用于check集群中的失效的节点,后续再细看这两个线程的内部原理把。本文暂时不去做详细的分析

Quartz线程模型如下图所示:

 

Quartz是如何保证任务只在一个节点上运行:

由于获取任务是由QuartzScheduleThread实现的,来看下它的run()方法是怎么获取Trigger然后执行的:
    run()方法内部会不断看到如下这种判断,只要在执行的任意过程中发现Quartz实例已经停止了,那么任务就不再会继续执行了。

获取待触发的Triggers:    

    Trigger状态变成ACQUIRED,后文会细说下。

    获取待触发的Triggers之后,需要检查下此时是否出现了触发时间更早的任务,如果存在需要判断是否要舍弃当前任务去获取更早的这些任务的Trigger来执行:
isCandidateNewTimeEarlierWithinReason()方法内部有如下判断逻辑:
    qsRsrcs.getJobStore().supportsPersistence() ? 70L : 7L
    即如果使用的是数据库存储,查询时间假定为70ms,内存存储假定为7ms,如果当前时间距已获得的第一个Trigger触发时间小于这个查询时间,则认为丢弃当前已经获取到的Triggers是不合算的,会继续执行当前已获取到的Trigger。

 

通知JobStore修改已获取到的Trigger的状态:
    将Trigger状态从ACQUIRED -> EXECUTING,表明Trigger已经被正确的被当前Quartz节点获取到并准备执行

如果操作Trigger时发生异常就释放掉Trigger,正常情况下会将Trigger对应的Job封装成JobRunShell然后扔到线程池中运行:


接下来,细看下从数据库中获取Trigger的流程:
    triggers = qsRsrcs.getJobStore().acquireNextTriggers(now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow())
    idleWaitTime默认是30s
    qsRsrcs.getMaxBatchSize()默认是1
    qsRsrcs.getBatchTimeWindow()默认是0
    即调度线程一次会拉取NEXT_FIRE_TIME小于(now + idleWaitTime +batchTimeWindow),大于(now - misfireThreshold)的,min(availThreadCount,maxBatchSize)个triggers,默认情况下,如果Quartz有空闲的线程,会拉取未来30s,过去60s之间还未fire的1个trigger。
    需要注意的是,如果不显示设置加锁并且获取的Trigger数量不超过1个,操作数据库时是不会加锁的(org.quartz.jobStore.acquireTriggersWithinLock=true):
 
    后续的其他代码就是根据触发时间条件从数据库获取Triggers,主要就是SQL语句,暂时不打算细看了,如果有必要后续再仔细分析吧。


回答最开始提出来的那三个问题:


1.Quartz如何保证任务只在一个节点运行?
    在acquireNextTriggers()方法中,通过数据库锁 + 事务来保证Trigger只被集群中的一个节点获取到 

2.在集群的哪个节点上运行,Quartz是如何进行选取的?
    随缘的。集群中各个节点做到时间同步很重要。如果待触发的任务少且运行快,那么很可能一直在时间最早的那一个节点上执行。

3.任务太多,而线程池中配置的线程太少时怎么办?
    没事,反正选取Trigger的时候也会考虑空闲线程的数量,空闲线程少的话实例就少选取几个Trigger来执行。


现在想到的新问题:
   单个节点在执行任务的过程中断电了,这些任务为什么设置recovery就会被重新调度执行,如果没有设置就直接是执行失败了。
    后续有时间再继续分析吧…


附:正常情况下TriggerStatus的变换:

 

 

 


参考:
    https://www.cnblogs.com/liuroy/p/7517777.html(Quartz中的线程模型图)
    https://segmentfault.com/a/1190000015492260#articleHeader0 (Quartz任务重复调度问题)
 

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值