在群集环境中运行的Spring Scheduled Task


97

我正在编写一个具有cron作业的应用程序,该作业每60秒执行一次。该应用程序被配置为在需要时扩展到多个实例。我只想每60秒(在任何节点上)在1个实例上执行任务。开箱即用,我找不到解决方案,但令我惊讶的是,之前没有多次被问到。我正在使用Spring 4.1.6。

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

7
我认为石英是你最好的解决办法:stackoverflow.com/questions/6663182/...
selalerer

使用任何建议CronJobkubernetes
ch271828n

Answers:


97

确实有一个ShedLock项目可以满足此目的。您只需注释执行时应锁定的任务

@Scheduled( ... )
@SchedulerLock(name = "scheduledTaskName")
public void scheduledTask() {
   // do something
}

配置Spring和LockProvider

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

1
我只想说“好工作!”。但是...一个不错的功能是,如果库可以发现数据库名称而无需在代码中提供明确的名称,则...除了它的出色表现外!
Krzysiek '17

适用于Oracle和Spring引导数据jpa启动程序。
Mahendran Ayyarsamy Kandiar

此解决方案是否适用于Spring 3.1.1.RELEASE和Java 6?请告诉。
Vikas Sharma

我尝试了MsSQL和Spring Boot JPA,并在SQL部分中使用了liquibase脚本。.效果很好..谢谢
片面

确实运作良好。但是我在这里遇到了一些复杂的情况,请您看看。谢谢!!!stackoverflow.com/questions/57691205/...
顿王


15

这是在集群中安全执行作业的另一种简单而强大的方法。仅当节点是集群中的“领导者”时,才可以基于数据库并执行任务。

同样,当一个节点发生故障或在集群中关闭时,另一个节点将成为领导者。

您所要做的就是创建“领导者选举”机制,并每次检查您是否是领导者:

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

请遵循以下步骤:

1.定义在集群中每个节点上包含一个条目的对象和表:

@Entity(name = "SYS_NODE")
public class SystemNode {

/** The id. */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

/** The name. */
@Column(name = "TIMESTAMP")
private String timestamp;

/** The ip. */
@Column(name = "IP")
private String ip;

/** The last ping. */
@Column(name = "LAST_PING")
private Date lastPing;

/** The last ping. */
@Column(name = "CREATED_AT")
private Date createdAt = new Date();

/** The last ping. */
@Column(name = "IS_LEADER")
private Boolean isLeader = Boolean.FALSE;

public Long getId() {
    return id;
}

public void setId(final Long id) {
    this.id = id;
}

public String getTimestamp() {
    return timestamp;
}

public void setTimestamp(final String timestamp) {
    this.timestamp = timestamp;
}

public String getIp() {
    return ip;
}

public void setIp(final String ip) {
    this.ip = ip;
}

public Date getLastPing() {
    return lastPing;
}

public void setLastPing(final Date lastPing) {
    this.lastPing = lastPing;
}

public Date getCreatedAt() {
    return createdAt;
}

public void setCreatedAt(final Date createdAt) {
    this.createdAt = createdAt;
}

public Boolean getIsLeader() {
    return isLeader;
}

public void setIsLeader(final Boolean isLeader) {
    this.isLeader = isLeader;
}

@Override
public String toString() {
    return "SystemNode{" +
            "id=" + id +
            ", timestamp='" + timestamp + '\'' +
            ", ip='" + ip + '\'' +
            ", lastPing=" + lastPing +
            ", createdAt=" + createdAt +
            ", isLeader=" + isLeader +
            '}';
}

}

2.创建以下服务:a)将节点插入数据库,b)检查领导者

@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);
    }
}

}

3.ping数据库以发送您还活着的消息

@Override
@Scheduled(cron = "0 0/5 * * * ?")
public void executeSystemNodePing() {
    systemNodeService.pingNode();
}

@Override
@Scheduled(cron = "0 0/10 * * * ?")
public void executeLeaderResolution() {
    systemNodeService.checkLeaderShip();
}

4.您准备好了!只需在执行任务之前检查您是否为领导者即可:

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

在这种情况下,什么是SystemService和SettingEnum?看起来非常简单,只是返回一个超时值。在那种情况下,为什么不只是硬编码超时呢?
tlavarea

@mspapant,什么是SettingEnum.SYSTEM_CONFIGURATION_SYSTEM_NODE_ALIVE_TIMEOUT?我在这里应该使用的最佳值是多少?
user525146

@tlavarea您是否实现了此代码,我对DateUtils.hasExpired方法有疑问吗?它是自定义方法还是apache通用工具?
user525146

10

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

一个简单的解决方案是在Spring Profile中配置您的作业。例如,如果您当前的配置是:

<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>

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

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


如果您还想为作业进行自动故障转移,那么事情将会改变。然后,您将需要使作业保持在所有服务器上运行,并通过公用资源(例如数据库表,集群缓存,JMX变量等)检查同步。


57
这是一个有效的解决方法,但这将违反集群环境的思想,在集群环境中,如果一个节点发生故障,则另一个节点可以处理其他请求。在这种解决方法中,如果具有“计划的”配置文件的节点出现故障,则该后台作业将不会运行
Ahmed Hashem 2015年

3
我认为,我们可以使用Redis的原子序数getset操作archieve这一点。
Thanh Nguyen Van

您的建议有几个问题:1.您通常希望集群的每个节点都具有完全相同的配置,因此它们将100%可互换,并且在它们共享的相同负载下需要相同的资源。2.当“任务”节点出现故障时,您的解决方案将需要手动干预。3.仍然不能保证该作业实际上已成功运行,因为“任务”节点在完成对当前执行的处理之前就已关闭,并且在第一个节点关闭后已创建了新的“任务运行器”,不知道是否它是否完成。
Moshe Bixenshpaner

1
它完全违反了集群环境的想法,您建议的方法无法解决任何问题。您甚至无法复制配置文件服务器来确保可用性,因为这将导致额外的成本以及不必要的资源浪费。@Thanh建议的解决方案比这更干净。就像MUTEX一样。任何运行脚本的服务器都将在Redis等分布式缓存中获得临时锁定,然后继续使用传统锁定的概念。
阿努·普拉丹(Anuj Pradhan)

2

dlock旨在通过使用数据库索引和约束仅运行一次任务。您可以简单地执行以下操作。

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

}

请参阅有关使用它的文章


3
如果使用dlock。假设我们使用DB来保持锁定。并且集群中的一个节点在锁定后意外崩溃,那么在这种情况下会发生什么?会处于死锁状态吗?
Badman '18

1

我正在使用数据库表进行锁定。一次仅一个任务可以对表进行插入。另一个将获得DuplicateKeyException。插入和删除逻辑由@Scheduled批注周围的一个方面处理。我正在使用Spring Boot 2.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;
        }
    }
}


@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)
);

3
您认为它会完美运行吗?因为如果节点中的一个节点在获得锁定后会崩溃,那么其他节点将不知道为什么会有锁定(在您的情况下,该行条目对应于表中的job)。
Badman '18

0

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

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

   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();

-1

Spring上下文不是集群的,因此在分布式应用程序中管理任务有点困难,您需要使用支持jgroup的系统来同步状态并使您的任务具有执行操作的优先权。或者,您可以使用ejb上下文来管理群集的ha单例服务,例如jboss ha环境 https://developers.redhat.com/quickstarts/eap/cluster-ha-singleton/?referrer=jbd 或您可以使用群集的缓存并访问锁定资源在服务和第一个服务之间采取锁定将形成一个动作或实现您自己的jgroup以传达您的服务并执行一个节点的动作

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.