Quartz集群实战及原理解析

原文出处:https://blog.csdn.net/wenniuwuren/article/details/45866807

       选Quartz的团队基本上是冲着Quartz本身实现的集群去的, 不然JDK自带Timer就可以实现相同的功能, 而Timer存在的单点故障是生产环境上所不能容忍的。 在自己造个有负载均衡和支持集群(高可用、伸缩性)的调度框架又影响项目的进度, 所以大多数团队都直接使用了Quartz来作为调度框架。


一、 Quartz集群的架构图:



二、 Quartz集群配置:

  1. <!-- 调度任务 -->
  2. <bean id="jobDetail"
  3. class= "org.springframework.scheduling.quartz.JobDetailFactoryBean">
  4. <property name="jobClass" value="全类名" />
  5. <property name="durability" value="true"/>
  6. <property name="targetMethod" value="execute" />
  7. <property name="concurrent" value="true" /> -->
  8. <!-- <property name="shouldRecover" value="true" /> -->
  9. </bean>
  10. <!-- 调度工厂 -->
  11. <bean id="scheduler" lazy-init="false" autowire="no"
  12. class= "org.springframework.scheduling.quartz.SchedulerFactoryBean">
  13. <!-- 注册JobDetails -->
  14. <property name="jobDetails">
  15. <list>
  16. <ref bean="jobDetail"/>
  17. </list>
  18. </property>
  19. <!--可选,QuartzScheduler 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 -->
  20. <property name="overwriteExistingJobs" value="true"/>
  21. <!-- 属性 -->
  22. <property name="quartzProperties">
  23. <props>
  24. <!-- 集群要求必须使用持久化存储 -->
  25. <prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreCMT </prop>
  26. <prop key="org.quartz.scheduler.instanceName">EventScheduler </prop>
  27. <!-- 每个集群节点要有独立的instanceId -->
  28. <prop key="org.quartz.scheduler.instanceId">AUTO </prop>
  29. <!-- Configure ThreadPool -->
  30. <prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPool </prop>
  31. <prop key="org.quartz.threadPool.threadCount">50 </prop>
  32. <prop key="org.quartz.threadPool.threadPriority">5 </prop>
  33. <prop key="org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread">true </prop>
  34. <!-- Configure JobStore -->
  35. <prop key="org.quartz.jobStore.misfireThreshold">60000 </prop>
  36. <prop key="org.quartz.jobStore.driverDelegateClass">org.quartz.impl.jdbcjobstore.StdJDBCDelegate </prop>
  37. <prop key="org.quartz.jobStore.tablePrefix">SCHEDULER_ </prop>
  38. <prop key="org.quartz.jobStore.maxMisfiresToHandleAtATime">10 </prop>
  39. <!-- 开启集群 -->
  40. <prop key="org.quartz.jobStore.isClustered">true </prop>
  41. <prop key="org.quartz.jobStore.clusterCheckinInterval">20000 </prop>
  42. <prop key="org.quartz.jobStore.dontSetAutoCommitFalse">true </prop>
  43. <prop key="org.quartz.jobStore.txIsolationLevelSerializable">false </prop>
  44. <prop key="org.quartz.jobStore.dataSource">myDS </prop>
  45. <prop key="org.quartz.jobStore.nonManagedTXDataSource">myDS </prop>
  46. <prop key="org.quartz.jobStore.useProperties">false </prop>
  47. <!-- Configure Datasources -->
  48. <prop key="org.quartz.dataSource.myDS.driver">com.mysql.jdbc.Driver </prop>
  49. <prop key="org.quartz.dataSource.myDS.URL">${db.url} </prop>
  50. <prop key="org.quartz.dataSource.myDS.user">${db.username} </prop>
  51. <prop key="org.quartz.dataSource.myDS.password">${db.password} </prop>
  52. <prop key="org.quartz.dataSource.myDS.maxConnections">10 </prop>
  53. <prop key="org.quartz.dataSource.myDS.validationQuery">select 0 from dual </prop>
  54. </props>
  55. </property>
  56. <property name="applicationContextSchedulerContextKey" value="applicationContext" />
  57. </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里面释放锁(一种典型的同步处理方法)

  1. protected <T> T executeInNonManagedTXLock(
  2. String lockName,
  3. TransactionCallback<T> txCallback, final TransactionValidator<T> txValidator) throws JobPersistenceException {
  4. boolean transOwner = false;
  5. Connection conn = null;
  6. try {
  7. if (lockName != null) {
  8. // If we aren't using db locks, then delay getting DB connection
  9. // until after acquiring the lock since it isn't needed.
  10. if (getLockHandler().requiresConnection()) {
  11. conn = getNonManagedTXConnection();
  12. }
  13. // 获取锁
  14. transOwner = getLockHandler().obtainLock(conn, lockName);
  15. }
  16. if (conn == null) {
  17. conn = getNonManagedTXConnection();
  18. }
  19. final T result = txCallback.execute(conn);
  20. try {
  21. commitConnection(conn);
  22. } catch (JobPersistenceException e) {
  23. rollbackConnection(conn);
  24. if (txValidator == null || !retryExecuteInNonManagedTXLock(lockName, new TransactionCallback<Boolean>() {
  25. @Override
  26. public Boolean execute(Connection conn) throws JobPersistenceException {
  27. return txValidator.validate(conn, result);
  28. }
  29. })) {
  30. throw e;
  31. }
  32. }
  33. Long sigTime = clearAndGetSignalSchedulingChangeOnTxCompletion();
  34. if(sigTime != null && sigTime >= 0) {
  35. signalSchedulingChangeImmediately(sigTime);
  36. }
  37. return result;
  38. } catch (JobPersistenceException e) {
  39. rollbackConnection(conn);
  40. throw e;
  41. } catch (RuntimeException e) {
  42. rollbackConnection(conn);
  43. throw new JobPersistenceException( "Unexpected runtime exception: "
  44. + e.getMessage(), e);
  45. } finally {
  46. try {
  47. // 释放锁
  48. releaseLock(lockName, transOwner);
  49. } finally {
  50. cleanupConnection(conn);
  51. }
  52. }
  53. }


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

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

  1. public void initialize(ClassLoadHelper loadHelper,
  2. SchedulerSignaler signaler) throws SchedulerConfigException {
  3. ...
  4. // If the user hasn't specified an explicit lock handler, then
  5. // choose one based on CMT/Clustered/UseDBLocks.
  6. if (getLockHandler() == null) {
  7. // If the user hasn't specified an explicit lock handler,
  8. // then we *must* use DB locks with clustering
  9. if (isClustered()) {
  10. setUseDBLocks( true);
  11. }
  12. if (getUseDBLocks()) {
  13. ...
  14. // 在初始化方法里面赋值了
  15. setLockHandler( new StdRowLockSemaphore(getTablePrefix(), getInstanceName(), getSelectWithLockSQL()));
  16. } else {
  17. getLog().info(
  18. "Using thread monitor-based data access locking (synchronization).");
  19. setLockHandler( new SimpleSemaphore());
  20. }
  21. }
  22. }

可以在StdRowLockSemaphore里面看到:

  1. public static final String SELECT_FOR_LOCK = "SELECT * FROM "
  2. + TABLE_PREFIX_SUBST + TABLE_LOCKS + " WHERE " + COL_SCHEDULER_NAME + " = " + SCHED_NAME_SUBST
  3. + " AND " + COL_LOCK_NAME + " = ? FOR UPDATE";
  4. public static final String INSERT_LOCK = "INSERT INTO "
  5. + TABLE_PREFIX_SUBST + TABLE_LOCKS + "(" + COL_SCHEDULER_NAME + ", " + COL_LOCK_NAME + ") VALUES ("
  6. + SCHED_NAME_SUBST + ", ?)";
可以看出采用了悲观锁的方式对triggers表进行行加锁, 以保证任务同步的正确性。

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


Quartz的锁存放在:

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


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



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

@DisallowConcurrentExecution

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



四、 参考资料


Quartz官网: http://quartz-scheduler.org/documentation/quartz-2.x/tutorials/tutorial-lesson-11



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值