redis-11 redis清除過期 key 詳解


始因

  有時候線上可能會遇到這樣的問題:

  明明我設置了對應的 key 以及超時時間,但是在使用的過程當中發現對應的 key 丟失了,尤其是在用戶賬號登錄狀態保持有效期的場景下,會越發的明顯。即:一個用戶正常登錄會產生一個有效期為一天的 token,這樣用戶再次進入網站是不需要登錄的。但是發生 key 丟失問題就會導致用戶需要頻繁的重新登錄,用戶體驗相當不好。導致這種問題的原因一般有以下兩種情況:

  1. token 生成時出現邏輯問題

  2. 驗證 token 時出問題了

  對於上線穩定的項目來說,發生 1 的概率基本為 0。那么會立馬定位到 2 的情況。這種情況就會引發我們今天討論的問題:

  redis 如何自動清理過期 key,以及對應 key 沒有過期但是也會被清理掉呢?說人話:redis 內部如何清理過期 key?

常見刪除策略(拋開 redis)

  1. 定時刪除:在設置鍵的過期時間的同時,創建一個定時器 timer。讓定時器在鍵的過期時間來臨時,立即執行對鍵的刪除操作。
  2. 惰性刪除:放任鍵過期不管,但是每次從鍵空間中獲取鍵時,都檢查取得的鍵是否過期,如果過期的話,就刪除該鍵;如果沒有過期,就返回該鍵。
  3. 定期刪除:每隔一段時間程序就對數據庫進行一次檢查,刪除里面的過期鍵。至於要刪除多少過期鍵,以及要檢查多少個數據庫,則由算法決定。

  在上述的三種策略中 定時刪除 和 定期刪除 屬於不同時間粒度的 主動刪除,惰性刪除屬於 被動刪除

  以上三種策略都有各自的優缺點:

    1. 定時刪除 對內存使用率有優勢,但是對 CPU 不友好;

    2. 惰性刪除 對內存不友好,如果某些鍵值對一直不被使用,那么會造成一定量的內存浪費;

    3. 定期刪除 是 定時刪除 和 惰性刪除 的折中。

正常情況下 redis 中的實現

  Reids 采用的是 惰性刪除 和 定時刪除 的結合,一般來說可以借助 最小堆 來實現 定時器,不過 Redis 的設計考慮到時間事件的有限種類 和 數量,使用了 無序鏈表 存儲時間事件,這樣如果在此基礎上實現定時刪除,就意味着 O(N) 遍歷獲取最近需要刪除的數據。

定期刪除策略

Redis 會將每個設置了過期時間的 key 放入到一個獨立的字典中,默認每 100ms 進行一次過期掃描:

  1. 隨機抽取 20 個 key

  2. 刪除這 20 個key中過期的key

  3. 如果過期的 key 比例超過 1/4,就重復步驟 1,繼續刪除。

為什不掃描所有的 key?

  Redis 是單線程,全部掃描豈不是卡死了。而且為了防止每次掃描過期的 key 比例都超過 1/4,導致不停循環卡死線程,Redis 為每次掃描添加了上限時間,默認是 25ms。

  如果客戶端將超時時間設置的比較短,比如 10ms,那么就會出現大量的鏈接因為超時而關閉,業務端就會出現很多異常。而且這時你還無法從 Redis 的 slowlog 中看到慢查詢記錄,因為慢查詢指的是邏輯處理過程慢,不包含等待時間。

  如果在同一時間出現大面積 key 過期,Redis 循環多次掃描過期詞典,直到過期的 key 比例小於 1/4。這會導致卡頓,而且在高並發的情況下,可能會導致緩存雪崩。

為什么 Redis 為每次掃描添的上限時間是 25ms,還會出現上面的情況?

  因為 Redis 是單線程,每個請求處理都需要排隊,而且由於 Redis 每次掃描都是 25ms,也就是每個請求最多 25ms,100 個請求就是 2500ms。

  如果有大批量的 key 過期,要給過期時間設置一個隨機范圍,而不宜全部在同一時間過期,分散過期處理的壓力。

從庫的過期策略

  從庫不會進行過期掃描,從庫對過期的處理是被動的。主庫在 key 到期時,會在 AOF 文件里增加一條 del 指令,同步到所有的從庫,從庫通過執行這條 del 指令來刪除過期的 key。

  因為指令同步是異步進行的,所以主庫過期的 key 的 del 指令沒有及時同步到從庫的話,會出現主從數據的不一致,主庫沒有的數據在從庫里還存在。

懶惰刪除策略

 Redis 為什么要懶惰刪除(lazy free)?

  刪除指令 del 會直接釋放對象的內存,大部分情況下,這個指令非常快,沒有明顯延遲。不過如果刪除的 key 是一個非常大的對象,比如一個包含了千萬元素的 hash,又或者在使用 FLUSHDB 和 FLUSHALL 刪除包含大量鍵的數據庫時,那么刪除操作就會導致單線程卡頓。

  redis 4.0 引入了 lazyfree 的機制,它可以將刪除鍵或數據庫的操作放在后台線程里執行, 從而盡可能地避免服務器阻塞。

unlink

  unlink 指令,它能對刪除操作進行懶處理,丟給后台線程來異步回收內存。

> unlink key
OK

flush

  flushdb 和 flushall 指令,用來清空數據庫,這也是極其緩慢的操作。Redis 4.0 同樣給這兩個指令也帶來了異步化,在指令后面增加 async 參數就可以將整棵大樹連根拔起,扔給后台線程慢慢焚燒。

> flushall async
OK

內存淘汰通過近似 LRU來實現

  在解釋近似LRU之前,先來簡單了解一下LRU。

  當 Redis 的內存占用超過我們設置的 maxmemory 時,會把長時間沒有使用的key清理掉。按照 LRU算法,我們需要對所有key(也可以設置成只淘汰有過期時間的key)按照空閑時間進行排序,然后淘汰掉空閑時間最大的那部分數據,使得Redis的內存占用降到一個合理的值。

  LRU算法的缺點:

    1. 我們需要維護一個全部(或只有過期時間)key的列表,還要按照最近使用時間排序。這會消耗大量內存

    2. 每次操作 key 時更新對應維護列表的排序也會占用額外的CPU資源。

  對於Redis這樣對性能要求很高的系統來說是不被允許的。

  因此,Redis采用了一種 近似LRU 的算法。當 Redis 接收到新的寫入命令,而內存又不夠時,就會觸發 近似LRU 算法來強制清理一些key。

  具體清理的步驟是:

    1. Redis會對 key 進行采樣,通常是取5個,然后會把過期的key放到我們上面說的“過期池”中

    2. 過期池中的 key 是按照空閑時間來排序的,Redis 會優先清理掉空閑時間最長的 key,直到內存小於 maxmemory。

  其中 Redis首先是采樣了一部分key,這里采樣數量 maxmemory_samples 通常是5,我們也可以自己設置,采樣數量越大,結果就越接近LRU算法的結果,帶來的影響是:性能隨之變差。

清理策略

  最后我們來看一下Redis支持的幾種清理策略

    1. noeviction:不會繼續處理寫請求(DEL可以繼續處理)。

    2. allkeys-lru:對所有key的近似LRU

    3. volatile-lru:使用近似LRU算法淘汰設置了過期時間的key

    4. allkeys-random:從所有key中隨機淘汰一些key

    5. volatile-random:對所有設置了過期時間的key隨機淘汰

    6. volatile-ttl:淘汰有效期最短的一部分key

  Redis4.0 開始支持了 LFU 策略,和 LRU 類似,它分為兩種:

    7. volatile-lfu:使用LFU算法淘汰設置了過期時間的key

    8. allkeys-lfu:從全部key中進行淘汰,使用LFU

最后

  針對文章開始提到的問題,最好的解決辦法是將使用內存量較大的業務 和 用戶賬號服務 使用的 redis 隔離開,這樣就單個用戶賬號正常情況下是不會發生以上類似的問題了。


  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM