還沒真正的遇到使用定時任務的場景,不管怎麼說先學起來
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