XXL-JOB日常實用進階,包括分片任務,阻塞處理策略,路由策略,運行模式

主要包括XXL-JOB日志清理,包括分片廣播任務,阻塞處理策略,路由策略,運行模式,創建子任務
如果查看XXL-JOB基本使用和整合SpringBoot,請參考我另一篇文章:XXL-JOB基本配置使用
導語:XLL-JOB是分布式任務調度平臺,常見功能特性:
1、簡單:支持通過Web頁面對任務進行CRUD操作,操作簡單,容易上手
2、動態:支持動態修改任務狀態,啟動/停止任務,以及終止運行中的任務,即時生效
3、調度中心HA(中心式):調度中心式設計,并支持集群部署,保證調度平臺高可用
4、執行器HA(分布式):任務分布執行,任務執行器支持集群部署,可保證任務執行高可用
5、彈性擴容縮容:一旦有新執行器機器上下線,下次調度執行時,將會重新分配任務執行

一、XXL-JOB任務類型:

1、BEAN模式: ①類形式 ②方法形式
2、GLUE模式:Java / Shell / Python / Nodejs / Php
1、Bean模式任務,支持基于方法的開發模式,每個任務對應一個方法
優點:

每個任務只需要開發一個方法,并添加@XxlJob注解即可,方便簡單快捷
支持自動掃描并添加至執行器容器中

缺點:

要求spring開發環境,基本現在項目spring必備,所以無傷大雅
新定時任務的CRUD需要項目的重新構建和項目啟動,如果遇到未執行完畢的情況,可能會多次執行,但是保證多次執行和一次執行的結果不影響,對系統也不會有影響

2、GLUE模式

定時任務以源碼方式維護在調度中心,不需要在本地編寫任何代碼,我們在使用過程中,經常是在本地編碼完畢后,直接復制到線上維護中心中

優點:

支持通過Web IDE在線更新,實時編譯和生效,因此不需要指定JobHandler和重啟項目

缺點:

如果你依賴了某個框架和服務,需要先依賴到自己項目中,然后在Web IDE中才能依賴,否則會執行報錯,正??梢岳斫鉃?,把代碼從項目中搬到線上,可以實時編輯,但是和自己在本地寫代碼的要求一樣,依賴和服務必須全部具備,多用于定時任務經常調整的場景中使用

調度中心使用示例:


二、XXL-JOB的日志清理:

日志分類:
1、調度日志:任務調度的時候,會告知一些比如執行器信息,調度結果等2、
2、執行日志:JOB執行過程中日志,XxlJobLogger.log("")中進行打印

日志執行過程中,可以編寫一個定時任務定時清理也可以/也可以調用自動清理的API,就是點擊確認清理,出發的Http請求的URL地址(服務訪問地址+/joblog/clearLog),根據源碼中參數。進行傳參即可

三、XLL-JOB子任務介紹:

XXL-JOB中有自帶的子任務編排功能,支持子任務依賴,當父任務執行結束且執行成功后將會主動出發一次子任務的執行,多個子任務使用逗號分隔
優點:

適合連續,連貫的業務場景,框架自帶任務編排,使用簡單,只需要通過調度中心頁面配置即可實現

缺點:

連續任務的數據不能直接進行傳遞,不像JAVA中CompletableFuture可以將上一個任務的執行結果傳遞到后續使用,可以就需要將每個任務的處理數據,存儲到第三方存儲中,比如Mysql,Redis等

四、XLL-JOB分片廣播任務:

執行器集群部署時,任務路由策略選擇 【分片廣播】路由策略情況下,一次任務調度將會廣播觸發對應集群中所有執行器都觸發執行一次任務,同時系統自動傳遞分片參數,可根據分片參數開發分片任務。
【分片廣播】:以執行器維度進行分片,支持動態擴容執行器從而動態增加分片數量,
協同進行業務處理,在進行大數據量業務操作時可顯著提升任務處理能力和速度。
分片廣播和普通任務開發流程一致,不同之處在于可以獲取分片參數,獲取分片參數進行分片任務處理

獲取分片參數

    final ShardingUtil.ShardingVO shardingVo = ShardingUtil.getShardingVo();
    
    index: 當前分片的序號(從0開始)執行器集群列表中當前執行器的序號
    total: 總分片數,執行器集群的總機器數量
代碼示例:
 @XxlJob("executeJobHandler")
    public ReturnT<String> executeJobHandler(String param) throws Exception {
        log.info("XXL-JOB, Hello World. time:{} ", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        // 分片參數
        ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo();
        log.info("分片參數:當前分片序號 = {}, 總分片數 = {}", shardingVO.getIndex(), shardingVO.getTotal());

        // 總分片數目
        final int total = shardingVO.getTotal();

        // 當前執行器序號
        final int index = shardingVO.getIndex();

        // 1.獲取執行數據
        final List<Integer> list = queryDataList();
        for (int i = 0; i < list.size(); i++) {
            Integer id = list.get(i);
            // 分片總數量取模等于當前分片
            if (id % total == index) {
                XxlJobLogger.log("=== 任務執行 ===");
            }
        }

        return ReturnT.SUCCESS;
    }
調度中心使用示例:

五、XLL-JOB阻塞處理策略類型

單機串行(默認)

調度進入單機執行器后,調度請求進入FIFO隊列中并以串行方式運行

丟棄后續調度(推薦)

調度請求進入單機執行器,發現執行器存在運行的調度任務,本次請求將會被丟棄并標記為失敗

覆蓋之前調度(不推薦)

調度請求進入單機執行器后,發現執行器存在運行的調度任務,
將會終止運行中的調度任務并清空隊列,然后運行本地調度

單機串行情況下:如果一個任務沒有執行完畢,第二次任務執行又開始了,那么第二次會一直等待直到第一次執行完畢才會執行第二次任務調度,這樣如果任務頻率比較高,同時執行時間長,不建議使用這種方式,這樣會導致阻塞i的任務越來越多

XXL-JOB定時任務超時注意事項:

任務超時/任務終止注意事項
JOB中不能消化InterruptedException必須往外拋出異常楊

如果異常被捕獲,但是在任務日志執行頁面手動點擊【終止任務】
會拋出InterruptedException異常, 但是任務不會停止,需要手動處理

    @XxlJob("executeJobHandler")
    public ReturnT<String> executeJobHandler(String param) throws Exception {
        log.info("XXL-JOB, Hello World. time:{} ", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        /*
         * 如果異常被捕獲,但是在任務日志執行頁面手動點擊【終止任務】,會拋出InterruptedException
         * 但是任務不會停止
         */

        try {
            for (int i = 0; i < 10; i++) {
                XxlJobLogger.log("執行中");
                TimeUnit.SECONDS.sleep(5);
            }
        } catch (Exception e) {

            /*
             * 解決方案:
             * 這里手動處理,用來避免這種情況
             */
            if (e instanceof InterruptedException) {
                throw e;
            }
            e.printStackTrace();
        }

        return ReturnT.SUCCESS;
    }

六、xxl-job執行器路由選擇策略

  • 路由策略:當執行器集群部署時,提供豐富的路由策略,包括:
FIRST(第一個):固定選擇第一個機器;

LAST(最后一個):固定選擇最后一個機器;

ROUND(輪詢):;

RANDOM(隨機):隨機選擇在線的機器;

CONSISTENT_HASH(一致性HASH):每個任務按照Hash算法固定選擇某一臺機器,且所有任務均勻散列在不同機器上。

LEAST_FREQUENTLY_USED(最不經常使用):使用頻率最低的機器優先被選舉;

LEAST_RECENTLY_USED(最近最久未使用):最久未使用的機器優先被選舉;

FAILOVER(故障轉移):按照順序依次進行心跳檢測,第一個心跳檢測成功的機器選定為目標執行器并發起調度;

BUSYOVER(忙碌轉移):按照順序依次進行空閑檢測,第一個空閑檢測成功的機器選定為目標執行器并發起調度;

SHARDING_BROADCAST(分片廣播):廣播觸發對應集群中所有機器執行一次任務,同時系統自動傳遞分片參數;可根據分片參數開發分片任務;

1、FIRST:獲取地址列表中的第一個
public class ExecutorRouteFirst extends ExecutorRouter {

    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList){
        return new ReturnT<String>(addressList.get(0));
    }

}

2、LAST:獲取地址列表中的最后一個
public class ExecutorRouteLast extends ExecutorRouter {

    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
        return new ReturnT<String>(addressList.get(addressList.size()-1));
    }

}
3、輪詢: 緩存時間是1天, 疊加次數最多為一百萬,超過后進行重置,但是重置時采用隨機方式,隨機到一個小于100的數字,基于計數器,對地址列表取模
public class ExecutorRouteRound extends ExecutorRouter {
    private static ConcurrentMap routeCountEachJob = new ConcurrentHashMap<>();
    private static long CACHE_VALID_TIME = 0;
    private static int count(int jobId) {
        // cache clear
        if (System.currentTimeMillis() > CACHE_VALID_TIME) {
            routeCountEachJob.clear();
            CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;
        }
        AtomicInteger count = routeCountEachJob.get(jobId);
        if (count == null || count.get() > 1000000) {
            // 初始化時主動Random一次,緩解首次壓力
            count = new AtomicInteger(new Random().nextInt(100));
        } else {
            // count++
            count.addAndGet(1);
        }
        routeCountEachJob.put(jobId, count);
        return count.get();
    }

    @Override
    public ReturnT route(TriggerParam triggerParam, List addressList) {
        String address = addressList.get(count(triggerParam.getJobId())%addressList.size());
        return new ReturnT(address);
    }

}
4、隨機,隨機選擇一臺及其執行
public class ExecutorRouteRandom extends ExecutorRouter {

    private static Random localRandom = new Random();
    @Override

    public ReturnT route(TriggerParam triggerParam, List addressList) {
        String address = addressList.get(localRandom.nextInt(addressList.size()));
        return new ReturnT(address);
    }
}
5、一致性哈希

分組下機器地址相同,不同JOB均勻散列在不同機器上,保證分組下機器分配JOB平均;且每個JOB固定調度其中一臺機器;
a、virtual node:解決不均衡問題
b、hash method replace hashCode:String的hashCode可能重復,需要進一步擴大hashCode的取值范圍

public class ExecutorRouteConsistentHash extends ExecutorRouter {

    private static int VIRTUAL_NODE_NUM = 100;

    /**
     * get hash code on 2^32 ring (md5散列的方式計算hash值)
     * @param key
     * @return
     */
    private static long hash(String key) {

        // md5 byte
        MessageDigest md5;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5 not supported", e);
        }
        md5.reset();
        byte[] keyBytes = null;
        try {
            keyBytes = key.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Unknown string :" + key, e);
        }

        md5.update(keyBytes);
        byte[] digest = md5.digest();

        // hash code, Truncate to 32-bits
        long hashCode = ((long) (digest[3] & 0xFF) << 24)
                | ((long) (digest[2] & 0xFF) << 16)
                | ((long) (digest[1] & 0xFF) << 8)
                | (digest[0] & 0xFF);

        long truncateHashCode = hashCode & 0xffffffffL;
        return truncateHashCode;
    }

    public String hashJob(int jobId, List<String> addressList) {

        // ------A1------A2-------A3------
        // -----------J1------------------
        TreeMap<Long, String> addressRing = new TreeMap<Long, String>();
        for (String address: addressList) {
            for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
                long addressHash = hash("SHARD-" + address + "-NODE-" + i);
                addressRing.put(addressHash, address);
            }
        }

        long jobHash = hash(String.valueOf(jobId));
        SortedMap<Long, String> lastRing = addressRing.tailMap(jobHash);
        if (!lastRing.isEmpty()) {
            return lastRing.get(lastRing.firstKey());
        }
        return addressRing.firstEntry().getValue();
    }

    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
        String address = hashJob(triggerParam.getJobId(), addressList);
        return new ReturnT<String>(address);
    }

}

6. LEAST_FREQUENTLY_USED(最不經常使用): 緩存時間還是一天,對地址列表進行篩選,如果新加入的地址列表或者使用次數超過一百萬次的話,就會隨機重置為小于地址列表地址個數的值。 最后返回的就是value值最小的地址
public class ExecutorRouteLFU extends ExecutorRouter {
    private static ConcurrentMap jobLfuMap = new ConcurrentHashMap();
    private static long CACHE_VALID_TIME = 0;
    public String route(int jobId, List addressList) {
        // cache clear
        if (System.currentTimeMillis() > CACHE_VALID_TIME) {
            jobLfuMap.clear();
            CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;
        }

        // lfu item init
        HashMap lfuItemMap = jobLfuMap.get(jobId);     // Key排序可以用TreeMap+構造入參Compare;Value排序暫時只能通過ArrayList;
        if (lfuItemMap == null) {
            lfuItemMap = new HashMap();
            jobLfuMap.putIfAbsent(jobId, lfuItemMap);   // 避免重復覆蓋
        }
        // put new
        for (String address: addressList) {
            if (!lfuItemMap.containsKey(address) || lfuItemMap.get(address) >1000000 ) {
                lfuItemMap.put(address, new Random().nextInt(addressList.size()));  // 初始化時主動Random一次,緩解首次壓力
            }
        }
        // remove old
        List delKeys = new ArrayList<>();
        for (String existKey: lfuItemMap.keySet()) {
            if (!addressList.contains(existKey)) {
                delKeys.add(existKey);
            }
        }
        if (delKeys.size() > 0) {
            for (String delKey: delKeys) {
               lfuItemMap.remove(delKey);
            }
        }
        // load least userd count address
        List lfuItemList = new ArrayList(lfuItemMap.entrySet());
        Collections.sort(lfuItemList, new Comparator() {
            @Override
            public int compare(Map.Entry o1, Map.Entry o2) {
                return o1.getValue().compareTo(o2.getValue());
            }
        });
        Map.Entry addressItem = lfuItemList.get(0);
        String minAddress = addressItem.getKey();
        addressItem.setValue(addressItem.getValue() + 1);
        return addressItem.getKey();
    }
    @Override
    public ReturnT route(TriggerParam triggerParam, List addressList) {
        String address = route(triggerParam.getJobId(), addressList);
        return new ReturnT(address);
    }
}

7、 LEAST_RECENTLY_USED(最近最久未使用):緩存時間還是一天,對地址列表進行篩選, 采用LinkedHashMap實現LRU算法
其中LinkedHashMap的構造器中有一個參數:
//accessOrder 為true, 每次調用get或者put都會將該元素放置到鏈表最后,因而獲取第一個元素就是當前沒有使用過的元素

public class ExecutorRouteLRU extends ExecutorRouter {
    private static ConcurrentMap jobLRUMap = new ConcurrentHashMap();
    private static long CACHE_VALID_TIME = 0;
    public String route(int jobId, List addressList) {
        // cache clear
        if (System.currentTimeMillis() > CACHE_VALID_TIME) {
            jobLRUMap.clear();
            CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;
        }
        // init lru
        LinkedHashMap lruItem = jobLRUMap.get(jobId);
        if (lruItem == null) {
            /**
             * LinkedHashMap
             *      a、accessOrder:true=訪問順序排序(get/put時排序);false=插入順序排期;
             *      b、removeEldestEntry:新增元素時將會調用,返回true時會刪除最老元素;可封裝LinkedHashMap并重寫該方法,比如定義最大容量,超出是返回true即可實現固定長度的LRU算法;
             */
            //accessOrder 為true, 每次調用get或者put都會將該元素放置到鏈表最后,因而獲取第一個元素就是當前沒有使用過的元素
            lruItem = new LinkedHashMap(16, 0.75f, true);
            jobLRUMap.putIfAbsent(jobId, lruItem);
        }
        // put new
        for (String address: addressList) {
            if (!lruItem.containsKey(address)) {
                lruItem.put(address, address);
            }
        }
        // remove old
        List delKeys = new ArrayList<>();
        for (String existKey: lruItem.keySet()) {
            if (!addressList.contains(existKey)) {
                delKeys.add(existKey);
            }
        }
        if (delKeys.size() > 0) {
            for (String delKey: delKeys) {
                lruItem.remove(delKey);
            }
        }
        // load
        String eldestKey = lruItem.entrySet().iterator().next().getKey();
        String eldestValue = lruItem.get(eldestKey);
        return eldestValue;
    }
    @Override
    public ReturnT route(TriggerParam triggerParam, List addressList) {
        String address = route(triggerParam.getJobId(), addressList);
        return new ReturnT(address);
    }
}

8、FAILOVER 會返回第一個心跳檢測ok的執行器,主要是使用xxl-job的執行器 RESTful API中的 beat

按照順序依次進行心跳檢測,第一個心跳檢測成功的機器選定為目標執行器并發起調度;

public class ExecutorRouteFailover extends ExecutorRouter {

    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {

        StringBuffer beatResultSB = new StringBuffer();
        for (String address : addressList) {
            // beat
            ReturnT<String> beatResult = null;
            try {
                ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
                beatResult = executorBiz.beat();
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                beatResult = new ReturnT<String>(ReturnT.FAIL_CODE, ""+e );
            }
            beatResultSB.append( (beatResultSB.length()>0)?"<br><br>":"")
                    .append(I18nUtil.getString("jobconf_beat") + ":")
                    .append("<br>address:").append(address)
                    .append("<br>code:").append(beatResult.getCode())
                    .append("<br>msg:").append(beatResult.getMsg());

            // beat success
            if (beatResult.getCode() == ReturnT.SUCCESS_CODE) {

                beatResult.setMsg(beatResultSB.toString());
                beatResult.setContent(address);
                return beatResult;
            }
        }
        return new ReturnT<String>(ReturnT.FAIL_CODE, beatResultSB.toString());

    }
}
9、BUSYOVER(忙碌轉移):按照順序依次進行空閑檢測,第一個空閑檢測成功的機器選定為目標執行器并發起調度;

會返回空閑的第一個執行器的地址,主要是使用xxl-job的執行器 RESTful API中的 idleBeat

public class ExecutorRouteBusyover extends ExecutorRouter {

    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
        StringBuffer idleBeatResultSB = new StringBuffer();
        for (String address : addressList) {
            // beat
            ReturnT<String> idleBeatResult = null;
            try {
                ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
                idleBeatResult = executorBiz.idleBeat(new IdleBeatParam(triggerParam.getJobId()));
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                idleBeatResult = new ReturnT<String>(ReturnT.FAIL_CODE, ""+e );
            }
            idleBeatResultSB.append( (idleBeatResultSB.length()>0)?"<br><br>":"")
                    .append(I18nUtil.getString("jobconf_idleBeat") + ":")
                    .append("<br>address:").append(address)
                    .append("<br>code:").append(idleBeatResult.getCode())
                    .append("<br>msg:").append(idleBeatResult.getMsg());

            // beat success
            if (idleBeatResult.getCode() == ReturnT.SUCCESS_CODE) {
                idleBeatResult.setMsg(idleBeatResultSB.toString());
                idleBeatResult.setContent(address);
                return idleBeatResult;
            }
        }

        return new ReturnT<String>(ReturnT.FAIL_CODE, idleBeatResultSB.toString());
    }

}
10、SHARDING_BROADCAST

SHARDING_BROADCAST(分片廣播):廣播觸發對應集群中所有機器執行一次任務,同時系統自動傳遞分片參數;可根據分片參數開發分片任務;

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容