詳解 Redis 內存管理機制和實現

來源關注程序員歷小冰 ,

作者歷小冰

Redis是一個基於內存的鍵值數據庫,其內存管理是非常重要的。本文內存管理的內容包括:過期鍵的懶性刪除和過期刪除以及內存溢出控制策略。

最大內存限制

Redis使用 maxmemory 參數限制最大可用內存,默認值為0,表示無限制。限制內存的目的主要 有:

  • 用於緩存場景,當超出內存上限 maxmemory 時使用 LRU 等刪除策略釋放空間。
  • 防止所用內存超過服務器物理內存。因為 Redis 默認情況下是會盡可能多使用服務器的內存,可能會出現服務器內存不足,導致 Redis 進程被殺死。
詳解 Redis 內存管理機制和實現

maxmemory 限制的是Redis實際使用的內存量,也就是 used_memory統計項對應的內存。由於內存碎片率的存在,實際消耗的內存 可能會比maxmemory設置的更大,實際使用時要小心這部分內存溢出。具體Redis 內存監控的內容請查看一文了解 Redis 內存監控和內存消耗。

Redis默認無限使用服務器內存,為防止極端情況下導致系統內存耗 盡,建議所有的Redis進程都要配置maxmemory。在保證物理內存可用的情況下,系統中所有Redis實例可以調整 maxmemory參數來達到自由伸縮內存的目的。

內存回收策略

Redis 回收內存大致有兩個機制:一是刪除到達過期時間的鍵值對象;二是當內存達到 maxmemory 時觸發內存移除控制策略,強制刪除選擇出來的鍵值對象。

刪除過期鍵對象

Redis 所有的鍵都可以設置過期屬性,內部保存在過期表中,鍵值表和過期表的結果如下圖所示。當 Redis保存大量的鍵,對每個鍵都進行精準的過期刪除可能會導致消耗大量的 CPU,會阻塞 Redis 的主線程,拖累 Redis 的性能,因此 Redis 採用惰性刪除和定時任務刪除機制實現過期鍵的內存回收。

詳解 Redis 內存管理機制和實現

惰性刪除是指當客戶端操作帶有超時屬性的鍵時,會檢查是否超過鍵的過期時間,然後會同步或者異步執行刪除操作並返回鍵已經過期。這樣可以節省 CPU成本考慮,不需要單獨維護過期時間鏈表來處理過期鍵的刪除。

過期鍵的惰性刪除策略由 db.c/expireifNeeded 函數實現,所有對數據庫的讀寫命令執行之前都會調用 expireifNeeded 來檢查命令執行的鍵是否過期。如果鍵過期,expireifNeeded 會將過期鍵從鍵值表和過期表中刪除,然後同步或者異步釋放對應對象的空間。源碼展示的時 Redis 4.0 版本。

expireIfNeeded 先從過期表中獲取鍵對應的過期時間,如果當前時間已經超過了過期時間(lua腳本執行則有特殊邏輯,詳看代碼註釋),則進入刪除鍵流程。刪除鍵流程主要進行了三件事:

  • 一是刪除操作命令傳播,通知 slave 實例並存儲到 AOF 緩衝區中
  • 二是記錄鍵空間事件,
  • 三是根據 lazyfreelazyexpire 是否開啟進行異步刪除或者異步刪除操作。
詳解 Redis 內存管理機制和實現

詳解 Redis 內存管理機制和實現

上圖是寫命令傳播的示意圖,刪除命令的傳播和它一致。propagateExpire 函數先調用 feedAppendOnlyFile 函數將命令同步到 AOF 的緩衝區中,然後調用 replicationFeedSlaves函數將命令同步到所有的 slave 中。Redis 複製的機制可以查看Redis 複製過程詳解。

詳解 Redis 內存管理機制和實現

dbAsyncDelete 函數會先調用 dictDelete 來刪除過期表中的鍵,然後處理鍵值表中的鍵值對象。它會根據值的佔用的空間來選擇是直接釋放值對象,還是交給 bio 異步釋放值對象。判斷依據就是值的估計大小是否大於 LAZYFREE_THRESHOLD 閾值。鍵對象和 dictEntry 對象則都是直接被釋放。

詳解 Redis 內存管理機制和實現

詳解 Redis 內存管理機制和實現

dictUnlink 會將鍵值從鍵值表中刪除,但是卻不釋放 key、val和對應的表entry對象,而是將其直接返回,然後再調用dictFreeUnlinkedEntry進行釋放。dictDelete 是它的兄弟函數,但是會直接釋放相應的對象。二者底層都通過調用 dictGenericDelete來實現。dbAsyncDelete d的兄弟函數 dbSyncDelete 就是直接調用dictDelete來刪除過期鍵。

詳解 Redis 內存管理機制和實現

Redis 有自己的 bio 機制,主要是處理 AOF 落盤、懶刪除邏輯和關閉大文件fd。bioCreateBackgroundJob 函數將釋放值對象的 job 加入到隊列中,bioProcessBackgroundJobs會從隊列中取出任務,根據類型進行對應的操作。

詳解 Redis 內存管理機制和實現

dbSyncDelete 則是直接刪除過期鍵,並且將鍵、值和 DictEntry 對象都釋放。

詳解 Redis 內存管理機制和實現

但是單獨用這種方式存在內存洩露的問題,當過期鍵一直沒有訪問將無法得到及時刪除,從而導致內存不能及時釋放。正因為如此,Redis還提供另一種定時任 務刪除機制作為惰性刪除的補充。

Redis 內部維護一個定時任務,默認每秒運行10次(通過配置控制)。定時任務中刪除過期鍵邏輯採用了自適應算法,根據鍵的 過期比例、使用快慢兩種速率模式回收鍵,流程如下圖所示。

詳解 Redis 內存管理機制和實現

  • 1)定時任務首先根據快慢模式( 慢模型掃描的鍵的數量以及可以執行時間都比快模式要多 )和相關閾值配置計算計算本週期最大執行時間、要檢查的數據庫數量以及每個數據庫掃描的鍵數量。
  • 2) 從上次定時任務未掃描的數據庫開始,依次遍歷各個數據庫。
  • 3)從數據庫中隨機選手 ACTIVEEXPIRECYCLELOOKUPSPER_LOOP 個鍵,如果發現是過期鍵,則調用 activeExpireCycleTryExpire 函數刪除它。
  • 4)如果執行時間超過了設定的最大執行時間,則退出,並設置下一次使用慢模式執行。
  • 5)未超時的話,則判斷是否採樣的鍵中是否有25%的鍵是過期的,如果是則繼續掃描當前數據庫,跳到第3步。否則開始掃描下一個數據庫。

定期刪除策略由 expire.c/activeExpireCycle 函數實現。在redis事件驅動的循環中的eventLoop->beforesleep和 週期性操作 databasesCron 都會調用 activeExpireCycle 來處理過期鍵。但是二者傳入的 type 值不同,一個是ACTIVEEXPIRECYCLESLOW 另外一個是ACTIVEEXPIRECYCLEFAST。activeExpireCycle 在規定的時間,分多次遍歷各個數據庫,從 expires 字典中隨機檢查一部分過期鍵的過期時間,刪除其中的過期鍵,相關源碼如下所示。

詳解 Redis 內存管理機制和實現

詳解 Redis 內存管理機制和實現

詳解 Redis 內存管理機制和實現

activeExpireCycleTryExpire 函數的實現就和 expireIfNeeded 類似,這裡就不贅述了。

詳解 Redis 內存管理機制和實現

定期刪除策略的關鍵點就是刪除操作執行的時長和頻率:

  • 如果刪除操作太過頻繁或者執行時間太長,就對 CPU 時間不是很友好,CPU 時間過多的消耗在刪除過期鍵上。
  • 如果刪除操作執行太少或者執行時間太短,就不能及時刪除過期鍵,導致內存浪費。

內存溢出控制策略

當Redis所用內存達到maxmemory上限時會觸發相應的溢出控制策略。具體策略受maxmemory-policy參數控制,Redis支持6種策略,如下所示:

  • 1)noeviction:默認策略,不會刪除任何數據,拒絕所有寫入操作並返 回客戶端錯誤信息(error)OOM command not allowed when used memory,此 時Redis只響應讀操作。
  • 2)volatile-lru:根據LRU算法刪除設置了超時屬性(expire)的鍵,直 到騰出足夠空間為止。如果沒有可刪除的鍵對象,回退到noeviction策略。
  • 3)allkeys-lru:根據LRU算法刪除鍵,不管數據有沒有設置超時屬性, 直到騰出足夠空間為止。
  • 4)allkeys-random:隨機刪除所有鍵,直到騰出足夠空間為止。
  • 5)volatile-random:隨機刪除過期鍵,直到騰出足夠空間為止。
  • 6)volatile-ttl:根據鍵值對象的ttl屬性,刪除最近將要過期數據。如果沒有,回退到noeviction策略。

內存溢出控制策略可以使用 config set maxmemory-policy {policy} 語句進行動態配置。Redis 提供了豐富的空間溢出控制策略,我們可以根據自身業務需要進行選擇。

當設置 volatile-lru 策略時,保證具有過期屬性的鍵可以根據 LRU 剔除,而未設置超時的鍵可以永久保留。還可以採用allkeys-lru 策略把 Redis 變為純緩存服務器使用。

當Redis因為內存溢出刪除鍵時,可以通過執行 info stats 命令查看 evicted_keys 指標找出當前 Redis 服務器已剔除的鍵數量。

每次Redis執行命令時如果設置了maxmemory參數,都會嘗試執行回收 內存操作。當Redis一直工作在內存溢出(used_memory>maxmemory)的狀態下且設置非 noeviction 策略時,會頻繁地觸發回收內存的操作,影響Redis 服務器的性能,這一點千萬要引起注意。


分享到:


相關文章: