前言 我們都知道redis是常駐在記憶體當中的,因此他的效率比MySQL要快很多很多。但又引發了另外一個問題,記憶體從本質上講,它是昂貴的,不能用於大量的長時間的存儲,他是“不安全不穩定的“,並且有可能存在記憶體泄露,不能與磁碟相比。 那麼如果解決這種問題呢?因此我們使用redis的時候,強制的應該給每個 ...
前言
我們都知道redis是常駐在記憶體當中的,因此他的效率比MySQL要快很多很多。但又引發了另外一個問題,記憶體從本質上講,它是昂貴的,不能用於大量的長時間的存儲,他是“不安全不穩定的“,並且有可能存在記憶體泄露,不能與磁碟相比。
那麼如果解決這種問題呢?因此我們使用redis的時候,強制的應該給每個Key加上過期時間。我們來看看redis對過期的Key是怎麼處理的。
過期鍵的判定
第一個問題,redis如何知道他是一個過期鍵呢?又該如何判定他過期了呢?
在資料庫中, 所有鍵的過期時間都被保存在 redisDb
結構的 expires
字典里:
1 typedef struct redisDb { 2 3 // ... 4 5 dict *expires; 6 7 // ... 8 9 } redisDb;
expires
字典的鍵是一個指向 dict
字典(鍵空間)里某個鍵的指針, 而字典的值則是鍵所指向的資料庫鍵的到期時間, 這個值以 long long
類型表示。
下圖展示了一個含有三個鍵的資料庫,其中 number
和 book
兩個鍵帶有過期時間
我們可以看到number和book是有一個過期時間的,他是long long類型。實則他是一個unix的時間戳,因此判斷他是否過期就十分的簡單了。
通過 expires
字典, 可以用以下步驟檢查某個鍵是否過期:
- 檢查鍵是否存在於
expires
字典:如果存在,那麼取出鍵的過期時間; - 檢查當前 UNIX 時間戳是否大於鍵的過期時間:如果是的話,那麼鍵已經過期;否則,鍵未過期。
可以用偽代碼來描述這一過程:
1 def is_expired(key): 2 3 # 取出鍵的過期時間 4 key_expire_time = expires.get(key) 5 6 # 如果過期時間不為空,並且當前時間戳大於過期時間,那麼鍵已經過期 7 if expire_time is not None and current_timestamp() > key_expire_time: 8 return True 9 10 # 否則,鍵未過期或沒有設置過期時間 11 return False
過期鍵的清除
當我們知道這個鍵過期了,我們該如何清除呢?基本上有以下三種策略:
- 定時刪除:在設置鍵的過期時間時,創建一個定時事件,當過期時間到達時,由事件處理器自動執行鍵的刪除操作。
- 惰性刪除:放任鍵過期不管,但是在每次從 dict 字典中取出鍵值時,要檢查鍵是否過期,如果過期的話,就刪除它,並返回空;如果沒過期,就返回鍵值。
- 定期刪除:每隔一段時間,對 expires 字典進行檢查,刪除裡面的過期鍵。
定時刪除
定時刪除策略對記憶體是最友好的: 因為它保證過期鍵會在第一時間被刪除, 過期鍵所消耗的記憶體會立即被釋放。
這種策略的缺點是, 它對 CPU 時間是最不友好的: 因為刪除操作可能會占用大量的 CPU 時間 —— 在記憶體不緊張、但是 CPU 時間非常緊張的時候 (比如說,進行交集計算或排序的時候), 將 CPU 時間花在刪除那些和當前任務無關的過期鍵上, 這種做法毫無疑問會是低效的。
除此之外, 目前 Redis 事件處理器對時間事件的實現方式 —— 無序鏈表, 查找一個時間複雜度為 O(N) —— 並不適合用來處理大量時間事件。
惰性刪除
惰性刪除對 CPU 時間來說是最友好的: 它只會在取出鍵時進行檢查, 這可以保證刪除操作只會在非做不可的情況下進行 —— 並且刪除的目標僅限於當前處理的鍵, 這個策略不會在刪除其他無關的過期鍵上花費任何 CPU 時間。
惰性刪除的缺點是, 它對記憶體是最不友好的: 如果一個鍵已經過期, 而這個鍵又仍然保留在資料庫中, 那麼 dict
字典和 expires
字典都需要繼續保存這個鍵的信息, 只要這個過期鍵不被刪除, 它占用的記憶體就不會被釋放。
在使用惰性刪除策略時, 如果資料庫中有非常多的過期鍵, 但這些過期鍵又正好沒有被訪問的話, 那麼它們就永遠也不會被刪除(除非用戶手動執行), 這對於性能非常依賴於記憶體大小的 Redis 來說, 肯定不是一個好消息。
舉個例子, 對於一些按時間點來更新的數據, 比如日誌(log), 在某個時間點之後, 對它們的訪問就會大大減少, 如果大量的這些過期數據積壓在資料庫裡面, 用戶以為它們已經過期了(已經被刪除了), 但實際上這些鍵卻沒有真正的被刪除(記憶體也沒有被釋放), 那結果肯定是非常糟糕。
定期刪除
從上面對定時刪除和惰性刪除的討論來看, 這兩種刪除方式在單一使用時都有明顯的缺陷: 定時刪除占用太多 CPU 時間, 惰性刪除浪費太多記憶體。
定期刪除是這兩種策略的一種折中:
- 它每隔一段時間執行一次刪除操作,並通過限制刪除操作執行的時長和頻率,籍此來減少刪除操作對 CPU 時間的影響。
- 另一方面,通過定期刪除過期鍵,它有效地減少了因惰性刪除而帶來的記憶體浪費。
因此最終redis使用的過期鍵刪除策略是惰性刪除加上定期刪除, 這兩個策略相互配合,可以很好地在合理利用 CPU 時間和節約記憶體空間之間取得平衡。
因此redis大致流程如下:獲取key之前,會檢查key是否過期,如過期,直接刪除,返回null。
並且會定期的隨機的檢查大約25%的key是否過期,如果超過一定比例的key被過期。那麼繼續迴圈,直至低於這個數值。
這個定期的時間,以及數值都可以在conf文件裡面配置。