Java併發程式設計 - 第十一章 Java併發程式設計實踐

SongYu-SY發表於2020-10-01

前言:

當你在進行併發程式設計時,看著程式的執行速度在自己的優化下執行得越來越快,你會覺得越來越有成就感,這就是併發程式設計的魅力。但與此同時,併發程式設計產生的問題和風險可能也會隨之而來。本章先介紹幾個併發程式設計的實戰案例,然後再介紹如何排查併發程式設計造成的問題。

一、生產者和消費者模式

在併發程式設計中使用生產者和消費者模式能夠解決絕大多數併發問題。該模式通過平衡生產執行緒和消費執行緒的工作能力來提高程式整體處理資料的速度。

線上程世界裡,生產者就是生產資料的執行緒,消費者就是消費資料的執行緒。在多執行緒開發中,如果生產者處理速度很快,而消費者處理速度很慢,那麼生產者就必須等待消費者處理完,才能繼續生產資料。同樣的道理,如果消費者的處理能力大於生產者,那麼消費者就必須等待生產者。為了解決這種生產消費能力不均衡的問題,便有了生產者和消費者模式。、

什麼是生產者和消費者模式?

生產者和消費者模式是通過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通訊,而是通過阻塞佇列來進行通訊,所以生產者生產完資料之後不用等待消費者處理,直接扔給阻塞佇列,消費者不找生產者要資料,而是直接從阻塞佇列裡取,阻塞佇列就相當於一個緩衝區,平衡了生產者和消費者的處理能力。

這個阻塞佇列就是用來給生產者和消費者解耦的。縱觀大多數設計模式,都會找一個第三者出來進行解耦,如工廠模式的第三者是工廠類,模板模式的第三者是模板類。在學習一些設計模式的過程中,先找到這個模式的第三者,能幫助我們快速熟悉一個設計模式。

1.1 生產者消費者模式實戰

我和同事一起利用業餘時間開發的 Yuna 工具中使用了生產者和消費者模式。我先介紹下 Yuna 工具,在阿里巴巴很多同事都喜歡通過郵件分享技術文章,因為通過郵件分享很方便,大家在網上看到好的技術文章,執行 複製 → 貼上 → 傳送 就完成了一次分享,但是我發現技術
文章不能沉澱下來,新來的同事看不到以前分享的技術文章,大家也很難找到以前分享過的技術文章。為了解決這個問題,我們開發了一個 Yuna 工具。

我們申請了一個專門用來收集分享郵件的郵箱,比如 share@alibaba.com,大家將分享的文章傳送到這個郵箱,讓大家每次都抄送到這個郵箱肯定很麻煩,所以我們的做法是將這個郵箱地址放在部門郵件列表裡,所有分享的同事只需要和以前一樣向整個部門分享文章就行。

Yuna 工具通過讀取郵件伺服器裡該郵箱的郵件,把所有分享的郵件下載下來,包括郵件的附件、圖片和郵件回覆。因為我們可能會從這個郵箱裡下載到一些非分享的文章,所以我們要求分享的郵件標題必須帶有一個關鍵字,比如 “內貿技術分享”。下載完郵件之後,通過 confluence 的 Web Service 介面,把文章插入到 confluence 裡,這樣新同事就可以在 confluence 裡看以前分享過的文章了,並且 Yuna 工具還可以自動把文章進行分類和歸檔。

為了快速上線該功能,當時我們花了 3 天業餘時間快速開發了 Yuna 1.0 版本。在 1.0 版本中並沒有使用生產者消費模式,而是使用單執行緒來處理,因為當時只需要處理我們一個部門的郵件,所以單執行緒明顯夠用,整個過程是序列執行的。在一個執行緒裡,程式先抽取全部的郵件,轉化為文章物件,然後新增全部的文章,最後刪除抽取過的郵件。程式碼如下。

public void extract() {
	logger.debug("開始" + getExtractorName() + "。。");
	// 抽取郵件
	List<Article> articles = extractEmail();
	// 新增文章
	for (Article article : articles) {
		addArticleOrComment(article);
	}
	// 清空郵件
	cleanEmail();
	logger.debug("完成" + getExtractorName() + "。。");
}

Yuna 工具在推廣後,越來越多的部門使用這個工具,處理的時間越來越慢,Yuna 是每隔 5 分鐘進行一次抽取的,而當郵件多的時候一次處理可能就花了幾分鐘,於是我在 Yuna 2.0 版本里使用了生產者消費者模式來處理郵件,首先生產者執行緒按一定的規則去郵件系統裡抽取郵
件,然後存放在阻塞佇列裡,消費者從阻塞佇列裡取出文章後插入到 conflunce 裡。程式碼如下。

public class QuickEmailToWikiExtractor extends AbstractExtractor {
    private ThreadPoolExecutor threadsPool;
    private ArticleBlockingQueue<ExchangeEmailShallowDTO> emailQueue;

    public QuickEmailToWikiExtractor() {
        emailQueue = new ArticleBlockingQueue<ExchangeEmailShallowDTO>();
        int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
        threadsPool = new ThreadPoolExecutor(corePoolSize, corePoolSize, 10l, TimeUnit.
                SECONDS,
                new LinkedBlockingQueue<Runnable>(2000));
    }

    public void extract() {
        logger.debug("開始" + getExtractorName() + "。。");
        long start = System.currentTimeMillis();
        // 抽取所有郵件放到佇列裡
        new ExtractEmailTask().start();
        // 把佇列裡的文章插入到Wiki
        insertToWiki();
        long end = System.currentTimeMillis();
        double cost = (end - start) / 1000;
        logger.debug("完成" + getExtractorName() + ",花費時間:" + cost + "秒");
    }

    /**
     * 把佇列裡的文章插入到Wiki
     */
    private void insertToWiki() {
        // 登入Wiki,每間隔一段時間需要登入一次
        confluenceService.login(RuleFactory.USER_NAME, RuleFactory.PASSWORD);
        while (true) {
            // 2秒內取不到就退出
            ExchangeEmailShallowDTO email = emailQueue.poll(2, TimeUnit.SECONDS);
            if (email == null) {
                break;
            }
            threadsPool.submit(new insertToWikiTask(email));
        }
    }

    protected List<Article> extractEmail() {
        List<ExchangeEmailShallowDTO> allEmails = getEmailService().queryAllEmails();
        if (allEmails == null) {
            return null;
        }
        for (ExchangeEmailShallowDTO exchangeEmailShallowDTO : allEmails) {
            emailQueue.offer(exchangeEmailShallowDTO);
        }
        return null;
    }

    /**
     * 抽取郵件任務
     */
    public class ExtractEmailTask extends Thread {
        public void run() {
            extractEmail();
        }
    }
}

1.2 多生產者和多消費者場景

在多核時代,多執行緒併發處理速度比單執行緒處理速度更快,所以可以使用多個執行緒來生產資料,同樣可以使用多個消費執行緒來消費資料。而更復雜的情況是,消費者消費的資料,有可能需要繼續處理,於是消費者處理完資料之後,它又要作為生產者把資料放在新的佇列裡,交給其他消費者繼續處理,如圖所示。

多生產者消費者模式
我們在一個長連線伺服器中使用了這種模式,生產者 1 負責將所有客戶端傳送的訊息存放在阻塞佇列 1 裡,消費者 1 從佇列裡讀訊息,然後通過訊息 ID 進行雜湊得到 N 個佇列中的一個,然後根據編號將訊息存放在到不同的佇列裡,每個阻塞佇列會分配一個執行緒來消費阻塞佇列
裡的資料。如果消費者 2 無法消費訊息,就將訊息再拋回到阻塞佇列 1 中,交給其他消費者處理。

以下是訊息總佇列的程式碼。

/**
 * 總訊息佇列管理
 */
public class MsgQueueManager implements IMsgQueue {
    private static final Logger LOGGER = LoggerFactory.getLogger(MsgQueueManager.class);
    /**
     * 訊息總佇列
     */
    public final BlockingQueue<Message> messageQueue;

    private MsgQueueManager() {
        messageQueue = new LinkedTransferQueue<Message>();
    }

    public void put(Message msg) {
        try {
            messageQueue.put(msg);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public Message take() {
        try {
            return messageQueue.take();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return null;
    }
}

啟動一個訊息分發執行緒。在這個執行緒裡子佇列自動去總佇列裡獲取訊息。

/**
 * 分發訊息,負責把訊息從大佇列塞到小佇列裡
 */
static class DispatchMessageTask implements Runnable {
    @Override
    public void run() {
        BlockingQueue<Message> subQueue;
        for (;;) {
            // 如果沒有資料,則阻塞在這裡
            Message msg = MsgQueueFactory.getMessageQueue().take();
            // 如果為空,則表示沒有Session機器連線上來,
            // 需要等待,直到有Session機器連線上來
            while ((subQueue = getInstance().getSubQueue()) == null) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            // 把訊息放到小佇列裡
            try {
                subQueue.put(msg);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

使用雜湊(hash)演算法獲取一個子佇列,程式碼如下。

/**
 * 均衡獲取一個子佇列
 */
public BlockingQueue<Message> getSubQueue() {
    int errorCount = 0;
    for (;;) {
        if (subMsgQueues.isEmpty()) {
            return null;
        }
        int index = (int) (System.nanoTime() % subMsgQueues.size());
        try {
            return subMsgQueues.get(index);
        } catch (Exception e) {
            // 出現錯誤表示,在獲取佇列大小之後,佇列進行了一次刪除操作
            LOGGER.error("獲取子佇列出現錯誤", e);
            if ((++errorCount) < 3) {
                continue;
            }
        }
    }
}

使用的時候,只需要往總佇列裡發訊息。

// 往訊息佇列裡新增一條訊息
IMsgQueue messageQueue = MsgQueueFactory.getMessageQueue();
Packet msg = Packet.createPacket(Packet64FrameType.TYPE_DATA, "{}".getBytes(), (short) 1);
messageQueue.put(msg);

1.3 執行緒池與生產消費者模式

Java 中的執行緒池類其實就是一種生產者和消費者模式的實現方式,但是我覺得其實現方式更加高明。生產者把任務丟給執行緒池,執行緒池建立執行緒並處理任務,如果將要執行的任務數大於執行緒池的基本執行緒數就把任務扔到阻塞佇列裡,這種做法比只使用一個阻塞佇列來實現生產者和消費者模式顯然要高明很多,因為消費者能夠處理直接就處理掉了,這樣速度更快,而生產者先存,消費者再取這種方式顯然慢一些。

我們的系統也可以使用執行緒池來實現多生產者和消費者模式。例如,建立 N 個不同規模的 Java 執行緒池來處理不同性質的任務,比如執行緒池 1 將資料讀到記憶體之後,交給執行緒池2裡的執行緒繼續處理壓縮資料。執行緒池 1 主要處理 IO 密集型任務,執行緒池 2 主要處理 CPU 密集型任務。

本節講解了生產者和消費者模式,並給出了例項。讀者可以在平時的工作中思考一下哪些場景可以使用生產者消費者模式,我相信這種場景應該非常多,特別是需要處理任務時間比較長的場景,比如上傳附件並處理,使用者把檔案上傳到系統後,系統把檔案丟到佇列裡,然後立刻返回告訴使用者上傳成功,最後消費者再去佇列裡取出檔案處理。再如,呼叫一個遠端介面查詢資料,如果遠端服務介面查詢時需要幾十秒的時間,那麼它可以提供一個申請查詢的介面,這個介面把要申請查詢任務放資料庫中,然後該介面立刻返回。然後伺服器端用執行緒輪詢並獲取申請任務進行處理,處理完之後發訊息給呼叫方,讓呼叫方再來呼叫另外一個介面取資料。

二、線上問題定位

有時候,有很多問題只有線上上或者預發環境才能發現,而線上又不能除錯程式碼,所以線上問題定位就只能看日誌、系統狀態和 dump 執行緒,本節只是簡單地介紹一些常用的工具,以幫助大家定位線上問題。

  1. 在 Linux 命令列下使用 TOP 命令檢視每個程式的情況,顯示如下。

    top - 22:27:25 up 463 days, 12:46, 1 user, load average: 11.80, 12.19, 11.79
    Tasks: 113 total, 5 running, 108 sleeping, 0 stopped, 0 zombie
    Cpu(s): 62.0%us, 2.8%sy, 0.0%ni, 34.3%id, 0.0%wa, 0.0%hi, 0.7%si, 0.2%st
    Mem: 7680000k total, 7665504k used, 14496k free, 97268k buffers
    Swap: 2096472k total, 14904k used, 2081568k free, 3033060k cached
    PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
    31177 admin 18 0 5351m 4.0g 49m S 301.4 54.0 935:02.08 java
    31738 admin 15 0 36432 12m 1052 S 8.7 0.2 11:21.05 nginx-proxy
    

    我們的程式是 Java 應用,所以只需要關注 COMMAND 是 Java 的效能資料,COMMAND 表示啟動當前程式的命令,在 Java 程式這一行裡可以看到 CPU 利用率是 300%,不用擔心,這個是當前機器所有核加在一起的 CPU 利用率。

  2. 再使用 top 的互動命令數字 1 檢視每個 CPU 的效能資料。

    top - 22:24:50 up 463 days, 12:43, 1 user, load average: 12.55, 12.27, 11.73
    Tasks: 110 total, 3 running, 107 sleeping, 0 stopped, 0 zombie
    Cpu0 : 72.4%us, 3.6%sy, 0.0%ni, 22.7%id, 0.0%wa, 0.0%hi, 0.7%si, 0.7%st
    Cpu1 : 58.7%us, 4.3%sy, 0.0%ni, 34.3%id, 0.0%wa, 0.0%hi, 2.3%si, 0.3%st
    Cpu2 : 53.3%us, 2.6%sy, 0.0%ni, 34.1%id, 0.0%wa, 0.0%hi, 9.6%si, 0.3%st
    Cpu3 : 52.7%us, 2.7%sy, 0.0%ni, 25.2%id, 0.0%wa, 0.0%hi, 19.5%si, 0.0%st
    Cpu4 : 59.5%us, 2.7%sy, 0.0%ni, 31.2%id, 0.0%wa, 0.0%hi, 6.6%si, 0.0%st
    Mem: 7680000k total, 7663152k used, 16848k free, 98068k buffers
    Swap: 2096472k total, 14904k used, 2081568k free, 3032636k cached
    

    命令列顯示了 CPU4,說明這是一個 5 核的虛擬機器,平均每個 CPU 利用率在 60% 以上。如果這裡顯示 CPU 利用率 100%,則很有可能程式裡寫了一個死迴圈。這些引數的含義,可以對比表來檢視。

    CPU 引數含義

  3. 使用 top 的互動命令 H 檢視每個執行緒的效能資訊。

    PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
    31558 admin 15 0 5351m 4.0g 49m S 12.2 54.0 10:08.31 java
    31561 admin 15 0 5351m 4.0g 49m R 12.2 54.0 9:45.43 java
    31626 admin 15 0 5351m 4.0g 49m S 11.9 54.0 13:50.21 java
    31559 admin 15 0 5351m 4.0g 49m S 10.9 54.0 5:34.67 java
    31612 admin 15 0 5351m 4.0g 49m S 10.6 54.0 8:42.77 java
    31555 admin 15 0 5351m 4.0g 49m S 10.3 54.0 13:00.55 java
    31630 admin 15 0 5351m 4.0g 49m R 10.3 54.0 4:00.75 java
    31646 admin 15 0 5351m 4.0g 49m S 10.3 54.0 3:19.92 java
    31653 admin 15 0 5351m 4.0g 49m S 10.3 54.0 8:52.90 java
    31607 admin 15 0 5351m 4.0g 49m S 9.9 54.0 14:37.82 java
    

    在這裡可能會出現 3 種情況。

    • 第一種情況,某個執行緒 CPU 利用率一直 100%,則說明是這個執行緒有可能有死迴圈,那麼請記住這個 PID。
    • 第二種情況,某個執行緒一直在 TOP 10 的位置,這說明這個執行緒可能有效能問題。
    • 第三種情況,CPU 利用率高的幾個執行緒在不停變化,說明並不是由某一個執行緒導致 CPU 偏高。

    如果是第一種情況,也有可能是 GC 造成,可以用 jstat 命令看一下 GC 情況,看看是不是因為持久代或年老代滿了,產生 Full GC,導致 CPU 利用率持續飆高,命令和回顯如下。

    sudo /opt/java/bin/jstat -gcutil 31177 1000 5
    S0 S1 E O P YGC YGCT FGC FGCT GCT
    0.00 1.27 61.30 55.57 59.98 16040 143.775 30 77.692 221.467
    0.00 1.27 95.77 55.57 59.98 16040 143.775 30 77.692 221.467
    1.37 0.00 33.21 55.57 59.98 16041 143.781 30 77.692 221.474
    1.37 0.00 74.96 55.57 59.98 16041 143.781 30 77.692 221.474
    0.00 1.59 22.14 55.57 59.98 16042 143.789 30 77.692 221.481
    

    還可以把執行緒 dump 下來,看看究竟是哪個執行緒、執行什麼程式碼造成的 CPU 利用率高。執行以下命令,把執行緒 dump 到檔案 dump17 裡。執行如下命令。

    sudo -u admin /opt/taobao/java/bin/jstack 31177 > /home/tengfei.fangtf/dump17
    

    dump 出來內容的類似下面內容。

    "http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in Object.
    wait() [0x0000000052423000]
    java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on (a org.apache.tomcat.util.net.AprEndpoint$Worker)
    at java.lang.Object.wait(Object.java:485)
    at org.apache.tomcat.util.net.AprEndpoint$Worker.await(AprEndpoint.java:1464)
    - locked (a org.apache.tomcat.util.net.AprEndpoint$Worker)
    at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1489)
    at java.lang.Thread.run(Thread.java:662)
    

    dump 出來的執行緒 ID(nid)是十六進位制的,而我們用 TOP 命令看到的執行緒 ID 是十進位制的,所以要用 printf 命令轉換一下進位制。然後用十六進位制的 ID 去 dump 裡找到對應的執行緒。

    printf "%x\n" 31558
    

    輸出:7b46。

三、效能測試

因為要支援某個業務,有同事向我們提出需求,希望系統的某個介面能夠支援 2 萬的 QPS,因為我們的應用部署在多臺機器上,要支援兩萬的 QPS,我們必須先要知道該介面在單機上能支援多少 QPS,如果單機能支援 1 千 QPS,我們需要 20 臺機器才能支援 2 萬的 QPS。需要注意的是,要支援的 2 萬的 QPS 必須是峰值,而不能是平均值,比如一天當中有 23 個小時 QPS 不足 1 萬,只有一個小時的 QPS 達到了 2 萬,我們的系統也要支援 2 萬的 QPS。

我們先進行效能測試。我們使用公司同事開發的效能測試工具進行測試,該工具的原理是,使用者寫一個 Java 程式向伺服器端發起請求,這個工具會啟動一個執行緒池來排程這些任務,可以配置同時啟動多少個執行緒、發起請求次數和任務間隔時長。將這個程式部署在多臺機器
上執行,統計出 QPS 和響應時長。我們在 10 臺機器上部署了這個測試程式,每臺機器啟動了 100 個執行緒進行測試,壓測時長為半小時。注意不能壓測線上機器,我們壓測的是開發伺服器。

測試開始後,首先登入到伺服器裡檢視當前有多少臺機器在壓測伺服器,因為程式的埠是 12200,所以使用 netstat 命令查詢有多少臺機器連線到這個埠上。命令如下。

$ netstat -nat | grep 12200 –c
10

通過這個命令可以知道已經有 10 臺機器在壓測伺服器。QPS 達到了 1400,程式開始報錯獲取不到資料庫連線,因為我們的資料庫埠是 3306,用 netstat 命令檢視已經使用了多少個資料庫連線。命令如下。

$ netstat -nat | grep 3306 –c
12

增加資料庫連線到 20,QPS 沒上去,但是響應時長從平均 1000 毫秒下降到 700 毫秒,使用 TOP 命令觀察 CPU 利用率,發現已經 90% 多了,於是升級 CPU,將 2 核升級成4核,和線上的機器保持一致。再進行壓測,CPU 利用率下去了達到了 75%,QPS 上升到了 1800。執行一段時間後響應時長穩定在 200 毫秒。

增加應用伺服器裡執行緒池的核心執行緒數和最大執行緒數到 1024,通過 ps 命令檢視下執行緒數是否增長了,執行的命令如下。

$ ps -eLf | grep java -c
1520

再次壓測,QPS 並沒有明顯的增長,單機 QPS 穩定在 1800 左右,響應時長穩定在 200 毫秒。

我在效能測試之前先優化了程式的 SQL 語句。使用瞭如下命令統計執行最慢的 SQL,左邊的是執行時長,單位是毫秒,右邊的是執行的語句,可以看到系統執行最慢的 SQL 是 queryNews和queryNewIds,優化到幾十毫秒。

$ grep Y /home/admin/logs/xxx/monitor/dal-rw-monitor.log |awk -F',' '{print $7$5}' |
sort -nr|head -20
1811 queryNews
1764 queryNews
1740 queryNews
1697 queryNews
679 queryNewIds

效能測試中使用的其他命令

  1. 檢視網路流量。

    $ cat /proc/net/dev
    Inter-| Receive | Transmit
    face |bytes packets errs drop fifo frame compressed multicast|bytes packets
    errs drop fifo colls carrier compressed
    lo:242953548208 231437133 0 0 0 0 0 0 242953548208 231437133 0 0 0 0 0 0
    eth0:153060432504 446365779 0 0 0 0 0 0 108596061848 479947142 0 0 0 0 0 0
    bond0:153060432504 446365779 0 0 0 0 0 0 108596061848 479947142 0 0 0 0 0 0
    
  2. 檢視系統平均負載。

    $ cat /proc/loadavg
    0.00 0.04 0.85 1/1266 22459
    
  3. 檢視系統記憶體情況。

    $ cat /proc/meminfo
    MemTotal: 4106756 kB
    MemFree: 71196 kB
    Buffers: 12832 kB
    Cached: 2603332 kB
    SwapCached: 4016 kB
    Active: 2303768 kB
    Inactive: 1507324 kB
    Active(anon): 996100 kB
    部分省略
    
  4. 檢視CPU的利用率。

    cat /proc/stat
    cpu 167301886 6156 331902067 17552830039 8645275 13082 1044952 33931469 0
    cpu0 45406479 1992 75489851 4410199442 7321828 12872 688837 5115394 0
    cpu1 39821071 1247 132648851 4319596686 379255 67 132447 11365141 0
    cpu2 40912727 1705 57947971 4418978718 389539 78 110994 8342835 0
    cpu3 41161608 1211 65815393 4404055191 554651 63 112672 9108097 0
    

四、非同步任務池

Java 中的執行緒池設計得非常巧妙,可以高效併發執行多個任務,但是在某些場景下需要對執行緒池進行擴充套件才能更好地服務於系統。例如,如果一個任務仍進執行緒池之後,執行執行緒池的程式重啟了,那麼執行緒池裡的任務就會丟失。另外,執行緒池只能處理本機的任務,在叢集環境
下不能有效地排程所有機器的任務。所以,需要結合執行緒池開發一個非同步任務處理池。圖為非同步任務池設計圖。

非同步任務池設計圖
任務池的主要處理流程是,每臺機器會啟動一個任務池,每個任務池裡有多個執行緒池,當某臺機器將一個任務交給任務池後,任務池會先將這個任務儲存到資料中,然後某臺機器上的任務池會從資料庫中獲取待執行的任務,再執行這個任務。

每個任務有幾種狀態,分別是建立(NEW)、執行中(EXECUTING)、RETRY(重試)、掛起(SUSPEND)、中止(TEMINER)和執行完成(FINISH)。

  • 建立:提交給任務池之後的狀態。
  • 執行中:任務池從資料庫中拿到任務執行時的狀態。
  • 重試:當執行任務時出現錯誤,程式顯式地告訴任務池這個任務需要重試,並設定下一次執行時間。
  • 掛起:當一個任務的執行依賴於其他任務完成時,可以將這個任務掛起,當收到訊息後,再開始執行。
  • 中止:任務執行失敗,讓任務池停止執行這個任務,並設定錯誤訊息告訴呼叫端。
  • 執行完成:任務執行結束。

任務池的任務隔離。非同步任務有很多種型別,比如抓取網頁任務、同步資料任務等,不同型別的任務優先順序不一樣,但是系統資源是有限的,如果低優先順序的任務非常多,高優先順序的任務就可能得不到執行,所以必須對任務進行隔離執行。使用不同的執行緒池處理不同的任務,或者不同的執行緒池處理不同優先順序的任務,如果任務型別非常少,建議用任務型別來隔離,如果任務型別非常多,比如幾十個,建議採用優先順序的方式來隔離。

任務池的重試策略。根據不同的任務型別設定不同的重試策略,有的任務對實時性要求高,那麼每次的重試間隔就會非常短,如果對實時性要求不高,可以採用預設的重試策略,重試間隔隨著次數的增加,時間不斷增長,比如間隔幾秒、幾分鐘到幾小時。每個任務型別可以
設定執行該任務型別執行緒池的最小和最大執行緒數、最大重試次數。

使用任務池的注意事項。任務必須無狀態:任務不能在執行任務的機器中儲存資料,比如某個任務是處理上傳的檔案,任務的屬性裡有檔案的上傳路徑,如果檔案上傳到機器 1,機器 2 獲取到了任務則會處理失敗,所以上傳的檔案必須存在其他的叢集裡,比如 OSS 或 SFTP。

非同步任務的屬性。包括任務名稱、下次執行時間、已執行次數、任務型別、任務優先順序和執行時的報錯資訊(用於快速定位問題)。

相關文章