在集群环境中创建 Quartz 触发器

Mus*_*ice 6 java race-condition quartz-scheduler

相关:Quartz 集群 - 服务器启动时重复触发器

我正在使用 Quartz Scheduler 来管理基于 java 的集群环境中的计划作业。在任何给定时间,集群中都有少量节点,它们都运行 Quartz,并由所有节点都连接到的 postgresql 数据库中的数据存储提供支持。

当实例初始化时,它会尝试通过执行以下代码来创建或更新 Quartz 数据存储中的作业和触发器:

private void createOrUpdateJob(JobKey jobKey, Class<? extends org.quartz.Job> clazz, Trigger trigger) throws SchedulerException {
    JobBuilder jobBuilder = JobBuilder.newJob(clazz).withIdentity(jobKey);
    if (!scheduler.checkExists(jobKey)) {
        // if the job doesn't already exist, we can create it, along with its trigger. this prevents us
        // from creating multiple instances of the same job when running in a clustered environment
        scheduler.scheduleJob(jobBuilder.build(), trigger);
        log.error("SCHEDULED JOB WITH KEY " + jobKey.toString());
    } else {
        // if the job has exactly one trigger, we can just reschedule it, which allows us to update the schedule for
        // that trigger.
        List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
        if (triggers.size() == 1) {
            scheduler.rescheduleJob(triggers.get(0).getKey(), trigger);
            return;
        }

        // if for some reason the job has multiple triggers, it's easiest to just delete and re-create the job,
        // since we want to enforce a one-to-one relationship between jobs and triggers
        scheduler.deleteJob(jobKey);
        scheduler.scheduleJob(jobBuilder.build(), trigger);
    }
}
Run Code Online (Sandbox Code Playgroud)

这种方法解决了许多问题:

  1. 如果环境配置不正确(即作业/触发器不存在),那么它们将由启动的第一个实例创建
  2. 如果作业已经存在,但我想修改其计划(将以前每 7 分钟运行一次的作业更改为现在每 5 分钟运行一次),我可以为其定义一个新触发器,并且重新部署将重新安排触发器中的触发器数据库
  3. 将创建一个作业实例,因为我们总是通过指定的 JobKey 来引用作业,而 JobKey 是由作业本身定义的。这意味着作业(及其关联的触发器)只会创建一次,无论集群中有多少个节点,或者我们部署了多少次。

这一切都很好,但我担心当两个实例同时启动时可能会出现潜在的竞争状况。由于此代码没有集群中所有节点都会遵守的全局锁,因此如果两个实例同时上线,我最终可能会得到重复的作业或触发器,这违背了此代码的要点。

是否有在集群环境中自动定义 Quartz 作业和触发器的最佳实践?或者我需要自己设置锁吗?

Ing*_*ngo 1

我不确定在 Quartz 中是否有更好的方法来做到这一点。但如果您已经在使用 Redis 或 Memcache,我建议让所有实例针对众所周知的键执行原子增量。如果您粘贴的代码应该每个集群每小时仅运行一项作业,您可以执行以下操作:

long timestamp = System.currentTimeMillis() / 1000 / 60 / 60;
String key = String.format("%s_%d", jobId, timestamp);

// this will only be true for one instance in the cluster per (job, timestamp) tuple
bool shouldExecute = redis.incr(key) == 1

if (shouldExecute) {
  // run the mutually exclusive code
}
Run Code Online (Sandbox Code Playgroud)

时间戳为您提供了一个移动窗口,在该窗口内作业竞争执行该作业。