一、Redis 中的過期鍵刪除策略有哪些?
采用了 惰性刪除 和 定期刪除 兩種策略處理過期鍵:
1. 惰性刪除(Lazy Deletion)
- 機制:只有在訪問 key 時才檢查是否過期,如果已過期則立刻刪除。
- 優點:對 CPU 資源最友好,只在必要時才處理。
- 缺點:若 key 過期但始終未被訪問,則不會釋放內存,容易造成空間浪費。
Redis 實現方式:每次訪問前調用
expireIfNeeded()
判斷是否過期,若已過期,Redis 4.0 起還支持lazyfree_lazy_expire
控制是否異步刪除。
2. 定期刪除(Periodic Deletion)
-
機制:Redis 每秒默認執行 10 次定期刪除,每次從過期字典中隨機抽取 20 個 key 檢查。
-
流程:
- 抽 20 個 key,刪除其中過期的;
- 若這 20 個里有超過 25%(即 5 個)過期,則繼續循環;
- 每次循環有時間上限,默認 不超過 25ms,防止阻塞主線程。
-
優點:相較惰性刪除,能主動清理部分過期 key,提升內存利用率;
-
缺點:受限于檢查頻率和時間,可能存在部分過期 key 無法及時清理的問題。
二、為什么不用定時刪除?
定時刪除(即為每個 key 單獨設置定時器,到期立即刪除)并未被 Redis 實際采用。
- 優點:能最及時地釋放內存;
- 缺點:大量 key 會引入大量定時任務,極大增加 CPU 開銷,尤其在過期 key 較多時,會嚴重影響 Redis 的性能和響應時間。
三、總結:
Redis 的過期鍵清除策略采用了 惰性刪除 + 定期刪除的組合策略,在保證較低 CPU 開銷的同時,盡可能釋放內存空間。
- 惰性刪除 是只在訪問key的時候檢查是否過期;
- 定期刪除 定時進行部分key的過期檢查;
- Redis 放棄了定時刪除,是因為對每個key單獨計時過期刪除,會大大增加cpu負擔
一、什么是 LRU 算法?
LRU,全稱 Least Recently Used,即「最近最少使用」策略,用于在內存滿時淘汰最久未被訪問的鍵。
傳統 LRU 的實現方式:
- 通常使用雙向鏈表 + 哈希表實現;
- 每次訪問某個 key,就將其移動到鏈表頭部;
- 淘汰時,從鏈表尾部刪除最舊訪問的 key。
傳統 LRU 的問題:
- 維護鏈表需要額外的內存;
- 每次訪問都要更新鏈表結構,頻繁的鏈表操作會帶來性能瓶頸。
二、Redis 如何實現 LRU?
Redis 沒有使用傳統 LRU,而是實現了近似 LRU,采用“隨機采樣 + 時間戳”方式。
實現細節:
- Redis 為每個鍵的對象結構添加了
lru
字段,記錄其最近訪問時間(非絕對時間,是一個全局邏輯時鐘)。 - Redis 每次進行內存淘汰時,并不會遍歷所有鍵,而是從所有鍵中隨機采樣若干個(默認 5 個),然后挑出其中訪問最久遠的鍵淘汰。
- 這個采樣數量可通過參數
maxmemory-samples
配置。
優點:
- 無需維護全局鏈表,節省空間;
- 不需要每次訪問都做鏈表操作,大幅提高性能;
- 適用于高性能場景的內存淘汰。
缺點:
- 由于是近似 LRU,并非全局最久未使用的鍵一定會被淘汰,存在一定誤差;
- 可能出現緩存污染問題:某些鍵被短時間大量訪問后遺留在內存中,卻很少被再次使用。
三、什么是 LFU 算法?(Redis 4.0+ 引入)
LFU,全稱 Least Frequently Used,即「最不常訪問」策略。用于淘汰訪問次數最少的鍵,更能避免短期熱點帶來的緩存污染。
Redis 中 LFU 的實現方式:
- Redis 在每個鍵中增加了一個 訪問頻率計數器;
- 每次訪問某個 key,會更新其計數;
- 內存淘汰時,采用類似于近似 LRU 的方式 —— 隨機采樣幾個 key,選擇訪問頻率最少的淘汰。
優點:
- 解決了 LRU 中“只訪問一次卻常駐內存”的問題;
- 更適合處理熱點數據與長尾數據共存的緩存場景。
特性 | Redis 中的 LRU | Redis 中的 LFU |
---|---|---|
全稱 | Least Recently Used | Least Frequently Used |
淘汰依據 | 最近一次訪問時間 | 被訪問的頻率 |
實現方式 | 每個 key 保存時間戳 + 隨機采樣 | 每個 key 保存訪問次數 + 隨機采樣 |
淘汰方式 | 隨機采樣若干 key,淘汰其中最久未訪問的 | 隨機采樣若干 key,淘汰其中訪問最少的 |
優點 | 高效節省內存 | 能減少緩存污染 |
典型場景 | 訪問時間分布均勻 | 存在熱點和冷數據 |
一、四種緩存一致性方案對比表格
編號 | 操作順序 | 是否推薦 | 問題說明 |
---|---|---|---|
1 | 先更新數據庫 → 后更新緩存 | 否 | 并發更新可能將舊值寫入緩存,導致臟數據 |
2 | 先更新緩存 → 后更新數據庫 | 否 | 緩存更新成功但數據庫失敗,導致數據不一致 |
3 | 先刪除緩存 → 后更新數據庫 | 是 | 可避免臟數據寫入緩存,但可能存在緩存空窗期 |
4 | 先更新數據庫 → 后刪除緩存 | 是 | 若刪除緩存失敗,可能導致臟緩存,可使用“延遲雙刪”策略優化 |
二、推薦方案三:先刪除緩存 → 后更新數據庫
時間線場景:
T1:請求A準備寫操作,刪除緩存
T2:請求B查詢,發現緩存 miss(被 A 刪除了)
T3:請求B 回源數據庫,讀取舊數據(因為 A 還沒更新 DB)
T4:請求B 將舊數據寫入緩存
T5:請求A 將新數據寫入數據庫
問題解決思路:
- 刪除緩存后寫數據庫,防止更新時緩存被覆蓋
- 并發下的讀寫覆蓋問題,需要加寫操作保護措施:例如分布式鎖、隊列、串行化寫
- 可使用分布式鎖優化并發場景
Go 語言實現(含分布式鎖):
依賴(Redlock 實現分布式鎖):
使用 github.com/bsm/redislock:
go get github.com/bsm/redislock
完整代碼:
package mainimport ("context""fmt""log""time""github.com/bsm/redislock""github.com/redis/go-redis/v9"
)var (ctx = context.Background()rdb = redis.NewClient(&redis.Options{Addr: "localhost:6379"})locker = redislock.New(rdb)
)type UserProfile struct {ID int64Name string
}// 模擬數據庫操作
func updateUserInDB(userID int64, newData *UserProfile) error {fmt.Printf("DB Updated: userID=%d, data=%+v\n", userID, newData)return nil
}func UpdateUserWithLock(userID int64, newData *UserProfile) error {lockKey := fmt.Sprintf("lock:user:%d", userID)lock, err := locker.Obtain(ctx, lockKey, 5*time.Second, nil)if err != nil {return fmt.Errorf("failed to obtain lock: %w", err)}defer lock.Release(ctx)// 刪除緩存cacheKey := fmt.Sprintf("cache:user:%d", userID)if err := rdb.Del(ctx, cacheKey).Err(); err != nil {log.Printf("failed to delete cache: %v", err)}// 更新數據庫if err := updateUserInDB(userID, newData); err != nil {return fmt.Errorf("failed to update db: %w", err)}return nil
}
三、推薦方案四:先更新數據庫 → 后刪除緩存(延遲雙刪)
時間線場景:
T1:請求A準備更新,先寫數據庫
T2:請求B 查詢緩存,發現存在舊值
T3:請求A 刪除緩存(第一次)
T4:請求B 讀取舊緩存數據并返回
T5:請求B 之后把舊數據重新寫入緩存(或未寫入)
T6:請求A 延遲100ms后再次刪除緩存
問題解決思路:
- 刪除緩存失敗會導致臟緩存
- 可采用“延遲雙刪策略”:更新完數據庫后立即刪一次緩存,并延遲一段時間再刪一次
Go 語言實現:
package mainimport ("context""fmt""log""time""github.com/redis/go-redis/v9"
)var (ctx = context.Background()rdb = redis.NewClient(&redis.Options{Addr: "localhost:6379"})
)type UserProfile struct {ID int64Name string
}// 模擬數據庫操作
func updateUserInDB(userID int64, newData *UserProfile) error {fmt.Printf("DB Updated: userID=%d, data=%+v\n", userID, newData)return nil
}func UpdateUserWithDelayDelete(userID int64, newData *UserProfile) error {// 更新數據庫if err := updateUserInDB(userID, newData); err != nil {return fmt.Errorf("failed to update db: %w", err)}// 刪除緩存cacheKey := fmt.Sprintf("cache:user:%d", userID)if err := rdb.Del(ctx, cacheKey).Err(); err != nil {log.Printf("delete cache failed: %v", err)}// 延遲雙刪(異步)go func() {time.Sleep(100 * time.Millisecond)if err := rdb.Del(ctx, cacheKey).Err(); err != nil {log.Printf("delayed delete failed: %v", err)}}()return nil
}
四、總結
場景 | 推薦方案 | 優點 | 需注意問題與優化點 |
---|---|---|---|
一般中小型項目 | 方案三 | 實現簡單、延遲可控 | 可用 Redis 鎖優化 |
高頻寫/對一致性敏感 | 方案四 | 數據庫操作主導、雙刪更穩健 | 需延遲雙刪、監控緩存刪除是否成功 |
高并發寫入場景 | 方案三+鎖 | 防止并發緩存回寫 | Redlock 實現需健壯 |
https://github.com/0voice