多方案定时任务(java语言)

1. Java定时任务简介

1.1基于java实现

a)利用thread人为实现
创建一个thread,然后让它在while循环里一直运行着,通过sleep方法来达到定时任务的效果。
b)用Timer和TimerTask
相比前者,当启动和去取消任务时可以控制,第一次执行任务时可以指定你想要的delay时间。
c)用ScheduledExecutorService
java.util.concurrent里,做为并发工具类被引进的,相比于Timer的单线程,它是通过线程池的方式来执行任务的,可以很灵活的去设定第一次执行任务delay时间,提供了良好的约定以便设定执行的时间间隔。

1.2基于spring实现

a)ScheduledTimerTask
Spring的ScheduledTimerTask定义了一个定时器任务的运行周期,遗憾的是,你可以指定任务执行的频度,但你无法精确指定它何时运行。
创建一个业务任务,在Spring配置文件中声明;在Spring配置文件中,配置ScheduledTimerTask,并且关联上自定义的任务实例;启动定时器,Spring的TimerFactoryBean负责启动定时任务。
b)使用Spring-Task
Spring自带的定时任务工具,spring task,可以将它比作一个轻量级的Quartz,而且使用起来很简单,除spring相关的包外不需要额外的包,而且支持注解和配置文件两种:
第一步:编写任务类;TaskJob,method job1 --代码省略

第二步:在spring配置文件头中添加命名空间及描述

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:task="http://www.springframework.org/schema/task"

xsi:schemaLocation="http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd">

第三步:spring配置文件中设置具体的任务

<task:scheduled-tasks>
<task:scheduled ref="taskJob" method="job1" cron="0 * * * * ?"/>
</task:scheduled-tasks>
<context:component-scan base-package="com.alibaba.mytask" />    
说明:ref参数指定的即任务类,method指定的即需要运行的方法,cron及cronExpression表达式,具体写法这里不介绍了,<context:component-scan base-package="com.alibaba.mytask" />spring扫描注解用的。
c)使用Quartz
有多种运行方式可选,同时也支持单点部署和集群两种模式,在当下企业应用中占有重要地位。

2. Quartz基本介绍

2.1 Quartz+Spring

Quartz是一个完全由java编写的开源作业调度框架,由OpenSymphony开源组织开发,该框架的核心是调度器,定时任务以作业的概念进行存储,调度器通过触发器来调度作业。
Spring提供了对Quartz的支持,开发者只需要在spring的配置中提供Quartz运行时所需要的作业及触发的相关信息即可。

2.2 Quartz核心元素

Quartz任务调度的核心元素为:Scheduler—任务调度器、Trigger—触发器、JobDetail—调度程序(Job-任务)。其中trigger和JobDetail是任务调度的元数据,scheduler是实际执行调度的控制器。
Scheduler由scheduler工厂创建:DirectSchedulerFactory或者StdSchedulerFactory。第二种工厂StdSchedulerFactory使用较多,因为DirectSchedulerFactory使用起来不够方便,需要作许多详细的手工编码设置。Scheduler主要有三种:RemoteMBeanScheduler,RemoteScheduler和StdScheduler。
Trigger是用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz中主要提供了四种类型的trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和NthIncludedDayTrigger。这四种trigger可以满足企业应用中的绝大部分需求。
JobDetail表示一个具体的可执行的调度程序,Job是这个可执行调度程序所要执行的内容,另外JobDetail还包含了这个任务调度的方案和策略。Job用于表示被调度的任务。主要有两种类型的job:无状态的(stateless)和有状态的(stateful)。对于同一个trigger来说,有状态的job不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job主要有两种属性:volatility和durability,其中volatility表示任务是否被持久化到数据库存储,而durability表示在没有trigger关联的时候任务是否被保留。两者都是在值为true的时候任务被持久化或保留。一个job可以被多个trigger关联,但是一个trigger只能关联一个job。
Quartz核心元素之间的关系如图所示:

2.3 Quartz线程视图

在Quartz中,有两类线程,Scheduler调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程
Scheduler调度线程主要有两个:执行常规调度的线程,和执行misfiredtrigger的线程。常规调度线程轮询存储的所有trigger,如果有需要触发的trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该trigger关联的任务。Misfire线程是扫描所有的trigger,查看是否有misfiredtrigger,如果有的话根据misfire的策略分别处理(fire now OR wait for the next fire)。

2.4单点部署和多点部署

Quartz提供RAMJobStore和JDBCJobStore两种基本作业存储类型,前者利用通常的内存来持久化调度程序信息,后者则需要利用后台数据库来持久化调度程序信息。要想实现集群环境下的分布式job,需要利用JDBCJobStore模式将定时任务信息持久化到数据库。
单点部署的模式下,Quartz同应用被部署在一台独立的服务器上,其调度器、job以及配置的相关信息被存储在的内存中,调度器容器随应用启动而初始化形成,如不手动关闭则随应用停止而关闭。这种模式下的定时任务无法持久,不适用于分布式的集群部署。
多点部署将Quartz与应用部署在多个服务上,它们的通信由持久化的数据库提供,调度器、job以及相关配置信息都被持久化到了共同的数据库当中,各个节点的Quartz调度器定时轮询访问数据库进行checkin,上报自身节点的心跳,并获取即将触发的job事件,同一个job只在一个服务器上执行。

3.定时任务分布式实现—spring+quartz(RAM方式)+缓存

3.1实现原理

Spring提供对Quartz定时任务的支持(配置作业和触发器的beans以及相关的一些基本的环境参数),Quartz的作业存储方式是RAMJobStore,这种方式适合非集群环境下,但对于集群部署,需利用缓存中间件(tair/redis...)控制并发,达到分布式锁的作用,这样能有效的控制各个服务器上的相同job在同一时间不会同时发生,避免了业务混乱,此处利用tair为例说明。

3.2优缺点

优点:tair是缓存中间件,除了封装成分布式锁,更重要的是高效存储数据,这在项目中也是需要用到的,利用tair控制job的并发,可以直接使用已存在的接口,减少开发成本;同时可以避免在数据库建Quartz相关的表,减少维护的成本;
缺点:Quartz相关节点彼此独立存在,调度和作业信息只能被临时保存在内存中,不能被持久化,一旦应用重启或者服务器宕机这些信息就会丢失,没有运行完的job下次也不会接着执行,同时其它的服务节点也不会接替来继续这些job;当job的数量过高时有可能全部在一台服务器上执行,资源无法做到合理分配利用;不管是否能够拿到分布式锁,job对应的线程都会被占用,不利于线程的合理使用。

4. Quartz定时任务分布式实现—spring+quartz(JDBC方式)

4.1实现原理

集群分布式并发环境中使用QUARTZ定时任务调度,在各个节点会上报任务,存到数据库中,执行时会从数据库中取出触发器来执行,如果触发器的名称和执行时间相同,则有一个节点并只有一个节点去执行此任务。如果此节点执行失败,则此任务则会被分派到另一节点执行,中途也会自动检查失效的定时调度,发现不成功的,其他节点立马接过来继续完成定时任务。对应的定时任务调度表比较多,根据Quartz的版本不同数目也有差异,大致11张。
Quartz集群环境下的定时任务基于分布式锁的规则,该锁被持久化到了数据库,在获取、触发job等事件的过程中都是基于该规则,这一规则是Quartz解决集群问题的核心思想,如图:

4.2优缺点

Quartz调度是通过触发器的类别来识别不同的任务,在不同的节点定义相同的触发器的类别,这样在集群下能稳定的运行,一个节点无法完成的任务,会被集群中拥有相同的任务的节点取代执行;分布式体现在当相同的任务定时在一个时间点,在那个时间点,不会被两个节点同时执行;Quartz的相关信息被持久化到数据库,数据库是各个节点Quartz的中心,服务节点只要能与数据库联动就可以正常执行job,其与服务器所属集群或者机房无关;程序重启或者机器宕机导致执行失败的job恢复后能被再次执行(Fail-over);一个服务节点执行一次后会等待随机时间,让其他的服务节点可以获取执行权限,实现负载均衡((Load balancing))。缺点是负载均衡无法做到将job按平摊到各节点,只能由Quartz内部策略来实现各服务节点分压。

4.3任务调度流程

1)调度器线程run()
包括读取配置资源,生成QuartzScheduler对象,创建该对象的运行线程并启动线程,初始化JobStore、QuartzScheduler、DBConnectionManager等重要组件;
至此,调度器的初始化工作已完成,初始化工作中quratz读取了数据库中存放的对应当前调度器的锁信息,对应CRM中的表QRTZ2_LOCKS,中的STATE_ACCESS,TRIGGER_ACCESS两个LOCK_NAME。
2)获取待触发trigger
a.数据库LOCKS表TRIGGER_ACCESS行加锁
b.读取JobDetail信息
c.读取trigger表中触发器信息并标记为"已获取"
d. commit事务,释放锁
3)触发trigger
a.数据库LOCKS表STATE_ACCESS行加锁
b.确认trigger的状态
c.读取trigger的JobDetail信息
d.读取trigger的Calendar信息
e.更新trigger信息
f. commit事务,释放锁
4)实例化并执行Job
从线程池获取线程执行JobRunShell的run方法。
当一个触发器触发时,调度器会通知实例化了JobListener和TriggerListener接口的0个或者多个Java对象(监听器可以是简单的Java对象, EJBs,或JMS发布者等).在任务执行后,这些监听器也会被通知。当任务完成时,他们会返回一个JobCompletionCode,这个代码告诉调度器任务执行成功或者失败.这个代码也会指示调度器做一些动作-例如立即再次执行任务。
整体流程时序图如下:

4.4具体配置和数据库建表

1)Quartz-2.1.7对应的mysql数据库表
QRTZ_FIRED_TRIGGERS:存储与已触发的Trigger相关的状态信息,以及相联job的执行信息;

QRTZ_PAUSED_TRIGGER_GRPS:存储已暂停的Trigger组的信息;

QRTZ_SCHEDULER_STATE:存储少量的有关Scheduler的状态信息,和别的Scheduler实例(假如是用于一个集群中)

QRTZ_LOCKS:存储程序的悲观锁的信息(假如使用了悲观锁)

QRTZ_SIMPLE_TRIGGERS:存储简单的Trigger,包括重复次数,间隔,以及已触的次数

QRTZ_SIMPROP_TRIGGERS:

QRTZ_CRON_TRIGGERS:存储Cron Trigger,包括Cron表达式和时区信息

QRTZ_BLOB_TRIGGERS:Trigger作为Blob类型存储(用于Quartz用户用JDBC创建他们自己定制的Trigger类型,JobStore并不知道如何存储实例的时候)

QRTZ_TRIGGERS:存储已配置的Trigger的信息

QRTZ_JOB_DETAILS:存储每一个已配置的Job的详细信息

QRTZ_CALENDARS:以Blob类型存储Quartz的Calendar信息
2)在web.xml中初始化调度器
<servlet>

<servlet-name>QuartzInitializer</servlet-name>

 <display-name>Quartz Initializer Servlet</display-name>

<servlet-class>

 org.quartz.ee.servlet.QuartzInitializerServlet

 </servlet-class>

 <load-on-startup>1</load-on-startup>

 <init-param>

 <param-name>config-file</param-name>

 <param-value>/some/path/my_quartz.properties</param-value>

 </init-param>

 <init-param>

 <param-name>shutdown-on-unload</param-name>

 <param-value>true</param-value>

 </init-param>

 <init-param>

 <param-name>start-scheduler-on-load</param-name>

 <param-value>true</param-value>

 </init-param>

</servlet>
3)配置节点的Quartz.Properties文件
#==============================================================

#Configure Main Scheduler Properties

#==============================================================

#配置集群时,quartz调度器的id,由于配置集群时,只有一个调度器,必须保证每个服务器该值都相同,可以不用修改,只要每个ams都一样就行

org.quartz.scheduler.instanceName = Scheduler1

#集群中每台服务器自己的id,AUTO表示自动生成,无需修改

org.quartz.scheduler.instanceId = AUTO

#==============================================================

#Configure ThreadPool

#==============================================================

#quartz线程池的实现类,无需修改

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool

#quartz线程池中线程数,可根据任务数量和负责度来调整

org.quartz.threadPool.threadCount = 5

#quartz线程优先级

org.quartz.threadPool.threadPriority = 5

#==============================================================

#Configure JobStore

#==============================================================

#表示如果某个任务到达执行时间,而此时线程池中没有可用线程时,任务等待的最大时间,如果等待时间超过下面配置的值(毫秒),本次就不在执行,而等待下一次执行时间的到来,可根据任务量和负责程度来调整

org.quartz.jobStore.misfireThreshold = 60000

#实现集群时,任务的存储实现方式,org.quartz.impl.jdbcjobstore.JobStoreTX表示数据库存储,以及事务的控制方式

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX/JobStoreCMT

#quartz存储任务相关数据的表的前缀,无需修改

org.quartz.jobStore.tablePrefix = QRTZ_

#连接数据库数据源名称,与下面配置中org.quartz.dataSource.myDS的myDS一致即可,可以无需修改

org.quartz.jobStore.dataSource = myDS

#是否启用集群,启用,改为true,注意:启用集群后,必须配置下面的数据源,否则quartz调度器会初始化失败

org.quartz.jobStore.isClustered = true

#集群中服务器相互检测间隔,每台服务器都会按照下面配置的时间间隔往服务器中更新自己的状态,如果某台服务器超过以下时间没有checkin,调度器就会认为该台服务器已经down掉,不会再分配任务给该台服务器

org.quartz.jobStore.clusterCheckinInterval = 20000

#==============================================================

#Non-Managed Configure Datasource

#==============================================================

#配置连接数据库的实现类,可以参照IAM数据库配置文件中的配置

org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver

#配置连接数据库连接,可以参照IAM数据库配置文件中的配置

org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/test

#配置连接数据库用户名

org.quartz.dataSource.myDS.user = yunxi

#配置连接数据库密码

org.quartz.dataSource.myDS.password = 123456

#配置连接数据库连接池大小,一般为上面配置的线程池的2倍

org.quartz.dataSource.myDS.maxConnections = 10
4)配置applicationContext-quartz.xml文件
含有多个Jobs的一个xml文件的一个例子:
<?xml version='1.0' encoding='utf-8'?>

<quartz xmlns="http://www.opensymphony.com/quartz/JobSchedulingData"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.opensymphony.com/quartz/JobSchedulingData


http://www.opensymphony.com/quartz/xml/job_scheduling_data_1_5.xsd
" version="1.5">

 <calendar class-name="org.quartz.impl.calendar.HolidayCalendar" replace="true">

 <name>holidayCalendar</name>

 <description>HolidayCalendar</description>

 <base-calendar class-name="org.quartz.impl.calendar.WeeklyCalendar">

<name>weeklyCalendar</name>

 <description>WeeklyCalendar</description>

 <base-calendar class-name="org.quartz.impl.calendar.AnnualCalendar">

<name>annualCalendar</name>

 <description>AnnualCalendar</description>

 </base-calendar>

 </base-calendar>

 </calendar>

 

 <job>

 <job-detail>

 <name>testJob1</name>

 <group>testJobs</group>

 <description>Test Job Number 1</description>

 <job-class>personal.ruanyang.quartz.plugin.SimpleJob</job-class>

<volatility>false</volatility>

<durability>false</durability>

 <recover>false</recover>

 <job-data-map allows-transient-data="true">

 <entry>

 <key>test1</key>

 <value>test1</value>

 </entry>

 <entry>

 <key>test2</key>

 <value>test2</value>

 </entry>

 </job-data-map>

 </job-detail>

 <trigger>

 <cron>

 <name>testTrigger1</name>

 <group>testJobs</group>

 <description>Test Trigger Number 1</description>

<job-name>testJob1</job-name>

 <job-group>testJobs</job-group>

 <cron-expression>0/15 * * ? * *</cron-expression>

<!-- every 15 seconds... -->

 </cron>

 </trigger>

 </job>

 

 <job>

 <job-detail>

 <name>testJob2</name>

 <group>testJobs</group>

 <description>Test Job Number 2</description>

 <job-class>personal.ruanyang.quartz.plugin.SimpleJob</job-class>

<volatility>false</volatility>

<durability>false</durability>

 <recover>false</recover>i)

 </job-detail>

 <trigger>

 <simple>

 <name>testTrigger2</name>

 <group>testJobs</group>

 <description>Test Trigger Number 2</description>

 <calendar-name>holidayCalendar</calendar-name>

<job-name>testJob2</job-name>

 <job-group>testJobs</job-group>

 <start-time>2004-02-26T12:26:00</start-time>

 <repeat-count>10</repeat-count>

 <repeat-interval>5000</repeat-interval>

 </simple>

 </trigger>

 </job>

</quartz>
dataSource:项目中用到的数据源,里面包含了quartz用到的11张数据库表;
applicationContextSchedulerContextKey:是org.springframework.scheduling.quartz.SchedulerFactoryBean这个类中把spring上下文以key/value的方式存放在了SchedulerContext中了,可以用applicationContextSchedulerContextKey所定义的key得到对应spring的ApplicationContext;
configLocation:用于指明quartz的配置文件的位置
requestsRecovery:requestsRecovery属性必须设置为true,当Quartz服务被中止后,再次启动或集群中其他机器接手任务时会尝试恢复执行之前未完成的所有任务。

5. Quartz定时任务分布式实现方式对比结果

两者对比
Quartz(RAM)+Tair
Quartz(JDBC)
优点
1、各节点不需要有中心,tair机制控制并发;
2、并发的控制能力由第三方组件提供,维护成本较低;
3、tair利用缓存比Quartz集群利用数据库效率可能略高(待测);
1、一个任务执行失败会有另一个节点的接替执行;
2、重启服务等动作导致此次任务失败后再次启动后可继续执行;
3、任务能被触发才会开启线程执行,不能触发则不会启动线程,提高资源利用率;
4、有内部的负载均衡机制,确保服务器都能得到执行权限;
缺点
1、容易形成死锁,且不会自动释放锁(现已优化,但还是有自动释放期限);
2、每个job到触发时间都会被触发,线程数增加,就集群而言资源利用率低;
3、job失败后不会有另一个节点接替执行;
4、job配置信息不会被持久化,重启服务器就会丢失,下次不会再继续执行;
1、job配置信息需要持久化到数据库中,11张表,数据库的检索能力会影响各节点的互通,同时维护成本会高些;
2、与tair(缓存)相比查询速度可能会有劣势;(可建索引优化)
3、水平集群如果时间节点不一致会出现混乱;
备注(索引语句):
create index idx_qrtz_t_next_fire_time on qrtz_triggers(NEXT_FIRE_TIME);

create index idx_qrtz_t_state on qrtz_triggers(TRIGGER_STATE);

create index idx_qrtz_t_nf_st on qrtz_triggers(TRIGGER_STATE,NEXT_FIRE_TIME);

create index idx_qrtz_ft_trig_name on qrtz_fired_triggers(TRIGGER_NAME);

create index idx_qrtz_ft_trig_group on qrtz_fired_triggers(TRIGGER_GROUP);

create index idx_qrtz_ft_trig_name on qrtz_fired_triggers(TRIGGER_NAME);

create index idx_qrtz_ft_trig_n_g on qrtz_fired_triggers(TRIGGER_NAME,TRIGGER_GROUP);

create index idx_qrtz_ft_trig_inst_name on qrtz_fired_triggers(INSTANCE_NAME);

create index idx_qrtz_ft_job_name on qrtz_fired_triggers(JOB_NAME);

create index idx_qrtz_ft_job_group on qrtz_fired_triggers(JOB_GROUP);

猜你喜欢

转载自blog.csdn.net/DislodgeCocoon/article/details/80520080