Redis例項在執行的時候,要和許多物件進行互動,這些不同的互動物件會有不同的操作。下面我們來看看,這些不同的互動物件以及相應的主要操作有哪些。
-
客戶端:鍵值對的增刪改查操作。
-
磁碟:生成RDB快照、記錄AOF日誌、AOF日誌重寫。
-
主從節點:主庫生成、傳輸RDB檔案,從庫接受RDB檔案、清空資料庫、載入RDB檔案。
下面我們來分析一下哪些操作會引起主執行緒阻塞。
1.和客戶端互動時的阻塞點。
鍵值對的增刪改查操作是Redis和客戶端互動的主要部分,也是Redis主執行緒執行的主要任務。所以複雜度高的增刪改查操作肯定會阻塞Redis。在Redis中複雜度高的操作都是對集合的操作,通常時間複雜度為O(N)。所以在使用的過程中需要注意起來,例如對集合的全量查詢操作HGETALL以及對集合的聚合統計操作,例如求交、並和差集。所以集合全量查詢和聚合操作是Redis的一個阻塞點。
除此之外,對集合刪除操作也會有潛在的阻塞風險。因為刪除操作的本質是要釋放鍵值對佔用的記憶體空間。釋放記憶體只是第一步,為了更加高效地管理記憶體空間,在應用程式釋放記憶體時,作業系統需要把釋放掉的記憶體塊插入一個空閒記憶體塊的連結串列,以便後續進行管理和再分配。這個過程本身需要一定時間,而且會阻塞當前釋放記憶體的應用程式,所以,如果一下子釋放了大量記憶體,空閒記憶體塊連結串列操作時間就會增加,相應地就會造成 Redis 主執行緒的阻塞。所以刪除bigkey也是Redis的一個阻塞點。
因為清空資料庫涉及到刪除和釋放所有的鍵值對。所以清空資料庫也是Redis的一個阻塞點。
2.和磁碟互動時的阻塞點。
Redis是採用後臺子程式的方式生成RDB快照檔案,以及執行AOF日誌重寫操作。所以這兩個操作由子程式負責執行,因此不會成為Redis的阻塞點。
但是Redis直接記錄AOF日誌時,會根據不同的寫回策略對資料做落盤儲存。
如果有大量的寫操作需要記錄在AOF日誌中,並寫回策略設定成同步寫回的話,就會阻塞主執行緒了。所以AOF日誌同步寫也是Redis的一個阻塞點。
3.主從節點互動時的阻塞點
在主從叢集中,主庫需要生成RDB檔案,並傳輸給從庫。主庫在複製的過程中,建立和傳輸 RDB 檔案都是由後臺子程式來完成的,不會阻塞主執行緒的執行。但是,對於從庫來說,它在接收了RDB 檔案後,需要使用 FLUSHDB 命令清空當前資料庫,然後需要把 RDB 檔案載入到記憶體,這個過程的快慢和 RDB 檔案的大小密切相關,RDB 檔案越大,載入過程越慢,所以,載入 RDB 檔案就成為了 Redis 又一個阻塞點。
所以總結一下,Redis中阻塞主執行緒的操作主要有以下五種。
-
集合全量查詢和聚合操作。
-
bigkey 刪除。
-
清空資料庫。
-
AOF 日誌同步寫。
-
從庫載入 RDB 檔案。
那有什麼方式可以避免阻塞式操作呢?Redis提供了非同步執行緒機制。為了避免一些阻塞主執行緒的操作,Redis會啟動一些子執行緒,然後把一些任務交給這些子執行緒去在後臺完成,而不再由主執行緒來執行這些任務。下面我們來看一下哪些阻塞主執行緒的操作可以採用非同步執行呢?
如果一個操作能被非同步執行,就意味著,它並不是Redis主執行緒的關鍵路徑上的操作。所謂的關鍵路徑是指客戶端把請求傳送給Redis後等著Redis返回資料結果的操作。如下圖所示。
主執行緒接收到操作1後,因為操作1並不用給客戶端返回具體的資料,所以,主執行緒可以把它交給後臺子執行緒來完成,同時只要給客戶端返回一個“OK”結果就行。在子執行緒執行操作1的時候,客戶端又向Redis例項傳送了操作2,而此時,客戶端是需要使用操作2返回的資料結果的,如果操作2不返回結果,那麼,客戶端將一直處於等待狀態。在這個例子中,操作 1 就不算關鍵路徑上的操作,因為它不用給客戶端返回具體資料,所以可以由後臺子執行緒非同步執行。而操作 2 需要把結果返回給客戶端,它就是關鍵路徑上的操作,所以主執行緒必須立即把這個操作執行完。
對於 Redis 來說,讀操作是典型的關鍵路徑操作,因為客戶端傳送了讀操作之後,就會等待讀取的資料返回。而Redis 的集合全量查詢和聚合操作都涉及到了讀操作,所以,它們是不能進行非同步操作了。
我們再來看看刪除操作。刪除操作並不需要給客戶端返回具體的資料結果,所以不是關鍵路徑操作。所以“bigkey 刪除”和“清空資料庫”,都是對資料做刪除,所以我們可以用後臺子執行緒來非同步執行刪除操作。
對於“AOF 日誌同步寫”來說,為了保證資料可靠性,Redis 例項需要保證 AOF 日誌中的操作記錄已經落盤,這個操作雖然需要例項等待,但它並不會返回具體的資料結果給例項。所以,我們也可以啟動一個子執行緒來執行 AOF 日誌的同步寫,而不用讓主執行緒等待 AOF 日誌的寫完成。
最後,我們再來看下“從庫載入 RDB 檔案”這個操作。從庫要想對客戶端提供資料存取服務,就必須把 RDB 檔案載入完成。所以,這個操作也屬於關鍵路徑上的操作,我們必須讓從庫的主執行緒來執行。
對於 Redis 的五大阻塞點來說,除了“集合全量查詢和聚合操作”和“從庫載入 RDB 檔案”,其他三個阻塞點涉及的操作都不在關鍵路徑上,所以,我們可以使用 Redis 的非同步子執行緒機制來實現 bigkey 刪除,清空資料庫,以及 AOF 日誌同步寫。
4.非同步的子執行緒機制
Redis 主執行緒啟動後,會使用作業系統提供的 pthread_create 函式建立 3 個子執行緒,分別由它們負責 AOF 日誌寫操作、鍵值對刪除以及檔案關閉的非同步執行。主執行緒通過一個連結串列形式的任務佇列和子執行緒進行互動。當收到鍵值對刪除和清空資料庫的操作時,主執行緒會把這個操作封裝成一個任務,放入到任務佇列中,然後給客戶端返回一個完成資訊,表明刪除已經完成。但實際上,這個時候刪除還沒有執行,等到後臺子執行緒從任務佇列中讀取任務後,才開始實際刪除鍵值對,並釋放相應的記憶體空間。因此,我們把這種非同步刪除也稱為惰性刪除(lazy free)。此時,刪除或清空操作不會阻塞主執行緒,這就避免了對主執行緒的效能影響。和惰性刪除類似,當 AOF 日誌配置成 everysec 選項後,主執行緒會把 AOF 寫日誌操作封裝成一個任務,也放到任務佇列中。後臺子執行緒讀取任務後,開始自行寫入 AOF 日誌,這樣主執行緒就不用一直等待 AOF 日誌寫完了。
當你的集合中有大量的元素要刪除時,我建議你使用非同步刪除命令 UNLINK 。而對於清空資料庫來說,可以在 FLUSHDB 和 FLUSHALL 命令後加上 ASYNC 選項,這樣就可以讓後臺子執行緒非同步地清空資料庫。
FLUSHDB ASYNC FLUSHALL AYSNC
更多硬核知識,請關注公眾號"程式設計師學長"。