Tác vụ theo lịch trình mùa xuân chạy trong môi trường được phân nhóm


98

Tôi đang viết một ứng dụng có công việc cron thực hiện cứ sau 60 giây. Ứng dụng được định cấu hình để mở rộng quy mô khi được yêu cầu trên nhiều phiên bản. Tôi chỉ muốn thực hiện nhiệm vụ trên 1 phiên bản cứ sau 60 giây (Trên bất kỳ nút nào). Ngoài ra, tôi không thể tìm ra giải pháp cho điều này và tôi ngạc nhiên vì nó đã không được hỏi nhiều lần trước đây. Tôi đang sử dụng Spring 4.1.6.

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

7
Tôi nghĩ Quartz là giải pháp tốt nhất cho bạn: stackoverflow.com/questions/6663182/…
selalerer

Bất kỳ đề xuất về việc sử dụng CronJobtrong kubernetes?
ch271828n

Câu trả lời:


97

Có một dự án ShedLock phục vụ chính xác mục đích này. Bạn chỉ cần chú thích các tác vụ sẽ bị khóa khi thực thi

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

Định cấu hình Spring và LockProvider

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

1
Tôi chỉ muốn nói rằng "Làm tốt lắm!". Nhưng ... Tính năng tuyệt vời sẽ là nếu thư viện có thể phát hiện ra tên cơ sở dữ liệu mà không cần cung cấp nó rõ ràng trong mã ... Ngoại trừ việc nó hoạt động xuất sắc!
Krzysiek

Làm việc cho tôi với bộ khởi động dữ liệu khởi động Oracle và Spring jpa.
Mahendran Ayyarsamy Kandiar

Giải pháp này có hoạt động cho Spring 3.1.1.RELEASE và java 6 không? Hãy cho biết.
Vikas Sharma

Tôi đã thử với MsSQL và Spring boot JPA và tôi đã sử dụng tập lệnh liquibase cho phần SQL .. hoạt động tốt .. Cảm ơn
sheetal

Nó thực sự hoạt động tốt. Tuy nhiên, tôi đã gặp một trường hợp hơi phức tạp ở đây, bạn có thể vui lòng xem qua. Cảm ơn!!! stackoverflow.com/questions/57691205/…
Dayton Wang,


15

Đây là một cách đơn giản và mạnh mẽ khác để thực hiện an toàn một công việc trong một cụm. Bạn có thể dựa trên cơ sở dữ liệu và thực thi nhiệm vụ chỉ khi nút là "đầu tàu" trong cụm.

Ngoài ra, khi một nút bị lỗi hoặc tắt trong cụm, một nút khác trở thành người dẫn đầu.

Tất cả những gì bạn có là tạo một cơ chế "bầu cử lãnh đạo" và mọi lúc để kiểm tra xem bạn có phải là người lãnh đạo hay không:

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

Làm theo các bước sau:

1. Xác định đối tượng và bảng chứa một mục nhập cho mỗi nút trong cụm:

@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.Tạo dịch vụ a) chèn nút vào cơ sở dữ liệu, b) kiểm tra nhà lãnh đạo

@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. nhập cơ sở dữ liệu để gửi rằng bạn còn sống

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

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

4. bạn đã sẵn sàng! Chỉ cần kiểm tra xem bạn có phải là người lãnh đạo hay không trước khi thực hiện nhiệm vụ:

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

Trong trường hợp này SystemService và SettingEnum là gì? Có vẻ như nó cực kỳ đơn giản và chỉ trả về một giá trị thời gian chờ. Trong trường hợp đó, tại sao không chỉ cần mã cứng thời gian chờ?
tlavarea

@mspapant, SettingEnum.SYSTEM_CONFIGURATION_SYSTEM_NODE_ALIVE_TIMEOUT là gì? Giá trị tối ưu mà tôi nên sử dụng ở đây là gì?
user525146 21/09/17

@tlavarea bạn đã triển khai mã này chưa, tôi có câu hỏi về phương thức DateUtils.hasExpired? nó là phương thức tùy chỉnh hay nó là một utils chung của apache?
user525146 22/09/17

10

Các công việc hàng loạt và theo lịch trình thường được chạy trên các máy chủ độc lập của riêng chúng, tránh xa các ứng dụng mà khách hàng phải đối mặt, vì vậy không phải là yêu cầu phổ biến khi đưa công việc vào một ứng dụng dự kiến ​​chạy trên một cụm. Ngoài ra, các công việc trong môi trường nhóm thường không cần phải lo lắng về các trường hợp khác của cùng một công việc đang chạy song song, vì vậy một lý do khác khiến việc cô lập các trường hợp công việc không phải là một yêu cầu lớn.

Một giải pháp đơn giản là định cấu hình công việc của bạn bên trong Spring Profile. Ví dụ: nếu cấu hình hiện tại của bạn là:

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

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

thay đổi nó thành:

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

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

Sau đó, khởi chạy ứng dụng của bạn chỉ trên một máy có scheduledcấu hình được kích hoạt ( -Dspring.profiles.active=scheduled).

Nếu máy chủ chính không khả dụng vì lý do nào đó, chỉ cần khởi chạy một máy chủ khác với cấu hình được bật và mọi thứ sẽ tiếp tục hoạt động tốt.


Mọi thứ thay đổi nếu bạn muốn chuyển đổi dự phòng tự động cho các công việc. Sau đó, bạn sẽ cần duy trì công việc chạy trên tất cả các máy chủ và kiểm tra đồng bộ hóa thông qua một tài nguyên chung như bảng cơ sở dữ liệu, bộ đệm được phân cụm, biến JMX, v.v.


58
Đây là một cách giải quyết hợp lệ, nhưng điều này sẽ vi phạm ý tưởng đằng sau việc có một môi trường được phân cụm, trong đó nếu một nút gặp sự cố, nút kia có thể phục vụ các yêu cầu khác. Trong cách giải quyết này, nếu nút có cấu hình "đã lên lịch" gặp sự cố, thì công việc nền này sẽ không chạy
Ahmed Hashem

3
Tôi nghĩ chúng ta có thể sử dụng Redis với nguyên tử getsethoạt động để lưu trữ điều đó.
Thanh Nguyen Van

Có một số vấn đề với đề xuất của bạn: 1. Nói chung, bạn muốn mỗi nút của một cụm có cùng một cấu hình chính xác, vì vậy chúng sẽ có thể hoán đổi cho nhau 100% và yêu cầu cùng một tài nguyên dưới cùng một tải mà chúng chia sẻ. 2. Giải pháp của bạn sẽ yêu cầu can thiệp thủ công khi nút "tác vụ" gặp trục trặc. 3. Nó vẫn sẽ không đảm bảo rằng công việc thực sự đã được chạy thành công, vì nút "tác vụ" đã bị lỗi trước khi nó xử lý xong việc thực thi hiện tại và "bộ chạy tác vụ" mới đã được tạo sau khi nút đầu tiên bị lỗi, không biết liệu có nó đã hoàn thành hay chưa.
Moshe Bixenshpaner

1
nó chỉ đơn giản là vi phạm ý tưởng về môi trường phân cụm, không thể có bất kỳ giải pháp nào với cách tiếp cận bạn đã đề xuất. Bạn không thể sao chép ngay cả các máy chủ hồ sơ để đảm bảo tính khả dụng vì điều đó sẽ dẫn đến chi phí bổ sung và lãng phí tài nguyên không cần thiết. Giải pháp do @Thanh gợi ý là sạch hơn nhiều. Hãy nghĩ giống như một MUTEX. Bất kỳ máy chủ nào đang chạy script sẽ có được một khóa tạm thời trong một số bộ đệm phân tán như redis và sau đó tiếp tục với các khái niệm về khóa truyền thống.
anuj pradhan

2

dlock được thiết kế để chạy các tác vụ chỉ một lần bằng cách sử dụng các chỉ mục và ràng buộc cơ sở dữ liệu. Bạn có thể đơn giản làm điều gì đó như dưới đây.

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

}

Xem bài viết về cách sử dụng nó.


3
Nếu sử dụng dlock .Assume, chúng ta đang sử dụng DB để duy trì khóa. Và một trong những nút trong cụm bị sập bất ngờ sau khi khóa lại thì điều gì sẽ xảy ra trong trường hợp này? Nó sẽ ở trạng thái bế tắc?
Badman

1

Tôi đang sử dụng một bảng cơ sở dữ liệu để thực hiện khóa. Mỗi lần chỉ có một tác vụ có thể thực hiện chèn vào bảng. Cái còn lại sẽ nhận được một DuplicateKeyException. Logic chèn và xóa được xử lý theo một khía cạnh xung quanh chú thích @Schedised. Tôi đang sử dụng 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
Bạn có nghĩ rằng nó sẽ hoạt động hoàn hảo? Bởi vì nếu một trong các nút sẽ bị khóa sau khi khóa thì những người khác sẽ không biết tại sao lại có khóa (trong trường hợp của bạn là mục nhập hàng tương ứng với công việc trong bảng).
Badman

0

Bạn có thể sử dụng một công cụ lập lịch có thể nhúng như db-Scheduler để thực hiện điều này. Nó có các thực thi liên tục và sử dụng cơ chế khóa lạc quan đơn giản để đảm bảo thực thi bởi một nút duy nhất.

Mã ví dụ về cách có thể đạt được ca sử dụng:

   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 context không được phân cụm nên việc quản lý tác vụ trong ứng dụng phân tán hơi khó khăn và bạn cần sử dụng hệ thống hỗ trợ jgroup để đồng bộ hóa trạng thái và để tác vụ của bạn được ưu tiên thực hiện hành động. Hoặc bạn có thể sử dụng ngữ cảnh ejb để quản lý dịch vụ ha singleton theo cụm như môi trường jboss ha https://developers.redhat.com/quickstarts/eap/cluster-ha-singleton/?referrer=jbd Hoặc bạn có thể sử dụng bộ nhớ đệm theo cụm và tài nguyên khóa truy cập giữa dịch vụ và dịch vụ đầu tiên lấy khóa sẽ định dạng hành động hoặc triển khai jgroup của bạn để giao tiếp dịch vụ của bạn và thực hiện hành động trên một nút

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.