定时任务的Solution:
- #crm
- 0 2 * * * /opt/***/javafiles/***/shell/***_daily_stat.sql
- 30 7 * * * /opt/***/javafiles/***/shell/***_data_fix
- 30 0 * * * /opt/***/javafiles/***/shell/***_sync_log
- 0 1 * * * /opt/***/javafiles/***/shell/***_clear_log
- 20 8 * * * /opt/***/javafiles/***/shell/***_daily >> /var/***/logs/***_daily.log 2>&1
- 40 1 * * * /opt/***/javafiles/***/shell/***_sync_account2
- 0 2 * * 1 /opt/***/javafiles/***/shell/***_weekly >> /var/***/logs/***_weekly.log 2>&1
(2)使用python(多数据源) + SQL的方式
- def connectCRM():
- return MySQLdb.Connection("localhost", "***", "***", "***", 3306, charset="utf8")
- def connectTemp():
- return MySQLdb.Connection("localhost", "***", "***", "***", 3306, charset="utf8")
- def connectOA():
- return MySQLdb.Connection("localhost", "***", "***", "***", 3306, charset="utf8")
- def connectCore():
- return MySQLdb.Connection("localhost", "***", "***", "***", 3306, charset="utf8")
- def connectCT():
- return MySQLdb.Connection("localhost", "***", "***", "***", 3306, charset="utf8")
(3)使用spring + JDK timer方式调用接口完成定时任务
- <bean id="accountStatusTaskScanner" class="***.impl.AccountStatusTaskScanner" />
- <task:scheduler id="taskScheduler" pool-size="5" />
- <task:scheduled-tasks scheduler="taskScheduler">
- <task:scheduled ref="accountStatusTaskScanner" method="execute" cron="0 0 1 * * ?" />
- </task:scheduled-tasks>
- public abstract class SingletonServerTaskScanner implements TaskScanner {
- private final Logger logger = LoggerFactory.getLogger(SingletonServerTaskScanner.class);
- @Override
- public void execute() {
- String hostname = "";
- try {
- hostname = InetAddress.getLocalHost().getHostName();
- } catch (UnknownHostException e) {
- logger.error(e.getMessage(), e);
- }
- //判断是否为当前可执行服务器
- if (ConfigUtil.getValueByKey("core.scan.server").equals(hostname)) {
- doScan();
- }
- }
- public abstract void doScan();
- }
//对于srv23的重启,保存在内存中的任务将丢失,每次重启srv23重新生成定时任务
- public class CrmInitializer implements InitializingBean {
- private Logger logger = LoggerFactory.getLogger(CrmInitializer.class);
- @Override
- public void afterPropertiesSet() throws Exception {
- // 扫描商家状态,创建定时任务
- logger.info("扫描商家状态,创建定时任务");
- accountStatusTaskScanner.execute();
- // 扫描N天未拜访商家,创建定时任务
- logger.info("扫描N天未拜访商家,创建定时任务");
- nDaysActivityScanner.execute();
- }
- }
- //通过调用srv23的特定URL的方式,动态指定任务(如取消N天未拜访,私海进保护期,保护期进公海等)
- public class SingletonServerTaskController {
- @Resource
- private AccountService accountService;
- @RequestMapping(value = "/reschedule")
- public @ResponseBody
- String checkAndRescheduleAccount(Integer accountId) {
- logger.debug("reschedule task for accountId:" + accountId);
- if (isCurrentServer()) {
- accountService.checkAndRescheduleAccount(Arrays.asList(accountId));
- }
- return "ok";
- }
- private boolean isCurrentServer() {
- String hostname = "";
- try {
- hostname = InetAddress.getLocalHost().getHostName();
- } catch (UnknownHostException e) {
- logger.error(e.getMessage(), e);
- }
- if (ConfigUtil.getValueByKey("core.scan.server").equals(hostname)) {
- return true;
- } else {
- return false;
- }
- }
- }
3)支持集群:org.quartz.jobStore.isClustered
4)支持任务恢复:requestsRecovery
项目中使用分布式并发部署定时任务,多台跨JVM,按照常理逻辑每个JVM的定时任务会各自运行,这样就会存在问题,多台分布式JVM机器的应用服务同时干活,一个是加重服务负担,另外一个是存在严重的逻辑问题,比如需要回滚的数据,就回滚了多次,刚好quartz提供很好的解决方案。
集群分布式并发环境中使用QUARTZ定时任务调度,会在各个节点会上报任务,存到数据库中,执行时会从数据库中取出触发器来执行,如果触发器的名称和执行时间相同,则只有一个节点去执行此任务。
1.持久化任务:当应用程序停止运行时,所有调度信息不被丢失,当你重新启动时,调度信息还存在,这就是持久化任务(保存到数据库表中)。
2.集群和分布式处理:当在集群环境下,当有配置Quartz的多个客户端时(节点),采用Quartz的集群和分布式处理时,我们要了解几点好处
1) 一个节点无法完成的任务,会被集群中拥有相同的任务的节点取代执行。
2) Quartz调度是通过触发器的类别来识别不同的任务,在不同的节点定义相同的触发器的类别,这样在集群下能稳定的运行,一个节点无法完成的任务,会被集群中拥有相同的任务的节点取代执行。
3)分布式体现在当相同的任务定时在一个时间点,在那个时间点,不会被两个节点同时执行。
quartz的分布式架构如上图,可以看到数据库是各节点上调度器的枢纽.各个节点并不感知其他节点的存在,只是通过数据库来进行间接的沟通.
组件间的通讯图如下:(*注:主要的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表中对应当前调度器的行级锁,获取锁后即可执行其他表中的数据库操作,随着操作事务的提交,行级锁被释放,供其他调度器实例获取.
中断 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操作。
主处理线程: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实例
quartz工作者线程
Quartz 不会在主线程(QuartzSchedulerThread)中处理用户的Job。Quartz 把线程管理的职责委托给ThreadPool。
一般的设置使用org.quartz.simpl.SimpleThreadPool。SimpleThreadPool 创建了一定数量的 WorkerThread 实例来使得Job能够在线程中进行处理。
WorkerThread 是定义在 SimpleThreadPool 类中的内部类,它实质上就是一个线程。
要创建 WorkerThread 的数量以及配置他们的优先级是在文件quartz.properties中并传入工厂。
spring properties
- <prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPool</prop>
- <prop key="org.quartz.threadPool.threadCount">20</prop>
- <prop key="org.quartz.threadPool.threadPriority">5</prop>
主线程(QuartzSchedulerThread)请求ThreadPool去运行 JobRunShell 实例,ThreadPool 就检查看是否有一个可用的工作者线
程。假如所以已配置的工作者线程都是忙的,ThreadPool 就等待直到有一个变为可用。当一个工作者线程是可用的,
表名描述
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实例使用多套的表,通过改变前缀来实现。
<!-- 调度任务 --> <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";
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里面看到:
可以看出采用了悲观锁的方式对triggers表进行行加锁, 以保证任务同步的正确性。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 + ", ?)";
当线程使用上述的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