Gần như trong phần mềm nào cũng có chức năng lập lịch để làm một thứ gì đó định kỳ, hiểu được điều này EzyPlatform đã đóng gói lại thành một lớp có tên Scheduler.

Thiết kế của Scheduler

Sẽ có hai thiết kế chính trong Scheduler:

  1. Thiết kế thành phần.
  2. Thiết kế luồng.

Thiết kế thành phần

Bên trong Scheduler cũng chỉ có hai thành phần chính là các task và các luồng.

EzyPlatform scheduler.png

Các task này trong suốt với các nhà phát triển, vì khi các nhà phát triển sử dụng họ chỉ truyền vào command ở dạng Runnable, các command này chỉ làm nhiệm vụ thực thi. Khi truyền vào các hàm của Scheduler thì command sẽ được bọc lại bởi một Task để tính toán logic về thời gian thực thi và số lần thực thi, đây là một trong những ứng dụng quan trọng của proxy design pattern. Mã nguồn của lớp Task cũng chỉ đơn giản như sau:

private static class Task {
    final Runnable command;
    final boolean runForever;
    final long periodMillis;
    final AtomicLong nexRunTime = new AtomicLong();

    Task(
        Runnable command,
        boolean runForever,
        long initialDelay,
        long period,
        TimeUnit unit
    ) {
        this.command = command;
        this.runForever = runForever;
        this.periodMillis = unit.toMillis(period);
        this.nexRunTime.set(System.currentTimeMillis() + unit.toMillis(initialDelay));
    }

    void calculateNextRunTime() {
        this.nexRunTime.addAndGet(periodMillis);
    }
}

Hàm calculateNextRunTime tương đối quan trọng, nó sẽ tính thời gian tiếp theo mà task được chạy nếu như task này là chạy mãi mãi.

Thiết kế luồng

Scheduler sẽ tổ chức thành 2 loại luồng:

EzyPlatform scheduler thread.png
  1. Inspector thread: Là một luồng duy nhất chạy định kỳ, nghỉ tối đa 5 milli giây. Luồng này có tranh nhiệm thanh toàn bộ các task xem đã có task nào đến thời điểm được chạy chưa, nếu đến rồi thì nó sẽ lấy một trong các worker thread ra để thực thi command trong task.
  2. Các worker thread: Là các luồng để thực thi command được bọc trong task, các worker thread này được quản lý bởi thread pool.

Việc tổ chức 1 luồng thanh tra như thế này để tránh gây ra tình trạng race condition trong việc kiểm tra các task đã đến thời điểm được chạy chưa.

Các hàm trong Scheduler

Có 3 hàm mà bạn sẽ hay sử dụng nhất đó là:

  1. scheduleOneTime: Chạy một command duy nhất một lần.
  2. scheduleAtFixRate: Chạy định kỳ một command.
  3. cancelSchedule: Dừng dạy một command.

Hàm scheduleOneTime

Hàm này có các tham số như sau:

public void scheduleOneTime(
    Runnable command,
    long delayTime,
    TimeUnit unit
)
  1. command: Là công việc mà bạn cần thực hiện một lần.
  2. delayTime: Là thời gian trì hoãn cho đến khi command được thực thi.
  3. unit: Là đơn vị thời gian cho việc trì hoãn, ví dụ milli giây, giây, phút, giờ, ngày, ...

Hàm scheduleAtFixRate

Đây là hàm mà bạn sẽ hay sử dụng nhất và nó có các tham số như sau:

public void scheduleAtFixRate(
    Runnable command,
    long initialDelay,
    long period,
    TimeUnit unit
)
  1. command: Là công việc mà bạn cần thực hiện một lần.
  2. initialDelay: Là thời gian trì hoãn cho đến khi command được thực thi lần đầu.
  3. period: Là khoảng thời gian định kỳ mà command sẽ được chạy sau lần đầu tiên.
  4. unit: Là đơn vị thời gian cho việc trì hoãn và thời gian định kỳ, ví dụ milli giây, giây, phút, giờ, ngày, ...

Hàm cancelSchedule

Hàm này sẽ bao gồm 1 tham số duy nhất là command mà bạn muốn dừng chạy định kỳ.

public void cancelSchedule(Runnable command)

Sử dụng

Hãy nói bạn cần phải backup dữ liệu định kỳ 24 giờ một lần bạn có thể sử dụng Scheduler với mã nguồn như sau:

scheduler.scheduleAtFixRate(
    ezyPlatformBackupDeletionWorker,
    30,
    24 * 60 * 60,
    TimeUnit.SECONDS
);

Hay bạn cần phải thực thi một công việc định kỳ theo một mốc thời gian nào đó trong ngày bạn có thể sử dụng mã nguồn:

long initDelayTime = calculateInitDelayTime(
    schedulerSetting.getRunTime()
);
long period = calculatePeriodTime(
    schedulerSetting.getPeriodInDay()
);
logger.info(
    "start data backup scheduler, " +
        "initDelayTime: {} seconds " +
        "period: {} day",
    initDelayTime / 1000,
    period / (24 * 60 * 60 * 1000)
);
Runnable command = this::runWorkers;
scheduler.scheduleAtFixRate(
    command,
    initDelayTime,
    period,
    TimeUnit.MILLISECONDS
);
runningSchedulerFuture.set(future);

Và đến khi cần dừng lại bạn có thể gọi hàm:

scheduler.cancelSchedule(command);

Tổng kết

Lập lịch là tính năng rất quan trọng trong EzyPlatform, vì có rất nhiều các công việc cần thực hiện định kỳ như cache dữ liệu, backup dữ liệu, xoá log, ... Để hạn chế sinh ra quá nhiều thread và tối ưu hiệu năng, chúng tôi đề xuất bạn sử dụng lớp Scheduler này cho các công việc cần lập lịch nhé.