Spring 和多个实例上的计划任务

Spring and scheduled tasks on multiple instances

我们有一个 Spring 启动应用程序,并且有计划任务。

我们想在多台服务器上部署我们的应用程序,因此会有多个应用程序实例。

如何配置Spring到运行定时任务只在指定的服务器上?

这是一个非常广泛的话题。有很多选择可以实现这一目标。

  1. 您可以将您的应用程序配置为具有多个配置文件。例如使用另一个配置文件 'cron' 。并仅在具有此配置文件的一台服务器上启动您的应用程序。因此,例如,在生产环境中,您有三台服务器(S1、S2、S3),那么您可以在 S1 上使用配置文件 prod 和 cron(-Dspring.profiles.active=prod,cron) 运行。在 S2 和 S3 上只需使用 prod profile(-Dspring.profiles.active=prod).

    并且在代码中,您可以在调度程序 类 上使用 @Profile("cron")。这样它只会在 cron 配置文件处于活动状态时执行

  2. 使用分布式锁。如果你的环境中有Zookeeper,你可以使用它来实现分布式锁系统。

  3. 您可以使用一些数据库 (mysql) 并创建示例代码来锁定其中一个 table 并添加一个条目。无论哪个实例获得锁,都会在该数据库中创建一个条目并执行 cron 作业。你需要 检查你的代码,如果 getLock() 成功才继续执行。 Mysql 有像 LOCK TABLES 这样的实用程序,你可以用它来摆脱并发 read/writes.

就我个人而言,选项 2 是最好的。

使用 Spring 最简单的方法是使用环境变量和值注释:

1 - 在 class:

中获取带有 Value 注释的环境变量
@Value("${TASK_ENABLED}")
private boolean taskEnabled;

2 - 检查 taskEnabled 值以执行任务:

@Scheduled(fixedDelay = 50000)
public void myTask() {
  if (this.taskEnabled) {
    //do stuff here...
  }
}

3 - 为每个服务器设置正确的环境变量:

错误:

java -DTASK_ENABLED=0 -jar software.jar

正确:

java -DTASK_ENABLED=1 -jar software.jar

具有全局配置的示例 class

要使用全局配置 class,您应该告诉 spring 它是一个带有 @Component 的组件,并注释一个 set 方法以将值传递给静态字段。

1 - 使用静态字段创建配置 class:

@Component
public class AppConfiguration {

  public static boolean taskEnabled;

  @Value("${TASK_ENABLED}")
  public void setTaskEnabled(boolean taskEnabled) {
    this.taskEnabled = taskEnabled;
  }
}

2 - 检查 taskEnabled 值以执行任务:

@Scheduled(fixedDelay = 50000)
public void myTask() {
  if (AppConfiguration.taskEnabled) {
    //do stuff here...
  }
}

3 - 为每个服务器设置正确的环境变量:

错误:

java -DTASK_ENABLED=0 -jar software.jar

正确:

java -DTASK_ENABLED=1 -jar software.jar

Spring - ShedLock 项目专门为此而创建。

依赖关系 -

<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>    

配置-

@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")

实施-

@Scheduled(cron = "0 0/15 * * * ?")
@SchedulerLock(name = "AnyUniqueName", 
  lockAtLeastForString = "PT5M", lockAtMostForString = "PT10M")
public void scheduledTask() {
    // ...
}

此设置将确保只有一个实例应该 运行 计划任务。

如果你只想要一个特定的实例应该运行调度程序任务,

您需要配置调度程序以使用属性文件并像这样控制调度程序开关 -

@ConditionalOnProperty(
  value = "scheduling.enabled", havingValue = "true", matchIfMissing = true
)
@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
public class SchedulingConfig {

现在,您需要在 application.properties 文件中提供一个 属性 scheduling.enabled = true,因为您希望 Schedular 成为 运行.

按照此 link 完成实施。

我认为您需要的帮助在另一个 post 的答案之一中。

看到这个post:

最简单的解决方案是您可以为不同的实例使用不同的属性文件。以下是步骤

  1. @ConditionalOnProperty(prefix = "enable-scheduler", havingValue = "true")
  2. 注释您的调度程序 class
  3. 在属性文件中添加一个布尔值 enable-scheduler=true
  4. 现在,对于任何实例,请在您的属性文件中使用 enable-scheduler=true,对于任何其他实例,请使用 enable-scheduler=false

示例:

@Component
@ConditionalOnProperty(prefix = "enable-scheduler", havingValue = "true")
public class AnyScheduler {

    private final Logger log = LoggerFactory.getLogger(getClass());

    private final AnyService service;

    @Autowired
    public AnyScheduler(AnyService service) {
        this.service = service;
    }

    @Scheduled(cron = "${scheduler-cron}")
    public void syncModifiedCve() {
        log.info("Scheduler started. . .");
        service.doTask();
    }

}

最佳选择之一 - 使用带有集群的 Quartz 调度程序。 很简单,就是:

implementation("org.springframework.boot:spring-boot-starter-quartz")

并使用 spring 为 quartz 配置作业(参见 tutorial

application.yaml 中的集群配置:

spring:
  datasource: ... # define jdbc datasource
  quartz:
    job-store-type: jdbc # Database Mode
    jdbc:
      initialize-schema: never # For clustering do not initialize table structure
    properties:
      org.quartz:
          scheduler:
            instanceId: AUTO #Default hostname and timestamp generate instance ID, which can be any string, but must be the only corresponding qrtz_scheduler_state INSTANCE_NAME field for all dispatchers
            #instanceName: clusteredScheduler #quartzScheduler
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX #Persistence Configuration
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate #We only make database-specific proxies for databases
            useProperties: true #Indicates that JDBC JobStore stores all values in JobDataMaps as strings, so more complex objects can be stored as name-value pairs rather than serialized in BLOB columns.In the long run, this is safer because you avoid serializing non-String classes to BLOB class versions.
            tablePrefix: QRTZ_  #Database Table Prefix
            misfireThreshold: 60000 #The number of milliseconds the dispatcher will "tolerate" a Trigger to pass its next startup time before being considered a "fire".The default value (if you do not enter this property in the configuration) is 60000 (60 seconds).
            clusterCheckinInterval: 5000 #Set the frequency (in milliseconds) of this instance'checkin'* with other instances of the cluster.Affects the speed of detecting failed instances.
            isClustered: true #Turn on Clustering
          threadPool: #Connection Pool
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10
            threadPriority: 5
            threadsInheritContextClassLoaderOfInitializingThread: true

注意initialize-schema: never - 集群模式需自行初始化

查看官方脚本:https://github.com/quartz-scheduler/quartz/tree/master/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore
您可以通过 liquibase/flyway/etc 使用它,但删除 DROP ... 查询!这就是为什么在集群中我们不自动初始化模式。

quartz docs
参见 spring boot docs quartz
参见 article with example