天天看點

第十六章 異步機制:如何避免單線程模型的阻塞

第十六章 異步機制:如何避免單線程模型的阻塞 ?

  • Redis 的網絡 IO 和鍵值對讀寫是由主線程完成的。
  • 如果在主線程上執行的操作消耗的時間太長,就會引起主線程阻塞。

Redis 執行個體有哪些阻塞點 ?

與 Redis 執行個體互動的對象有哪些 ?
  • 用戶端:網絡 IO,鍵值對增删改查操作,資料庫操作;
  • 磁盤:生成 RDB 快照,記錄 AOF 日志,AOF 日志重寫;
  • 主從節點:主庫生成、傳輸 RDB 檔案,從庫接收 RDB 檔案、清空資料庫、加載 RDB 檔案;
  • 切片叢集執行個體:向其他執行個體傳輸哈希槽資訊,資料遷移。
第十六章 異步機制:如何避免單線程模型的阻塞

和用戶端互動時的阻塞點

  • Redis 主線程執行的主要任務是:鍵值對的增删改查操作,
  • 是以複雜度高的增删改查操作肯定會阻塞 Redis。
Redis 執行個體和用戶端互動有哪些阻塞點呢 ?
  • 集合全量查詢和聚合操作會阻塞 Redis
  • 删除包含大量元素的集合的操作會阻塞 Redis
  • 清空資料庫操作會阻塞 Redis
為什麼删除操作還會阻塞 Redis ?
  • 删除操作的本質是要釋放鍵值對占用的記憶體空間。
  • 在應用程式釋放記憶體時,作業系統需要把釋放掉的記憶體塊插入一個空閑記憶體塊的連結清單,以便後續進行管理和再配置設定。
  • 這個過程本身需要一定時間,而且會阻塞目前​

    ​釋放記憶體​

    ​的應用程式。
  • 如果一下子釋放了大量記憶體,空閑記憶體塊連結清單操作時間就會增加,相應地就會造成 Redis 主線程的​

    ​阻塞​

    ​。
什麼時候會釋放大量記憶體呢 ?

删除大量鍵值對資料的時候,最典型的就是删除包含了大量元素的集合,也稱為 ​

​bigkey​

​ 删除。

和磁盤互動時的阻塞點

  • 生成 RDB 快照檔案和執行 AOF 日志重寫操作由子程序完成,不阻塞主線程。
  • 如果有大量的寫操作需要記錄在 AOF 日志中,并同步寫回的話,就會​

    ​阻塞​

    ​主線程,耗時大概 1~2 ms。

主從節點互動時的阻塞點

  • 主庫在複制的過程中,建立和傳輸 RDB 檔案都是由子程序來完成的,不會阻塞主線程。
  • 從庫接收RDB檔案後,需要使用 FLUSHDB 指令清空目前資料庫,這樣會​

    ​阻塞​

    ​ 從庫的Redis 主線程。
  • 從庫在清空目前資料庫後,還需要把 RDB 檔案加載到記憶體,這個過程的快慢和 RDB 檔案的大小密切相關,RDB 檔案越大,加載過程越慢,這樣也會​

    ​阻塞​

    ​ 從庫的Redis 主線程。

切片叢集執行個體互動時的阻塞點

  • 當沒有 bigkey 時,切片叢集的各執行個體在進行互動時不會阻塞主線程。
  • 如果你使用了 Redis Cluster 方案,而且同時正好遷移的是 bigkey 的話,就會造成主線程的​

    ​阻塞​

    ​,因為 Redis Cluster 使用了同步遷移。

總結有哪些阻塞點 ?

  • 集合全量查詢和聚合操作;
  • bigkey 删除;
  • 清空資料庫;
  • AOF 日志同步寫;
  • 從庫加載 RDB 檔案。
如果在主線程中執行這些操作,必然會導緻主線程長時間無法服務其他請求。為了避免阻塞式操作,Redis 提供了​

​異步線程​

​機制。

所謂的異步線程機制,就是指,Redis 會啟動一些子線程,然後把一些任務交給這些子線程,讓它們在背景完成,而不再由主線程來執行這些任務。

使用​

​異步線程機制​

​執行操作,可以避免阻塞主線程。

哪些阻塞點可以異步執行 ?

異步執行 對操作的要求 ?
第十六章 異步機制:如何避免單線程模型的阻塞
  • 主線程接收到操作 1 後,因為操作 1 并不用給用戶端傳回具體的資料,是以,主線程可以把它交給背景子線程來完成,同時隻要給用戶端傳回一個“OK”結果就行。
  • 在子線程執行操作 1 的時候,用戶端又向 Redis 執行個體發送了操作 2,而此時,用戶端是需要使用操作 2 傳回的資料結果的,如果操作 2 不傳回結果,那麼,用戶端将一直處于等待狀态。
  • 在這個例子中,操作 1 就不算關鍵路徑上的操作,因為它不用給用戶端傳回具體資料,是以可以由背景子線程​

    ​異步執行​

    ​。
  • 而操作 2 需要把結果傳回給用戶端,它就是關鍵路徑上的操作,是以主線程必須立即把這個操作執行完。
哪些阻塞點可以異步執行呢 ? 哪些算關鍵路徑操作,哪些不算呢 ?
  • 讀操作是典型的關鍵路徑操作,因為它需要把結果傳回給用戶端,是以它不能進行異步操作。
  • 删除操作并不需要給用戶端傳回具體的資料結果,是以不算是關鍵路徑操作,可以使用背景子線程異步執行删除操作。
  • 對于AOF同步寫來說,為了保證資料可靠性,Redis 執行個體需要保證 AOF 日志中的操作記錄已經落盤,這個操作雖然需要執行個體等待,但它并不會傳回具體的資料結果給執行個體。
  • 是以,我們也可以啟動一個子線程來執行 AOF 日志的同步寫,而不用讓主線程等待 AOF 日志的寫完成。
  • 對于從庫加載 RDB 檔案來說,從庫要想對用戶端提供資料存取服務,就必須把 RDB 檔案加載完成。
  • 是以,這個操作也屬于​

    ​關鍵路徑​

    ​​上的操作,我們必須讓​

    ​從庫的主線程​

    ​來執行。

Redis 實作的異步子線程機制具體是怎麼執行呢 ?

  • Redis 主線程啟動後,會使用作業系統提供的 pthread_create 函數建立 3 個子線程,分别由它們負責
  • AOF 日志寫操作
  • 鍵值對删除
  • 檔案關閉的異步執行
  • 主線程通過一個連結清單形式的任務隊列和子線程進行互動。當收到鍵值對删除和清空資料庫的操作時,主線程會把這個操作封裝成一個任務,放入到​

    ​任務隊列​

    ​中,然後給用戶端傳回一個完成資訊,表明删除已經完成。
  • 但實際上,這個時候删除還沒有執行,等到背景子線程從任務隊列中讀取任務後,才開始實際删除鍵值對,并釋放相應的記憶體空間。是以,我們把這種異步删除也稱為​

    ​惰性删除​

    ​​(lazy free)。此時,删除或清空操作不會​

    ​阻塞​

    ​主線程,這就避免了對主線程的性能影響。
  • 和惰性删除類似,當 AOF 日志配置成​

    ​everysec​

    ​ 選項後,主線程會把 AOF 寫日志操作封裝成一個任務,也放到任務隊列中。背景子線程讀取任務後,開始自行寫入 AOF 日志,這樣主線程就不用一直等待 AOF 日志寫完了。
第十六章 異步機制:如何避免單線程模型的阻塞
異步的鍵值對删除和資料庫清空操作是 ​

​Redis 4.0​

​ 後提供的功能,Redis 也提供了新的指令來執行這兩個操作:
  • 鍵值對删除:當你的集合類型中有大量元素(例如有百萬級别或千萬級别元素)需要删除時,我建議你使用​

    ​UNLINK​

    ​ 指令。
  • 清空資料庫:可以在​

    ​FLUSHDB​

    ​​ 和​

    ​FLUSHALL​

    ​​ 指令後加上​

    ​ASYNC​

    ​ 選項,這樣就可以讓背景子線程異步地清空資料庫,如下所示:
FLUSHDB ASYNC
FLUSHALL AYSNC      
對于 ​

​集合全量查詢和聚合操作​

​​、​

​從庫加載 RDB 檔案​

​​ 這兩個​

​無法使用異步操作​

​來完成的阻塞點,有以下建議:
  • 集合全量查詢和聚合操作:可以使用 SCAN 指令,分批讀取資料,再在用戶端進行聚合計算;
  • 從庫加載 RDB 檔案:把主庫的資料量大小控制在 2~4GB 左右,以保證 RDB 檔案能以較快的速度加載。

Redis 的寫操作(例如 SET、HSET、SADD 等)是在關鍵路徑上嗎 ?

需要用戶端根據業務需要來區分:
  • 如果用戶端依賴操作傳回值的不同,進而需要處理不同的業務邏輯,那麼HSET和SADD操作算關鍵路徑,而SET操作不算關鍵路徑。
  • 因為HSET和SADD操作,如果field或member不存在時,Redis結果會傳回1,否則傳回0。而SET操作傳回的結果都是OK,用戶端不需要關心結果有什麼不同。
  • 如果用戶端不關心傳回值,隻關心資料是否寫入成功,那麼SET/HSET/SADD不算關鍵路徑,多次執行這些指令都是幂等的,這種情況下可以放到異步線程中執行。
什麼時候 Redis 才會真正的異步釋放記憶體 ?

lazy free機制:Redis收到鍵值對删除和清空資料庫的指令時,主線線程會把這個操作封裝成一個任務,放入任務隊列中,然後給用戶端傳回一個完成資訊,但實際上,這個删除還沒有執行,需要等待背景子線程從任務隊列中讀取到這個任務後,才開始實際删除鍵值對,并釋放相應的記憶體空間。

但是 lazy-free 是4.0新增功能,預設關閉。開啟這個配置後, 除了 ​

​replica-lazy-flush​

​​ 之外,其他情況都隻是​

​可能​

​​去異步釋放key的記憶體,并不是每次​

​必定​

​異步釋放記憶體的。

是否會真正異步釋放記憶體,這和key的類型、編碼方式、元素數量都有關系!!!

  • 當Hash/Set底層采用哈希表存儲(非ziplist/int編碼存儲)時,并且元素數量超過64個
  • 當ZSet底層采用跳表存儲(非ziplist編碼存儲)時,并且元素數量超過64個
  • 當List連結清單節點數量超過64個(注意,不是元素數量,而是連結清單節點的數量,List的實作是在每個節點包含了若幹個元素的資料,這些元素采用ziplist存儲)

繼續閱讀