叢集及分散式定時任務中介軟體MEE_TIMED

funnyZpC發表於2024-07-19

叢集及分散式定時任務中介軟體MEE_TIMED

轉載請著名出處:https://www.cnblogs.com/funnyzpc/p/18312521

MEE_TIMED一套開源的定時任務中介軟體,MEE_TIMED 簡化了 scheduledshedlock的配置,同時也升級了這兩種中介軟體的能力 ,使定時任務開發更具靈活性的同時
具備叢集及分散式節點的管理,同時也增加了傳參,使之更加強大💪

開發初衷

目前 java 語言下可用的定時任務基礎元件無非這倆: spring scheduled 以及 quartz,其中 scheduled 屬於輕量級的設計 預設整合在 spring-context 包中,所以springboot使用 scheduled 簡單快捷,
既然簡單也必有簡單的侷限(後面會聊),quartz 則屬於重量級的設計,內部提供了 RMIJMX 支援 以及使用基於DB的行鎖使之支援叢集,這都很好,不過內部程式碼設計及擴充套件似乎過於臃腫,不使用表又會退化為 scheduled ~

有時,專案不大不小,但是有叢集需求並且需要保證任務不重複執行,這時就需要 scheduled+shedlock 這樣的搭配,可這樣無法動態傳參,同時增加了業務程式碼的複雜度,這是問題;
當然也可以使用 quartz+資料庫表 的方式 則管理叢集及節點任務會變得比較複雜, 而且任務的啟停及關閉操作在分散式環境下使用 quartz 提供的api操作尤其的麻煩,這也是問題...

  • spring scheduled 所面臨的問題:

    • CRON表示式過於簡單,不支援複雜的表示式,比如每月最後一天,雖然提供zone支援但在特殊的國度,如在美國,無法計算夏令時及冬令時的偏差
    • @Schedules@SchedulerLock配合時 多執行時間 會存在被鎖定的問題
    • scheduled 如果不指定執行緒池時 預設是單執行緒執行,不管應用下有多少定時任務都會是單執行緒,這是瓶頸...
    • scheduled 不支援傳參,函式使用時必須是void的函式返回且不可有形參
    • 部分api可能存在spring版本迭代時不相容問題,這是二開可能的問題
  • shedlock 的不足之處:

    • 無法做叢集及分散式節點管理,除非key定義的十分小心
    • 不太好透過鎖的控制做任務及節點的啟停控制(可以透過特殊方法 比較另類)
    • 任務執行時的關鍵資訊預設不記錄(IP、時間、CRON、應用資訊等等)
    • 加鎖過程可能存在不必要的更新操作(這是程式碼問題)

基於現有情況我改造了 scheduled,用較少的更改 做出了處於 scheduledquartz 中間的定時任務元件,這就是 MEE_TIMED 🌹.

MEE_TIMED 所做的改進

  • 新增app表(SYS_SHEDLOCK_APP),提供叢集及多節點控制支援
  • 擴充套件job(SYS_SHEDLOCK_JOB)表data欄位,提供傳參及引數修改支援
  • @Schedule@SchedulerLock 二合一並簡化註解配置
  • spring scheduledCronExpression 替換為 quartzCronExpression,支援更靈活更復雜的CRON表示式
  • 修改掉 scheduled 內部預設單執行緒的問題,提供執行緒池支援
  • 固定於spring強繫結的api,儘量與springboot相容性做到最佳
  • 任務資訊落表 等等

基本使用

詳細配置程式碼及後臺整合在mee-admin有例項 👊(,)👊

  • 1.下載 表結構 及 mee_timed-X.X.X.jar 依賴 依賴 並存放於專案或nexus私服中

  • 2.POM中定義dependency依賴:

            <dependency>
                <groupId>com.mee.timed</groupId>
                <artifactId>mee_timed</artifactId>
                <version>1.0.1</version>
                <scope>system</scope>
                <systemPath>${pom.basedir}/src/main/resources/lib/mee_timed-1.0.1.jar</systemPath>
            </dependency>
    
  • 3.匯入表結構(SQL)

    根據所使用的db,按需匯入對應廠商所支援的表結構,目前僅提供 mysqloraclepostgresql支援:

        table_mysql.sql
        table_oracle.sql
        table_postgresql.sql
    
  • 4.定義配置及bean

    目前配置僅有三項:

    spring.mee.timed.shed=${spring.application.name}
    spring.mee.timed.table-name=SYS_SHEDLOCK_JOB
    spring.mee.timed.table-app-name=SYS_SHEDLOCK_APP
    

    其中配置項spring.mee.timed.table-app-name是管理叢集及節點用的,如不需要可不配置
    應用啟動時會自動寫入必要的初始化引數,也可提前將初始資料提前匯入

    配置bean: 這一步是非必須的,只是內部執行緒池的配置較為保守,如需自定義可以以下配置指定執行緒數及執行緒名字首:

        /**
         * 設定執行執行緒數
         * @return
         */
        @Bean
        public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
            ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
            scheduler.setPoolSize(PROCESSOR*2);
            scheduler.setThreadNamePrefix("SHEDLOCK-");
            scheduler.initialize();
            return scheduler;
        }
    
  • 5.定義定時任務

    樣例一:

    
    import com.mee.timed.Job;
    import com.mee.timed.JobExecutionContext;
    import com.mee.timed.annotation.MeeTimed;
    import com.mee.timed.annotation.MeeTimeds;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;
    
    import java.util.concurrent.TimeUnit;
    
    @Component
    public class Job01TestService implements Job {
        private static final Logger LOGGER = LoggerFactory.getLogger(Job01TestService.class);
    
        @MeeTimed(fixedRate = 10000,lockAtLeastFor = "PT5S",lockAtMostFor ="PT5S" )
        public void exec01() throws InterruptedException {
            LOGGER.info("=====> [exec01] Already Executed! <=====");
            TimeUnit.SECONDS.sleep(6);
        }
    
        @MeeTimeds({
             @MeeTimed(cron = "10,20,30,40,50 * * * * ?",lockAtMostFor ="PT5S",lockName = "execute1"),
             @MeeTimed(cron = "0 0/2 * * * ?",lockAtMostFor ="PT1M",lockName = "execute2"),
             @MeeTimed(cron = "0 0/4 * ? * MON-FRI",lockAtMostFor ="PT1M",lockName = "execute3"),
             // 紐約時間每年的7月9號22點2分執行
             @MeeTimed(cron = "0 2 22 9 7 ?",lockAtMostFor ="PT1M",lockName = "execute4",zone = "America/New_York"),
             // 每月最後一天的十點半(eg:2024-07-31 10:30:00)
             @MeeTimed(cron = "0 30 10 L * ?",lockAtMostFor ="PT1M",lockName = "execute5")
        })
        @Override
        public void execute(JobExecutionContext context)   {
            LOGGER.info("=====> proxy job exec! data:"+context.getJobInfo().getName()+"  <=====");
            try {
                TimeUnit.SECONDS.sleep(8);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    
    }
    

    樣例二:

    package com.mee.timed.test.job;
    
    import com.mee.timed.annotation.MeeTimed;
    import com.mee.timed.annotation.MeeTimeds;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;
    
    @Component
    public class ScheduledTasks {
        private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTasks.class);
    
        @MeeTimeds({
                @MeeTimed(fixedRate = 10000,lockAtLeastFor = "PT5S",lockAtMostFor ="PT5S",lockName = "T1"),
                @MeeTimed(fixedDelay = 8000,lockAtLeastFor = "PT5S",lockAtMostFor ="PT5S",lockName = "T2"),
        })
        public void exec01() {
            LOGGER.info("=====> [exec01] Already Executed! <=====");
        }
    
        @MeeTimed(cron = "0/20 * * * * ?",lockAtLeastFor = "PT5S",lockAtMostFor ="PT10S" )
        public void exec02(JobExecutionContext context) {
            LOGGER.info("=====> proxy job exec! data:"+context.getJobDataJson()+"  <=====");
        }
        
    }
    

    以上兩種方式均可,如果需要傳遞引數 其函式的形引數 必須是 JobExecutionContext 或其實現類

    如果是同一函式多時間配置(使用 @MeeTimeds 配置),其每一項 lockName 不可為空!

整合後臺管理

  • 具體效果及程式碼整合 具體見: mee-admin

  • 後臺配置及管理

實際執行效果

2024-07-18 09:59:20.006 -> [MEE_TIMED-7] -> INFO  com.mee.cron.JobTimedService:25 - =====> proxy job exec! data:{"key":"執行資料"}  <=====
2024-07-18 09:59:40.020 -> [MEE_TIMED-7] -> INFO  com.mee.cron.JobTimedService:25 - =====> proxy job exec! data:{"key":"執行資料"}  <=====
2024-07-18 09:59:59.993 -> [MEE_TIMED-1] -> INFO  com.mee.cron.DefaultTimerService:27 - ===>testTask2執行時間: 2024-07-18 09:59:59
2024-07-18 10:00:00.003 -> [MEE_TIMED-5] -> INFO  com.mee.cron.DefaultTimerService:21 - ===>testTask1執行時間: 2024-07-18 10:00:00
2024-07-18 10:00:00.009 -> [MEE_TIMED-4] -> INFO  com.mee.cron.JobTimedService:25 - =====> proxy job exec! data:{"key":"執行資料"}  <=====
2024-07-18 10:00:20.014 -> [MEE_TIMED-4] -> INFO  com.mee.cron.JobTimedService:25 - =====> proxy job exec! data:{"key":"執行資料"}  <=====
2024-07-18 10:00:40.015 -> [MEE_TIMED-4] -> INFO  com.mee.cron.JobTimedService:25 - =====> proxy job exec! data:{"key":"執行資料"}  <=====
2024-07-18 10:01:00.019 -> [MEE_TIMED-4] -> INFO  com.mee.cron.JobTimedService:25 - =====> proxy job exec! data:{"key":"執行資料"}  <=====

後續計劃

  1. 首先是傳參考慮做反序列化處理,在必要場景下這是需要的

  2. fix bug,當然這需要碼友多多支援啦

  3. 動態修改執行時間,尤其是cron,這功能是與quartz的差距的縮小是決定性的

  4. 執行日誌支援,並提供擴充套件支援

  5. 其他待定

最後

再次感謝 spring scheduledshedlock 的開源,MEE_TIMEDgithub 有開源,詳見: https://github.com/funnyzpc/mee_timed_parent 🎈

相關文章