Redis緩存問題與緩存更新機制詳解
一、緩存問題
1.1 緩存穿透
1.1.1 問題來源
緩存穿透是指緩存和數(shù)據(jù)庫中都沒有的數(shù)據(jù),而用戶不斷發(fā)起請求。由于緩存是不命中時被動寫的,并且出于容錯考慮,如果從存儲層查不到數(shù)據(jù)則不寫入緩存,這將導致這個不存在的數(shù)據(jù)每次請求都要到存儲層去查詢,失去了緩存的意義。在流量大時,可能DB就掛掉了,要是有人利用不存在的key頻繁攻擊我們的應用,這就是漏洞。
1.1.2 解決方案
1.1.2.1 緩存空對象
- 從緩存取不到的數(shù)據(jù),在數(shù)據(jù)庫中也沒有取到,這時也可以將key-value對寫為key-null,緩存有效時間可以設置短點,如30秒(設置太長會導致正常情況也沒法使用)。
- 這樣可以防止攻擊用戶反復用同一個id暴力攻擊。
1.1.2.2 使用布隆過濾器
- 類似于一個hash set,用于快速判某個元素是否存在于集合中,其典型的應用場景就是快速判斷一個key是否存在于某容器,不存在就直接返回。
- 布隆過濾器的關(guān)鍵就在于hash算法和容器大小。
1.2 緩存擊穿
1.2.1 問題來源
緩存擊穿是指緩存某些熱點數(shù)據(jù)失效(一般是緩存時間到期),這時由于并發(fā)用戶特別多,同時讀緩存沒讀到數(shù)據(jù),又同時去數(shù)據(jù)庫去取數(shù)據(jù),引起數(shù)據(jù)庫壓力瞬間增大,造成過大壓力。
1.2.2 解決方案
1.2.2.1 設置熱點數(shù)據(jù)永遠不過期
可以在刷緩存時,設置熱點數(shù)據(jù)不過期。
1.2.2.2 新增后臺定時更新緩存線程(邏輯不過期)
后臺新增一個緩存更新線程,緩存快要過期前刷新緩存時間,防止緩存失效。
1.2.2.3 使用分布式互斥鎖
可以使用Redis提供的分布式互斥鎖,保證只有一個請求查詢數(shù)據(jù)庫和更新緩存,其他請求阻塞等待緩存更新完成后在訪問緩存。
1.2.2.4 接口限流與熔斷,降級
重要的接口一定要做好限流策略,防止用戶惡意刷接口,同時要降級準備,當接口中的某些服務不可用時候,進行熔斷,失敗快速返回機制。
1.3 緩存雪崩
1.3.1 問題來源
緩存雪崩是指Redis緩存不能正常提供服務了(阻塞、服務宕機、大面積緩存失效等造成),導致所有請求都落到了數(shù)據(jù)庫上,增加了數(shù)據(jù)庫壓力或者導致數(shù)據(jù)庫宕機。
1.3.2 解決方案
1.3.2.1 緩存過期時間隨機
緩存數(shù)據(jù)的過期時間設置隨機,防止同一時間大量數(shù)據(jù)過期現(xiàn)象發(fā)生。
1.3.2.2 分布式部署
采用分布式部署方式部署緩存,避免緩存服務單節(jié)點,同時將熱點數(shù)據(jù)均勻分布在不同的緩存數(shù)據(jù)庫中。
1.3.2.3 設置熱點數(shù)據(jù)永遠不過期
可以在刷緩存時,設置熱點數(shù)據(jù)不過期。
1.3.2.4 接口限流與熔斷,降級
重要的接口一定要做好限流策略,防止用戶惡意刷接口,同時要降級準備,當接口中的某些服務不可用時候,進行熔斷,失敗快速返回機制。
二、緩存更新機制
2.1 緩存更新策略分類
內(nèi)存淘汰 | 超時剔除 | 主動更新 | |
說明 | 重要的接口一定要做好限流策略,防止用戶惡意刷接口,同時要降級準備,當接口中的某些服務不可用時候,進行熔斷,失敗快速返回機制。 | 給緩存數(shù)據(jù)添加TTL時間,到期后自動刪除緩存,下次查詢時更新緩存 | 編寫業(yè)務邏輯,在修改數(shù)據(jù)的同時,更新緩存 |
一致性 | 差 | 一般 | 好 |
維護成本 | 無 | 低 | 高 |
2.2 內(nèi)存淘汰機制
2.2.1 noeviction
不淘汰,這是默認的淘汰策略;
當內(nèi)存達到限制后,寫請求(set)會返回錯誤,讀請求(get)和刪除請求(del)可以繼續(xù)進行
2.2.2 volatile-lru
內(nèi)存不足時,在設置了過期時間的key中,優(yōu)先刪除最近最少使用的key
2.2.3 volatile-lfu
內(nèi)存不足時,在設置了過期時間的key中,優(yōu)先刪除使用頻率最少的key
2.2.4 volatile-ttl
內(nèi)存不足時,在設置了過期時間的key中,優(yōu)先刪除存活剩余時間最少的key
2.2.5 volatile-random
內(nèi)存不足時,在設置了過期時間的key中,隨機刪除某個key
2.2.6 allkey-lru
內(nèi)存不足時,在全體key范圍內(nèi),優(yōu)先刪除最近最少使用的key
2.2.7 allkey-lfu
內(nèi)存不足時,在全體key范圍內(nèi),優(yōu)先刪除使用頻率最少的key
2.2.8 allkey-random
內(nèi)存不足時,在全體key范圍內(nèi),隨機刪除某個key
2.3 超時剔除
2.3.1 定時刪除
設置一個定時任務,隨機抽取部分過期時間的key,檢查是否過期,過期了就清除掉
2.3.2 惰性刪除
查詢獲取數(shù)據(jù)時,檢查緩存是否過期,過期則刪除,沒過期不刪除
Redis 默認采用惰性刪除+定時刪除結(jié)合的過期策略
2.4 主動更新
2.4.1 主動更新策略
2.4.1.1 Cache Aside Pattern
- 由緩存的調(diào)用者
- 在更新數(shù)據(jù)庫的同時更新緩存
2.4.1.2 Read/Write Through Pattern
- 緩存和數(shù)據(jù)庫整合為一個服務,由服務來維護一致性。
- 調(diào)用者調(diào)用服務,不用關(guān)心一致性問題。
2.4.1.3 Write Behind Caching Pattern
調(diào)用者只操作緩存,由其他線程異步的將緩存數(shù)據(jù)持久化到數(shù)據(jù)庫,最終保持一致。
在企業(yè)中使用最多的主動更新策略是 Cache Aside Pattern。也就是我們自己編碼來保證數(shù)據(jù)的一致性。
2.4.2 主動更新策略需要考慮的三個問題
2.4.1 刪除緩存還是更新緩存?
- 2.4.1.1 刪除緩存
更新數(shù)據(jù)庫時讓緩存失效,查詢時再更新緩存。(延遲加載)一般選擇這個方案。
這個方案比較合理一點,可以避免過多的無效寫操作,緩存刪除后,只要沒人來查詢這條數(shù)據(jù),數(shù)據(jù)就不會被寫入緩存,這樣就可以避免大量無效的寫操作
- 2.4.1.2 更新緩存
每次更新數(shù)據(jù)庫都更新緩存,無效寫操作比較多。
這種方式的缺點很明顯,舉個例子:假如我更新了100次數(shù)據(jù)庫,然后又同時更新了100次緩存,但是在更新的時候并沒有人來查這個數(shù)據(jù),那么我更新這100次緩存好像也沒啥用吧,相當于前99次都是無用功,只有最后一次才是有用的。這就是無效寫操作過多的原因。
2.4.2 如何保證緩存與數(shù)據(jù)庫的操作同時成功或失敗?
1)單體系統(tǒng),將緩存與數(shù)據(jù)庫操作放在一個事務中。
2)分布式系統(tǒng),利用TCC等分布式事務方案。
2.4.3 先操作緩存還是數(shù)據(jù)庫?
- 2.4.3.1 先刪除緩存,再操作數(shù)據(jù)庫
這種方式存在很明顯的問題,假設有兩個并發(fā)操作,線程A更新,線程B查詢。線程A先刪除緩存,然后還沒來得及更新數(shù)據(jù)庫,CPU資源被線程B搶走,線程B查詢緩存發(fā)現(xiàn)沒有命中(因為已經(jīng)被線程A刪除了),查詢數(shù)據(jù)庫,然后把結(jié)果寫入到緩存中。這個時候線程A終于搶到CPU資源了,然后更新數(shù)據(jù)庫,此時就會造成數(shù)據(jù)不一致問題。
- 2.4.3.2 先操作數(shù)據(jù)庫,再刪除緩存
這種處理方式使用的頻率是最高的,因為出錯的概率非常小,只有一種比較極端的情況才會出現(xiàn)數(shù)據(jù)一致性問題。
同樣有兩個并發(fā)請求,線程A查詢、線程B更新,當線程A查詢的時候,緩存剛好失效,然后就去查詢數(shù)據(jù)庫拿到數(shù)據(jù),在準備寫入緩存的時候,CPU資源被線程B搶走,線程B開始更新數(shù)據(jù)庫,然后刪除緩存(這一步其實等于無用,因為緩存已經(jīng)過期)。此時線程A再次獲取到CPU資源,然后寫入緩存,此時寫入的是更新前的舊數(shù)據(jù),會產(chǎn)生數(shù)據(jù)一致性問題。
看起來這確實也是一個問題,但是我們仔細分析一下這種情況都需要滿足哪些條件:
- 1)并發(fā)讀寫操作
- 2)讀緩存時,緩存剛好失效
- 3)寫數(shù)據(jù)庫操作要比寫緩存快
寫數(shù)據(jù)庫是操作磁盤,寫緩存是操作內(nèi)存的,所以不太可能會出現(xiàn)寫磁盤的速度快于寫內(nèi)存的。因此使用這種方式出現(xiàn)數(shù)據(jù)一致性的概率是很小的。
- 2.4.3.3 延時雙刪策略
延遲雙刪策略是分布式系統(tǒng)中數(shù)據(jù)庫存儲和緩存數(shù)據(jù)保持一致性的常用策略,但它不是強一致。其實不管哪種方案,都避免不了Redis存在臟數(shù)據(jù)的問題,只能減輕這個問題,要想徹底解決,得要用到同步鎖和對應的業(yè)務邏輯層面解決。
前面兩種方案的不足點我們進行了分析,第二種方式的使用頻率比較高,但是也有一些小缺陷,雖然說發(fā)生的概率很低,但是這個概率到了線上會不會發(fā)生也不好說,所以就有了延時雙刪策略對第二種方式做補充。
所謂延時雙刪就是先進行緩存清除,再執(zhí)行數(shù)據(jù)庫操作,最后(延遲N秒)再執(zhí)行緩存清除。延遲N秒的時間要大于一次寫操作的時間,這個延時N秒就是了完善保證第二種策略中不足,可以保證線程A的寫緩存和線程B的修改數(shù)據(jù)庫、刪除緩存都執(zhí)行完畢,然后再刪除緩存一次,就可以保證后面再來的查詢請求可以查詢到最新數(shù)據(jù)。
ps: 一般的延時時間設置為3S左右,具體情況要根據(jù)業(yè)務場景取最佳值。
2.5 緩存更新機制總結(jié)
- 1)內(nèi)存淘汰:不用自己維護,利用Redis內(nèi)存淘汰機制,自動刪除部分緩存數(shù)據(jù),這些被刪除的數(shù)據(jù)在下一次被查詢時更新。這種方式一致性最差。
- 2)超時剔除:給緩存數(shù)據(jù)加上過期時間 ,到期后自動刪除,下次查詢時更新,數(shù)據(jù)一致性問題大概率會出現(xiàn)。維護成本比較低。
- 3)主動更新:編寫業(yè)務邏輯,在修改數(shù)據(jù)庫的同時更新緩存,一致性比較好,維護成本比較高。一般采用先操作數(shù)據(jù)庫再更新緩存的方式。
一般在數(shù)據(jù)一致性要求比較低的場景下可以使用內(nèi)存淘汰機制,比如商城首頁的分類信息,這些東西基本上是不會變化的。如果一致性要求比較高,我們可以采用主動更新+超時剔除兜底的方式來處理。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Redis內(nèi)存空間占用及避免數(shù)據(jù)丟失的方法
在現(xiàn)代的互聯(lián)網(wǎng)應用中,Redis作為一種高性能的內(nèi)存數(shù)據(jù)庫,被廣泛應用于緩存、會話管理和消息隊列等場景,然而,Redis的內(nèi)存資源是有限的,過多的內(nèi)存占用可能會導致數(shù)據(jù)丟失所以本文將給大家介紹一下Redis內(nèi)存空間占用及避免數(shù)據(jù)丟失的方法2023-08-08淺析Redis中String數(shù)據(jù)類型及其底層編碼
這篇文章主要介紹?Redis?中?String?數(shù)據(jù)類型及其底層編碼,文中有詳細的代碼示例,對大家的工作及學習有一定的幫助,需要的朋友可以參考下2023-05-05Redis遍歷所有key的兩個命令(KEYS 和 SCAN)
這篇文章主要介紹了Redis遍歷所有key的兩個命令(KEYS 和 SCAN),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-04-04