在群集环境中运行的Spring计划任务

在群集环境中运行的Spring计划任务,spring,spring-scheduled,Spring,Spring Scheduled,我正在编写一个应用程序,它有一个cron作业,每60秒执行一次。应用程序配置为在需要时可扩展到多个实例。我只想每60秒在一个实例上执行一次任务(在任何节点上)。开箱即用,我找不到解决这个问题的办法,我很惊讶以前没有人问过多次。我使用的是Spring 4.1.6 <task:scheduled-tasks> <task:scheduled ref="beanName" method="execute" cron="0/60 * * * * *"/>

我正在编写一个应用程序,它有一个cron作业,每60秒执行一次。应用程序配置为在需要时可扩展到多个实例。我只想每60秒在一个实例上执行一次任务(在任何节点上)。开箱即用,我找不到解决这个问题的办法,我很惊讶以前没有人问过多次。我使用的是Spring 4.1.6

    <task:scheduled-tasks>
        <task:scheduled ref="beanName" method="execute" cron="0/60 * * * * *"/>
    </task:scheduled-tasks>

批处理作业和计划作业通常在它们自己的独立服务器上运行,远离面向客户的应用程序,因此通常不需要在预期在群集上运行的应用程序中包含作业。此外,集群环境中的作业通常不需要担心同一作业的其他实例并行运行,因此作业实例隔离不是一个大要求的另一个原因

一个简单的解决方案是在Spring概要文件中配置作业。例如,如果当前配置为:

<beans>
  <bean id="someBean" .../>

  <task:scheduled-tasks>
    <task:scheduled ref="someBean" method="execute" cron="0/60 * * * * *"/>
  </task:scheduled-tasks>
</beans>

将其更改为:

<beans>
  <beans profile="scheduled">
    <bean id="someBean" .../>

    <task:scheduled-tasks>
      <task:scheduled ref="someBean" method="execute" cron="0/60 * * * * *"/>
    </task:scheduled-tasks>
  </beans>
</beans>

然后,仅在一台机器上启动应用程序,并激活
计划的
配置文件(
-Dspring.profiles.active=scheduled

如果主服务器由于某种原因变得不可用,只需启动另一台启用了配置文件的服务器,一切就会继续正常工作



如果您也希望作业自动故障切换,情况会发生变化。然后,您需要在所有服务器上保持作业运行,并通过公共资源(如数据库表、群集缓存、JMX变量等)检查同步。

这是在群集中安全执行作业的另一种简单而健壮的方法。只有当节点是集群中的“领导者”时,才能基于数据库执行任务

此外,当集群中的一个节点出现故障或关闭时,另一个节点将成为领导者

你所要做的就是创建一个“领导人选举”机制,每次检查你是否是领导人:

@Scheduled(cron = "*/30 * * * * *")
public void executeFailedEmailTasks() {
    if (checkIfLeader()) {
        final List<EmailTask> list = emailTaskService.getFailedEmailTasks();
        for (EmailTask emailTask : list) {
            dispatchService.sendEmail(emailTask);
        }
    }
}
}

2.创建服务,a)在数据库中插入节点,b)检查leader

@Service
@Transactional
public class SystemNodeServiceImpl implements SystemNodeService,    ApplicationListener {

/** The logger. */
private static final Logger LOGGER = Logger.getLogger(SystemNodeService.class);

/** The constant NO_ALIVE_NODES. */
private static final String NO_ALIVE_NODES = "Not alive nodes found in list {0}";

/** The ip. */
private String ip;

/** The system service. */
private SystemService systemService;

/** The system node repository. */
private SystemNodeRepository systemNodeRepository;

@Autowired
public void setSystemService(final SystemService systemService) {
    this.systemService = systemService;
}

@Autowired
public void setSystemNodeRepository(final SystemNodeRepository systemNodeRepository) {
    this.systemNodeRepository = systemNodeRepository;
}

@Override
public void pingNode() {
    final SystemNode node = systemNodeRepository.findByIp(ip);
    if (node == null) {
        createNode();
    } else {
        updateNode(node);
    }
}

@Override
public void checkLeaderShip() {
    final List<SystemNode> allList = systemNodeRepository.findAll();
    final List<SystemNode> aliveList = filterAliveNodes(allList);

    SystemNode leader = findLeader(allList);
    if (leader != null && aliveList.contains(leader)) {
        setLeaderFlag(allList, Boolean.FALSE);
        leader.setIsLeader(Boolean.TRUE);
        systemNodeRepository.save(allList);
    } else {
        final SystemNode node = findMinNode(aliveList);

        setLeaderFlag(allList, Boolean.FALSE);
        node.setIsLeader(Boolean.TRUE);
        systemNodeRepository.save(allList);
    }
}

/**
 * Returns the leaded
 * @param list
 *          the list
 * @return  the leader
 */
private SystemNode findLeader(final List<SystemNode> list) {
    for (SystemNode systemNode : list) {
        if (systemNode.getIsLeader()) {
            return systemNode;
        }
    }
    return null;
}

@Override
public boolean isLeader() {
    final SystemNode node = systemNodeRepository.findByIp(ip);
    return node != null && node.getIsLeader();
}

@Override
public void onApplicationEvent(final ApplicationEvent applicationEvent) {
    try {
        ip = InetAddress.getLocalHost().getHostAddress();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    if (applicationEvent instanceof ContextRefreshedEvent) {
        pingNode();
    }
}

/**
 * Creates the node
 */
private void createNode() {
    final SystemNode node = new SystemNode();
    node.setIp(ip);
    node.setTimestamp(String.valueOf(System.currentTimeMillis()));
    node.setCreatedAt(new Date());
    node.setLastPing(new Date());
    node.setIsLeader(CollectionUtils.isEmpty(systemNodeRepository.findAll()));
    systemNodeRepository.save(node);
}

/**
 * Updates the node
 */
private void updateNode(final SystemNode node) {
    node.setLastPing(new Date());
    systemNodeRepository.save(node);
}

/**
 * Returns the alive nodes.
 *
 * @param list
 *         the list
 * @return the alive nodes
 */
private List<SystemNode> filterAliveNodes(final List<SystemNode> list) {
    int timeout = systemService.getSetting(SettingEnum.SYSTEM_CONFIGURATION_SYSTEM_NODE_ALIVE_TIMEOUT, Integer.class);
    final List<SystemNode> finalList = new LinkedList<>();
    for (SystemNode systemNode : list) {
        if (!DateUtils.hasExpired(systemNode.getLastPing(), timeout)) {
            finalList.add(systemNode);
        }
    }
    if (CollectionUtils.isEmpty(finalList)) {
        LOGGER.warn(MessageFormat.format(NO_ALIVE_NODES, list));
        throw new RuntimeException(MessageFormat.format(NO_ALIVE_NODES, list));
    }
    return finalList;
}

/**
 * Finds the min name node.
 *
 * @param list
 *         the list
 * @return the min node
 */
private SystemNode findMinNode(final List<SystemNode> list) {
    SystemNode min = list.get(0);
    for (SystemNode systemNode : list) {
        if (systemNode.getTimestamp().compareTo(min.getTimestamp()) < -1) {
            min = systemNode;
        }
    }
    return min;
}

/**
 * Sets the leader flag.
 *
 * @param list
 *         the list
 * @param value
 *         the value
 */
private void setLeaderFlag(final List<SystemNode> list, final Boolean value) {
    for (SystemNode systemNode : list) {
        systemNode.setIsLeader(value);
    }
}
你准备好了!在执行任务之前,只需检查您是否是领导者:

@Override
@Scheduled(cron = "*/30 * * * * *")
public void executeFailedEmailTasks() {
    if (checkIfLeader()) {
        final List<EmailTask> list = emailTaskService.getFailedEmailTasks();
        for (EmailTask emailTask : list) {
            dispatchService.sendEmail(emailTask);
        }
    }
}
@覆盖
@已计划(cron=“*/30*****”)
public void executeFailedEmailTasks(){
if(checkiReader()){
最终列表列表=emailTaskService.getFailedEmailTasks();
对于(EmailTask EmailTask:列表){
dispatchService.sendEmail(emailTask);
}
}
}
我认为您必须为此目的使用一个项目。您只需对执行时应锁定的任务进行注释

@Scheduled( ... )
@SchedulerLock(name = "scheduledTaskName")
public void scheduledTask() {
   // do something
}
配置Spring和锁提供程序

@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
class MySpringConfiguration {
    ...
    @Bean
    public LockProvider lockProvider(DataSource dataSource) {
       return new JdbcTemplateLockProvider(dataSource);
    }
    ...
}

您可以使用类似的嵌入式调度器来完成此任务。它具有持久执行,并使用简单的乐观锁定机制来保证单个节点的执行

如何实现用例的示例代码:

   RecurringTask<Void> recurring1 = Tasks.recurring("my-task-name", FixedDelay.of(Duration.ofSeconds(60)))
    .execute((taskInstance, executionContext) -> {
        System.out.println("Executing " + taskInstance.getTaskAndInstance());
    });

   final Scheduler scheduler = Scheduler
          .create(dataSource)
          .startTasks(recurring1)
          .build();

   scheduler.start();
RecurringTask recurring1=Tasks.recurring(“我的任务名称”,FixedDelay.of(持续时间秒(60)))
.execute((taskInstance,executionContext)->{
System.out.println(“正在执行”+taskInstance.getTaskAndInstance());
});
最终调度程序=调度程序
.create(数据源)
.startTasks(重复出现1)
.build();
scheduler.start();

我正在使用一个数据库表进行锁定。一次只能有一个任务可以对表进行插入。另一个将获得DuplicateKeyException。 插入和删除逻辑由@Scheduled注释周围的方面处理。 我使用的是SpringBoot2.0

@Component
@Aspect
public class SchedulerLock {

    private static final Logger LOGGER = LoggerFactory.getLogger(SchedulerLock.class);

    @Autowired
    private JdbcTemplate jdbcTemplate;  

    @Around("execution(@org.springframework.scheduling.annotation.Scheduled * *(..))")
    public Object lockTask(ProceedingJoinPoint joinPoint) throws Throwable {

        String jobSignature = joinPoint.getSignature().toString();
        try {
            jdbcTemplate.update("INSERT INTO scheduler_lock (signature, date) VALUES (?, ?)", new Object[] {jobSignature, new Date()});

            Object proceed = joinPoint.proceed();

            jdbcTemplate.update("DELETE FROM scheduler_lock WHERE lock_signature = ?", new Object[] {jobSignature});
            return proceed;

        }catch (DuplicateKeyException e) {
            LOGGER.warn("Job is currently locked: "+jobSignature);
            return null;
        }
    }
}


设计为通过使用数据库索引和约束只运行一次任务。您可以简单地执行以下操作

@Scheduled(cron = "30 30 3 * * *")
@TryLock(name = "executeMyTask", owner = SERVER_NAME, lockFor = THREE_MINUTES)
public void execute() {

}

请参阅关于使用它的。

Spring上下文不是群集的,因此在分布式应用程序中管理任务有点困难,您需要使用支持jgroup的系统来同步状态,并让您的任务优先执行操作。或者您可以使用ejb上下文来管理集群ha单例服务,如jboss ha环境
或者,您可以使用群集缓存并访问服务和第一个服务之间的锁资源。采取锁将形成操作,或者实现您自己的jgroup来通信您的服务并在一个节点上执行操作。我认为Quartz是您的最佳解决方案:关于在
kubernetes
中使用
CronJob
的任何建议是一种有效的解决方法,但这将违反集群环境背后的理念,在集群环境中,如果一个节点关闭,另一个节点可以服务于其他请求。在此解决方法中,如果具有“计划”配置文件的节点关闭,则此后台作业将无法运行。我认为我们可以使用Redis和原子
get
set
操作来归档该配置文件。您的建议有几个问题:1。您通常希望集群中的每个节点都具有完全相同的配置,因此它们将100%可互换,并且在共享相同负载的情况下需要相同的资源。2.当“任务”节点关闭时,您的解决方案需要手动干预。3.它仍然不能保证作业确实成功运行,因为“任务”节点在完成当前执行之前就已关闭,而新的“任务运行器”是在第一个节点关闭之后创建的,不知道它是否已完成。这完全违反了群集环境的理念,你建议的方法不可能有任何解决办法。您甚至不能复制配置文件服务器来确保可用性,因为这将导致额外的成本和不必要的资源浪费。@Thanh建议的解决方案比这个要干净得多。将其视为互斥对象。任何运行该脚本的服务器都将在某些分布式缓存(如redis和Linux)中获得临时锁
@Component
@Aspect
public class SchedulerLock {

    private static final Logger LOGGER = LoggerFactory.getLogger(SchedulerLock.class);

    @Autowired
    private JdbcTemplate jdbcTemplate;  

    @Around("execution(@org.springframework.scheduling.annotation.Scheduled * *(..))")
    public Object lockTask(ProceedingJoinPoint joinPoint) throws Throwable {

        String jobSignature = joinPoint.getSignature().toString();
        try {
            jdbcTemplate.update("INSERT INTO scheduler_lock (signature, date) VALUES (?, ?)", new Object[] {jobSignature, new Date()});

            Object proceed = joinPoint.proceed();

            jdbcTemplate.update("DELETE FROM scheduler_lock WHERE lock_signature = ?", new Object[] {jobSignature});
            return proceed;

        }catch (DuplicateKeyException e) {
            LOGGER.warn("Job is currently locked: "+jobSignature);
            return null;
        }
    }
}
@Component
public class EveryTenSecondJob {

    @Scheduled(cron = "0/10 * * * * *")
    public void taskExecution() {
        System.out.println("Hello World");
    }
}
CREATE TABLE scheduler_lock(
    signature varchar(255) NOT NULL,
    date datetime DEFAULT NULL,
    PRIMARY KEY(signature)
);
@Scheduled(cron = "30 30 3 * * *")
@TryLock(name = "executeMyTask", owner = SERVER_NAME, lockFor = THREE_MINUTES)
public void execute() {

}