服務重啟了,如何保證執行緒池中的資料不丟失?

苏三说技术發表於2024-08-30

大家好,我是蘇三,又跟大家見面了。

前言

最近有位小夥伴在我的技術群裡,問了我一個問題:服務down機了,執行緒池中如何保證不丟失資料?

這個問題挺有意思的,今天透過這篇文章,拿出來跟大家一起探討一下。

1 什麼是執行緒池?

之前沒有執行緒池的時候,我們在程式碼中,建立一個執行緒有兩種方式:

  1. 繼承Thread類
  2. 實現Runnable介面

雖說透過這兩種方式建立一個執行緒,非常方便。

但也帶來了下面的問題:

  1. 建立和銷燬一個執行緒,都是比較耗時,頻繁的建立和銷燬執行緒,非常影響系統的效能。
  2. 無限制的建立執行緒,會導致記憶體不足。
  3. 有新任務過來時,必須要先建立好執行緒才能執行,不能直接複用執行緒。

為了解決上面的這些問題,Java中引入了:執行緒池

它相當於一個存放執行緒的池子。

使用執行緒池帶來了下面3個好處:

  1. 降低資源消耗。透過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
  2. 提高響應速度。當任務到達時,可以直接使用已有空閒的執行緒,不需要的等到執行緒建立就能立即執行。
  3. 提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性。而如果我們使用執行緒池,可以對執行緒進行統一的分配、管理和監控。

2 執行緒池原理

先看看執行緒池的構造器:

public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler)
  • corePoolSize:核心執行緒數,執行緒池維護的最少執行緒數。
  • maximumPoolSize:最大執行緒數,執行緒池允許建立的最大執行緒數。
  • keepAliveTime:執行緒存活時間,當執行緒數超過核心執行緒數時,多餘的空閒執行緒的存活時間。
  • unit:時間單位。
  • workQueue:任務佇列,用於儲存等待執行的任務。
  • threadFactory:執行緒工廠,用於建立新執行緒。
  • handler:拒絕策略,當任務無法執行時的處理策略。

執行緒池的核心流程圖如下:

服務重啟了,如何保證執行緒池中的資料不丟失?

執行緒池的工作過程如下:

  1. 執行緒池初始化:根據corePoolSize初始化核心執行緒。
  2. 任務提交:當任務提交到執行緒池時,根據當前執行緒數判斷:
  • 若當前執行緒數小於corePoolSize,建立新的執行緒執行任務。
  • 若當前執行緒數大於或等於corePoolSize,任務被加入workQueue佇列。
  1. 任務處理:當有空閒執行緒時,從workQueue中取出任務執行。
  2. 執行緒擴充套件:若佇列已滿且當前執行緒數小於maximumPoolSize,建立新的執行緒處理任務。
  3. 執行緒回收:當執行緒空閒時間超過keepAliveTime,多餘的執行緒會被回收,直到執行緒數不超過corePoolSize。
  4. 拒絕策略:若佇列已滿且當前執行緒數達到maximumPoolSize,則根據拒絕策略處理新任務。

說白了線上程池中,多餘的任務會被放到workQueue任務佇列中。

這個任務佇列的資料儲存在記憶體中。

這樣就會出現一些問題。

接下來,看看執行緒池有哪些問題。

3 執行緒池有哪些問題?

在JDK中為了方便大家建立執行緒池,專門提供了Executors這個工具類。

3.1 佇列過大

Executors.newFixedThreadPool,它可以建立固定執行緒數量的執行緒池,任務佇列使用的是LinkedBlockingQueue,預設最大容量是Integer.MAX_VALUE。

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, 
                               nThreads,
                                     0L, 
                  TimeUnit.MILLISECONDS,
     new LinkedBlockingQueue<Runnable>(),
                          threadFactory);
}

如果向newFixedThreadPool執行緒池中提交的任務太多,可能會導致LinkedBlockingQueue非常大,從而出現OOM問題。

3.2 執行緒太多

Executors.newCachedThreadPool,它可以建立可緩衝的執行緒池,最大執行緒數量是Integer.MAX_VALUE,任務佇列使用的是SynchronousQueue。

public static ExecutorService newCachedThreadPool() {
  return new ThreadPoolExecutor(0, 
                Integer.MAX_VALUE,
                               60L, 
                  TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>());
}

如果向newCachedThreadPool執行緒池中提交的任務太多,可能會導致建立大量的執行緒,也會出現OOM問題。

3.3 資料丟失

如果執行緒池在執行過程中,服務突然被重啟了,可能會導致執行緒池中的資料丟失。

上面的OOM問題,我們在日常開發中,可以透過自定義執行緒池的方式解決。

比如建立這樣的執行緒池:

new ThreadPoolExecutor(8, 
                       10,
                       30L, 
     TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<Runnable>(300),
            threadFactory);

自定義了一個最大執行緒數量和任務佇列都在可控範圍內執行緒池。

這樣做基本上不會出現OOM問題。

但執行緒池的資料丟失問題,光靠自身的功能很難解決。

4 如何保證資料不丟失?

執行緒池中的資料,是儲存到記憶體中的,一旦遇到伺服器重啟了,資料就會丟失。

之前的系統流程是這樣的:

服務重啟了,如何保證執行緒池中的資料不丟失?

使用者請求過來之後,先處理業務邏輯1,它是系統的核心功能。

然後再將任務提交到執行緒池,由它處理業務邏輯2,它是系統的非核心功能。

但如果執行緒池在處理的過程中,服務down機了,此時,業務邏輯2的資料就會丟失。

那麼,如何保證資料不丟失呢?

答:需要提前做持久化

我們最佳化的系統流程如下:

服務重啟了,如何保證執行緒池中的資料不丟失?

使用者請求過來之後,先處理業務邏輯1,緊接著向DB中寫入一條任務資料,狀態是:待執行。

處理業務邏輯1和向DB寫任務資料,可以在同一個事務中,方便出現異常時回滾。

然後有一個專門的定時任務,每個一段時間,按新增時間升序,分頁查詢狀態是待執行的任務。

最早的任務,最先被查出來。

然後將查出的任務提交到執行緒池中,由它處理業務邏輯2。

處理成功之後,修改任務的待執行狀態為:已執行。

需要注意的是:業務邏輯2的處理過程,要做冪等性設計,同一個請求允許被執行多次,其結果不會有影響。

如果此時,執行緒池在處理的過程中,服務down機了,業務邏輯2的資料會丟失。

但此時DB中儲存了任務的資料,並且丟失那些任務的狀態還是:待執行。

在下一次定時任務週期開始執行時,又會將那些任務資料重新查詢出來,重新提交到執行緒池中。

業務邏輯2丟失的資料,又自動回來了。

如果要考慮失敗的情況,還需要在任務表中增加一個失敗次數欄位。

在定時任務的執行緒池中執行業務邏輯2失敗了,在下定時任務執行時可以自動重試。

但不可能無限制的一直重試下去。

當失敗超過了一定的次數,可以將任務狀態改成:失敗。

這樣後續可以人工處理。

最後說一句(求關注,別白嫖我)
如果這篇文章對您有所幫助,或者有所啟發的話,幫忙掃描下發二維碼關注一下,您的支援是我堅持寫作最大的動力。

求一鍵三連:點贊、轉發、在看。
關注公眾號:【蘇三說技術】,在公眾號中回覆:面試、程式碼神器、開發手冊、時間管理有超讚的粉絲福利,另外回覆:加群,可以跟很多BAT大廠的前輩交流和學習。

相關文章