Redis 的過期與淘汰:TTL、惰性刪除與 maxmemory 政策
· tech
📑 目錄
把 Redis 當快取,遲早會碰到兩個問題:設了 TTL 的 key 到期後怎麼被清掉? 以及 記憶體滿了會怎樣? 這兩件事常被混為一談,其實是兩回事——前者是過期(expiration):「這個 key 自己時間到了、該走了」;後者是淘汰(eviction):「地方不夠了,得請人走」。分清這兩者,是把 Redis 當快取用好的基礎。
過期:key 到期了,怎麼被清掉
你以為一個 key 的 TTL 一到,Redis 就立刻把它刪掉、釋放記憶體?其實不是。 Redis 用兩種機制搭配著清過期 key,而它們都不保證「即時」:
這裡的反直覺點值得記住:一個 key 的 TTL 到了,不代表它立刻從記憶體消失。 如果沒人去存取它(惰性刪除沒觸發)、背景抽樣也還沒抽到它,它就會先躺在記憶體裡——只是你 GET 它會拿到 nil(邏輯上已過期),但實體記憶體還沒釋放。順帶一提,replica 不會自己刪過期 key,它等 master 過期後送來的 DEL——這是為了保證主從資料一致,由 master 當過期的權威。
淘汰:記憶體滿了,踢誰走
過期是 key 自己該走;淘汰則是另一回事——當記憶體用量撞到 maxmemory 上限,Redis 得主動踢掉一些 key 來騰出空間。踢誰?由 eviction policy 決定,總共 8 種(外加一個「不踢」):
選政策的關鍵,是先問一句:這台 Redis 是純快取,還是也存了不能丟的東西?
- 純快取(所有 key 丟了都能從後面資料庫重建):用
allkeys-lru或allkeys-lfu,讓 Redis 自由踢掉最沒用的,把記憶體留給熱資料。 - 混著存了重要資料:用
volatile-*——只踢「有設 TTL 的可丟資料」,保護那些沒 TTL 的重要 key;或用noeviction+ 自己控管容量。 - 千萬別在「當快取用」的場景留著預設的
noeviction——記憶體一滿,所有寫入都會開始報錯,而你可能以為 Redis 只是「舊資料被自動清掉」而已。
redis-cli:TTL 與 maxmemory 操作
過期與淘汰各有一組命令,分別對應「自己該走」與「地方不夠踢誰」:
# 過期(TTL)
SET session:1 "..." EX 3600 # 設值同時給 1 小時 TTL(最常用)
EXPIRE user:1 60; TTL user:1 # 事後補 TTL / 查剩幾秒(-1=永久,-2=不存在)
PERSIST user:1 # 拔掉 TTL,變回永久
# 淘汰(記憶體上限與政策)
CONFIG SET maxmemory 2gb
CONFIG SET maxmemory-policy allkeys-lru # 8 種政策之一(見上面矩陣)
INFO stats # 看 evicted_keys:不為 0 且一直漲 = 該擴或調政策
OBJECT FREQ hotkey # LFU 政策下,看某 key 的存取頻率
盯住兩個數字就抓住重點:TTL 告訴你某把 key 還能活多久,INFO stats 裡的 evicted_keys 一路上漲則是「記憶體撞頂、開始踢人」的第一警報——那是該加記憶體、還是該換個淘汰政策的分水嶺。
反思
過期 vs 淘汰:分清「自己該走」和「地方不夠請人走」
剛用 Redis 時,我一直把過期和淘汰當同一件事,結果配 maxmemory 時完全搞錯方向。後來想通:過期是 key 的個人時程(TTL 到了自己該走),淘汰是系統的空間壓力(記憶體滿了得請人走)——兩者獨立,一個沒設 TTL 的 key 永遠不會「過期」,但照樣可能被「淘汰」。這個區分看似細節,卻直接決定你怎麼設計:哪些 key 該給 TTL(讓它自然過期)、記憶體上限設多少、撞頂時該踢誰。把「時間到了」和「空間不夠」這兩種淘汰壓力分開想,很多 Redis 的容量問題就清晰了。
近似 LRU:又一個「用一點精度換記憶體」
Redis 的 LRU 不是「精確地踢最久沒用的那個」——維護一條完整的 LRU 鏈太耗記憶體。它改用抽樣近似:隨機抽幾個 key,踢掉其中最久沒用的。這又是 Redis 一貫的哲學,跟 HyperLogLog 用 12KB 估上億基數、fork COW 只複製被改的頁,是同一種思路——在「完全精確」與「省資源」之間,聰明地選擇不那麼精確。而且它通常夠好:cache 淘汰本來就不需要「數學上最優」,近似的 LRU 對命中率的影響微乎其微,卻省下維護精確結構的大量開銷。我越來越覺得,分辨「哪裡需要精確、哪裡近似就好」,是資深工程師和新手最明顯的差距之一。
eviction policy 選錯,是「平常沒事、滿了才爆」的隱形炸彈
淘汰政策是那種「你不主動想、它就用預設咬你」的設定。預設的 noeviction 對「當快取」的場景是災難——平常記憶體沒滿時風平浪靜,一旦資料長到撞頂,不是自動清舊資料,而是所有寫入開始報錯,整個服務跟著掛。我看過不只一次這種事故:大家以為 Redis 會「自己清掉舊的」,結果它其實在拒絕新的。這件事教我一個更普遍的教訓:任何有「上限」的資源,都要主動想清楚「撞到上限時會發生什麼」——是自動回收、拒絕服務、還是直接崩潰?這個「邊界行為」平常看不到,卻往往是半夜事故的主角。上線前把每個上限的撞頂行為都想一遍,是很值得的偏執。