能爲有價值的工做而努力,就是生活給予你最珍貴的禮物。redis
在平常開發中,咱們使用 Redis 存儲 key 時一般會設置一個過時時間,可是 Redis 是怎麼刪除過時的 key,並且 Redis 是單線程的,刪除 key 會不會形成阻塞。要搞清楚這些,就要了解 Redis 的過時策略和內存淘汰機制。 算法
Redis採用的是按期刪除 + 懶惰刪除策略。數據庫
Redis 會將每一個設置了過時時間的 key 放入到一個獨立的字典中,默認每 100ms 進行一次過時掃描:緩存
爲什不掃描全部的 key?安全
Redis 是單線程,所有掃描豈不是卡死了。並且爲了防止每次掃描過時的 key 比例都超過 1/4,致使不停循環卡死線程,Redis 爲每次掃描添加了上限時間,默認是 25ms。服務器
若是客戶端將超時時間設置的比較短,好比 10ms,那麼就會出現大量的連接由於超時而關閉,業務端就會出現不少異常。並且這時你還沒法從 Redis 的 slowlog 中看到慢查詢記錄,由於慢查詢指的是邏輯處理過程慢,不包含等待時間。併發
若是在同一時間出現大面積 key 過時,Redis 循環屢次掃描過時詞典,直到過時的 key 比例小於 1/4。這會致使卡頓,並且在高併發的狀況下,可能會致使緩存雪崩。dom
爲何 Redis 爲每次掃描添的上限時間是 25ms,還會出現上面的狀況?異步
由於 Redis 是單線程,每一個請求處理都須要排隊,並且因爲 Redis 每次掃描都是 25ms,也就是每一個請求最多 25ms,100 個請求就是 2500ms。async
若是有大批量的 key 過時,要給過時時間設置一個隨機範圍,而不宜所有在同一時間過時,分散過時處理的壓力。
從庫不會進行過時掃描,從庫對過時的處理是被動的。主庫在 key 到期時,會在 AOF 文件裏增長一條 del 指令,同步到全部的從庫,從庫經過執行這條 del 指令來刪除過時的 key。
由於指令同步是異步進行的,因此主庫過時的 key 的 del 指令沒有及時同步到從庫的話,會出現主從數據的不一致,主庫沒有的數據在從庫裏還存在。
Redis 爲何要懶惰刪除(lazy free)?
刪除指令 del 會直接釋放對象的內存,大部分狀況下,這個指令很是快,沒有明顯延遲。不過若是刪除的 key 是一個很是大的對象,好比一個包含了千萬元素的 hash,又或者在使用 FLUSHDB 和 FLUSHALL 刪除包含大量鍵的數據庫時,那麼刪除操做就會致使單線程卡頓。
redis 4.0 引入了 lazyfree 的機制,它能夠將刪除鍵或數據庫的操做放在後臺線程裏執行, 從而儘量地避免服務器阻塞。
unlink 指令,它能對刪除操做進行懶處理,丟給後臺線程來異步回收內存。
> unlink key OK
flushdb 和 flushall 指令,用來清空數據庫,這也是極其緩慢的操做。Redis 4.0 一樣給這兩個指令也帶來了異步化,在指令後面增長 async 參數就能夠將整棵大樹連根拔起,扔給後臺線程慢慢焚燒。
> flushall async OK
主線程將對象的引用從「大樹」中摘除後,會將這個 key 的內存回收操做包裝成一個任務,塞進異步任務隊列,後臺線程會從這個異步隊列中取任務。任務隊列被主線程和異步線程同時操做,因此必須是一個線程安全的隊列。
不是全部的 unlink 操做都會延後處理,若是對應 key 所佔用的內存很小,延後處理就沒有必要了,這時候 Redis 會將對應的 key 內存當即回收,跟 del 指令同樣。
Redis 回收內存除了 del 指令和 flush 以外,還會存在於在 key 的過時、LRU 淘汰、rename 指令以及從庫全量同步時接受完 rdb 文件後會當即進行的 flush 操做。
Redis4.0 爲這些刪除點也帶來了異步刪除機制,打開這些點須要額外的配置選項。
Redis 的內存佔用會愈來愈高。Redis 爲了限制最大使用內存,提供了 redis.conf 中的
配置參數 maxmemory。當內存超出 maxmemory,Redis 提供了幾種內存淘汰機制讓用戶選擇,配置 maxmemory-policy:
實現 LRU 算法除了須要 key/value 字典外,還須要附加一個鏈表,鏈表中的元素按照必定的順序進行排列。當空間滿的時候,會踢掉鏈表尾部的元素。當字典的某個元素被訪問時,它在鏈表中的位置會被移動到表頭。因此鏈表的元素排列順序就是元素最近被訪問的時間順序。
使用 Python 的 OrderedDict(雙向鏈表 + 字典) 來實現一個簡單的 LRU 算法:
from collections import OrderedDict class LRUDict(OrderedDict): def __init__(self, capacity): self.capacity = capacity self.items = OrderedDict() def __setitem__(self, key, value): old_value = self.items.get(key) if old_value is not None: self.items.pop(key) self.items[key] = value elif len(self.items) < self.capacity: self.items[key] = value else: self.items.popitem(last=True) self.items[key] = value def __getitem__(self, key): value = self.items.get(key) if value is not None: self.items.pop(key) self.items[key] = value return value def __repr__(self): return repr(self.items) d = LRUDict(10) for i in range(15): d[i] = i print d
Redis 使用的並非徹底 LRU 算法。不使用 LRU 算法,是爲了節省內存,Redis 採用的是隨機LRU算法,Redis 爲每個 key 增長了一個24 bit的字段,用來記錄這個 key 最後一次被訪問的時間戳。
注意 Redis 的 LRU 淘汰策略是懶惰處理,也就是不會主動執行淘汰策略,當 Redis 執行寫操做時,發現內存超出 maxmemory,就會執行 LRU 淘汰算法。這個算法就是隨機採樣出5(默認值)個 key,而後移除最舊的 key,若是移除後內存仍是超出 maxmemory,那就繼續隨機採樣淘汰,直到內存低於 maxmemory 爲止。
如何採樣就是看 maxmemory-policy 的配置,若是是 allkeys 就是從全部的 key 字典中隨機,若是是 volatile 就從帶過時時間的 key 字典中隨機。每次採樣多少個 key 看的是 maxmemory_samples 的配置,默認爲 5。
Redis 4.0 裏引入了一個新的淘汰策略 —— LFU(Least Frequently Used) 模式,做者認爲它比 LRU 更加優秀。
LFU 表示按最近的訪問頻率進行淘汰,它比 LRU 更加精準地表示了一個 key 被訪問的熱度。
若是一個 key 長時間不被訪問,只是剛剛偶然被用戶訪問了一下,那麼在使用 LRU 算法下它是不容易被淘汰的,由於 LRU 算法認爲當前這個 key 是很熱的。而 LFU 是須要追蹤最近一段時間的訪問頻率,若是某個 key 只是偶然被訪問一次是不足以變得很熱的,它須要在近期一段時間內被訪問不少次纔有機會被認爲很熱。
Redis 對象的熱度
Redis 的全部對象結構頭中都有一個 24bit 的字段,這個字段用來記錄對象的熱度。
// redis 的對象頭 typedef struct redisObject { unsigned type:4; // 對象類型如 zset/set/hash 等等 unsigned encoding:4; // 對象編碼如 ziplist/intset/skiplist 等等 unsigned lru:24; // 對象的「熱度」 int refcount; // 引用計數 void *ptr; // 對象的 body } robj;
LRU 模式
在 LRU 模式下,lru 字段存儲的是 Redis 時鐘 server.lruclock,Redis 時鐘是一個 24bit 的整數,默認是 Unix 時間戳對 2^24 取模的結果,大約 97 天清零一次。當某個 key 被訪問一次,它的對象頭的 lru 字段值就會被更新爲 server.lruclock。
LFU 模式
在 LFU 模式下,lru 字段 24 個 bit 用來存儲兩個值,分別是 ldt(last decrement time) 和 logc(logistic counter)。
logc 是 8 個 bit,用來存儲訪問頻次,由於 8 個 bit 能表示的最大整數值爲 255,存儲頻次確定遠遠不夠,因此這 8 個 bit 存儲的是頻次的對數值,而且這個值還會隨時間衰減。若是它的值比較小,那麼就很容易被回收。爲了確保新建立的對象不被回收,新對象的這 8 個 bit 會初始化爲一個大於零的值,默認是 LFU_INIT_VAL=5。
ldt 是 16 個位,用來存儲上一次 logc 的更新時間,由於只有 16 位,因此精度不可能很高。它取的是分鐘時間戳對 2^16 進行取模,大約每隔 45 天就會折返。
同 LRU 模式同樣,咱們也可使用這個邏輯計算出對象的空閒時間,只不過精度是分鐘級別的。圖中的 server.unixtime 是當前 redis 記錄的系統時間戳,和 server.lruclock 同樣,它也是每毫秒更新一次。