Java 定時任務

Howlet發表於2021-11-20

還沒真正的遇到使用定時任務的場景,不管怎麼說先學起來


1. 定時任務

很多情況下任務並非需要立即執行,而是需要往後或定期執行,這不可能人工去操作,所以定時任務就出現了。專案中肯定會用到使用定時任務的情況,筆者就需要定時去拉取埋點資料


使用定時任務的情況:

  • 每週末凌晨備份資料
  • 觸發條件 5 分鐘後傳送郵件通知
  • 30 分鐘未支付取消訂單
  • 每 1 小時去拉取資料
  • ......






2. Thread實現

筆試中首次遇到定時任務急急忙忙想出來的方法


2.1 使用

public class ThreadSchedule {
    public static void main(String[] args) {
      
        // 5 秒後執行任務
        int interval = 1000 * 5;

        // 新執行緒執行
        new Thread(() -> {
            try {
                Thread.sleep(interval);
                System.out.println("執行定時任務");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}


2.2 分析

  • 定時不準確,因依賴底層硬體,Windows誤差為10微妙
  • System.currentTimeMillis() 依賴系統硬體,還會受網路時間同步修改
  • System.nanoTime() 依賴 JVM 的執行納秒數,並不受同步影響,適用於計算準確的時間差
  • 但計算當前日期還是要使用 currentTimeMillis 的格林威治時間,而 nanoTime 計算 JVM 執行時間不準確






3. java.util.Timer

Timer 負責執行計劃功能,會啟動一個後臺執行緒,而 TimerTask 負責任務邏輯


3.1 使用

public class TimeSchedule {
    public static void main(String[] args) {
        
        // 啟動一個守護執行緒
        Timer timer = new Timer();  
        
        // 定時任務
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println("執行定時任務");
            }
        };

        long delay = 0;     		// 延遲執行
        long period = 1000 * 5;  	// 間隔
        timer.scheduleAtFixedRate(timerTask, 1, period);
        // timer.cancel();			   可取消
    }
}


3.2 分析

// new Timer() 最終的建構函式會啟動一個執行緒
public Timer(String name) {
    thread.setName(name);
    thread.start();
}

// 這個執行緒裡面封裝了一個 Queue 優先順序佇列,該執行緒會去佇列裡不停執行裡面的任務
class TimerThread extends Thread {
    private TaskQueue queue;
    TimerThread(TaskQueue queue) {
        this.queue = queue;
    }
}

// 這個佇列裡面存放了各種 TimerTask 定時的任務邏輯
class TaskQueue {
    private TimerTask[] queue = new TimerTask[128];
}
  • 只有一個單執行緒執行,所以是序列執行
  • 某個任務執行時間較長會阻塞後面預定執行的任務,所以時間並不準確
  • 執行緒報錯後續的定時任務直接停止






4. ScheduledExecutorService

java.util.concurrent中的工具類,是一個多執行緒的定時器


4.1 使用

public class ExecutorSchedule {
    public static void main(String[] args) {
        
        // 定時任務
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("執行定時任務");
            }
        };

        // 執行緒池執行
        long delay = 0;     		// 延遲執行
        long period = 1000 * 5;  	// 間隔
        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
        service.scheduleAtFixedRate(runnable, delay, period, TimeUnit.MILLISECONDS);
}

筆者最常用的一個定時操作了,之前還寫過定時的探測任務







5. Spring的定時任務

需要開啟定時功能@EnableScheduling

@Component
public class SpringSchedule {
    
    // cron 表示式,每秒執行一次
    
    @Scheduled(cron = "*/1 * * * * ?")
    public void springSchedule(){
        System.out.println("執行定時任務");
    }
    
}

底層是 ScheduledThreadPoolExecutor 執行緒池,和上面的 ScheduledExecutorService 是同根同源







6. XXL-JOB

xxl-job 是個人維護的分散式任務排程框架(國人寫的,有詳細的中文文件),分為 排程中心 和 執行器。執行器就是定時任務,而排程中心則負責管理呼叫這些定時任務,排程中心也可以儲存定時任務通過指令碼形式(Java 是 Grovvy)免編譯地實時下發到各服務中執行。最重要的是有 UI 介面,使用者友好的體驗


6.1 建立資料庫

xxl-job 的儲存是基於資料庫的,相對比 quartz 可儲存在記憶體和資料庫有一點效能影響。首先第一步就是要建庫,在 xxl-job 官網有 SQL 語句 tables_xxl_job.sql,直接執行即可建庫建表



6.2 部署 xxl-job-admin 排程中心

從 Git 上拉取最新的程式碼,然後編譯根模組,填好 admin 模組的資料庫地址等,即可啟動這個排程中心(支援 Docker 部署,更加方便)



6.3 建立定時任務

在需要定時任務的服務中 引入依賴、新增配置、建立定時任務



6.3.1 依賴

<!-- xxl-job-core -->
<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>${project.parent.version}</version>
</dependency>

6.3.2 基本配置

# xxl-job admin address
xxl.job.admin.addresses=http://xxx.xxx.xxx:8080/xxl-job-admin

# xxl-job executor appname
xxl.job.executor.appname=xxl-job-executor-demo

6.3.3 定時任務

@Component
public class MyJob {
    
    @XxlJob("MyJob")
    public void MyJob() throws Exception {
        
        // 執行器日誌記錄
        XxlJobHelper.log("myjob is execute");
        
        // 定時任務邏輯
        System.out.println("myjob is executing");
        
        // default success
    }
}


6.4 執行定時任務

進入排程中心新建一個任務,然後執行定時任務即可(使用的是 RPC 遠端過程呼叫)



6.5 遇到的問題

預設執行器是自動註冊到排程中心的,但是時常進去的地址有問題而導致執行失敗,所以要手動錄入執行器的地址



6.6 分析

作為輕量級的分散式定時任務,有 UI 介面簡單方便使用,而且對程式碼沒什麼侵入性,已經能滿足大部分專案的需求了,筆者如果要用定時任務也會首選 xxl-job