緩存穿透、緩存雪崩、緩存擊穿?再也不怕了,你隨便問吧!
?背景
在現代軟件架構中,緩存的應用已經非常普及。緩存的使用在面試和實踐中都是避不開的硬技能、硬知識,如果你說還不太熟悉緩存的使用,可能都不好意思說自己是程序員。
在上篇文章《??如果不知道這4種緩存模式,敢說懂緩存嗎???》中,我們介紹了緩存使用的四種策略,如果能夠結合不同的場景進行靈活運用,你已經超過了大多數人。畢竟,那四種策略,很多開發多年的人可能都沒聽說過。
這篇文章,帶大家進一步學習在緩存使用中不得不考慮三個特殊場景:緩存穿透、緩存雪崩、緩存擊穿。
為什么說不得不考慮?因為如果不考慮這些特殊的場景,在高并發的情況可能直接導致系統崩潰。下面以常見的Redis緩存組件為例來講解這三種場景及解決方案。
大前提
當我們使用緩存時,目標通常有兩個:第一,提升響應效率和并發量;第二,減輕數據庫的壓力。
而本文中所提到的這三種場景:緩存穿透、緩存雪崩和緩存擊穿的發生,都是因為在某些特殊情況下,緩存失去了預期的功能所致。
當緩存失效或沒有抵擋住流量,流量直接涌入到數據庫,在高并發的情況下,可能直接擊垮數據庫,導致整個系統崩潰。
這就是我們需要知道的大前提,而緩存穿透、緩存雪崩和緩存擊穿,只不過是在這個大前提下的不同場景的細分場景而已。
緩存穿透
大多數情況,緩存可以減少數據庫的查詢,提升系統性能。
通常流程是:一個請求過來,先查詢是否在緩存當中,如果緩存中存在,則直接返回。如果緩存中不存在對應的數據,則檢索數據庫,如果數據庫中存在對應的數據,則更新緩存并返回結果。如果數據庫中也不存在對應的數據,則返回空或錯誤。
緩存穿透(cache penetration)是用戶訪問的數據既不在緩存當中,也不在數據庫中。出于容錯的考慮,如果從底層數據庫查詢不到數據,則不寫入緩存。這就導致每次請求都會到底層數據庫進行查詢,緩存也失去了意義。當高并發或有人利用不存在的Key頻繁攻擊時,數據庫的壓力驟增,甚至崩潰,這就是緩存穿透問題。
緩存穿透
緩存穿透發生的場景一般有兩類:
- 原來數據是存在的,但由于某些原因(誤刪除、主動清理等)在緩存和數據庫層面被刪除了,但前端或前置的應用程序依舊保有這些數據;
- 惡意攻擊行為,利用不存在的Key或者惡意嘗試導致產生大量不存在的業務數據請求。
緩存穿透通常有四種解決方案,我們逐一介紹分析。
方案一:緩存空值(null)或默認值
分析業務請求,如果是正常業務請求時發生緩存穿透現象,可針對相應的業務數據,在數據庫查詢不存在時,將其緩存為空值(null)或默認值。需要注意的是,針對空值的緩存失效時間不宜過長,一般設置為5分鐘之內。當數據庫被寫入或更新該key的新數據時,緩存必須同時被刷新,避免數據不一致。
方案二:業務邏輯前置校驗
在業務請求的入口處進行數據合法性校驗,檢查請求參數是否合理、是否包含非法值、是否惡意請求等,提前有效阻斷非法請求。比如,根據年齡查詢時,請求的年齡為-10歲,這顯然是不合法的請求參數,直接在參數校驗時進行判斷返回。
方案三:使用布隆過濾器請求白名單
在寫入數據時,使用布隆過濾器進行標記(相當于設置白名單),業務請求發現緩存中無對應數據時,可先通過查詢布隆過濾器判斷數據是否在白名單內,如果不在白名單內,則直接返回空或失敗。
方案四:用戶黑名單限制
當發生異常情況時,實時監控訪問的對象和數據,分析用戶行為,針對故意請求、爬蟲或攻擊者,進行特定用戶的限制;
當然,可能針對緩存穿透的情況,也有可能是其他的原因引起,可以針對具體情況,采用對應的措施。
緩存雪崩
在使用緩存時,通常會對緩存設置過期時間,一方面目的是保持緩存與數據庫數據的一致性,另一方面是減少冷緩存占用過多的內存空間。
但當緩存中大量熱點緩存采用了相同的實效時間,就會導致緩存在某一個時刻同時實效,請求全部轉發到數據庫,從而導致數據庫壓力驟增,甚至宕機。從而形成一系列的連鎖反應,造成系統崩潰等情況,這就是緩存雪崩(Cache Avalanche)。
緩存雪崩
上面講到的是熱點key同時失效的場景,另外就是由于某些原因導致緩存服務宕機、掛掉或不響應,也同樣會導致流量直接轉移到數據庫。
所以,緩存雪崩的場景通常有兩個:
- 大量熱點key同時過期;
- 緩存服務故障;
緩存雪崩的解決方案:
- 通常的解決方案是將key的過期時間后面加上一個隨機數(比如隨機1-5分鐘),讓key均勻的失效。
- 考慮用隊列或者鎖的方式,保證緩存單線程寫,但這種方案可能會影響并發量。
- 熱點數據可以考慮不失效,后臺異步更新緩存,適用于不嚴格要求緩存一致性的場景。
- 雙key策略,主key設置過期時間,備key不設置過期時間,當主key失效時,直接返回備key值。
- 構建緩存高可用集群(針對緩存服務故障情況)。
- 當緩存雪崩發生時,服務熔斷、限流、降級等措施保障。
緩存擊穿
緩存雪崩是指只大量熱點key同時失效的情況,如果是單個熱點key,在不停的扛著大并發,在這個key失效的瞬間,持續的大并發請求就會擊破緩存,直接請求到數據庫,好像蠻力擊穿一樣。這種情況就是緩存擊穿(Cache Breakdown)。
緩存擊穿
從定義上可以看出,緩存擊穿和緩存雪崩很類似,只不過是緩存擊穿是一個熱點key失效,而緩存雪崩是大量熱點key失效。因此,可以將緩存擊穿看作是緩存雪崩的一個子集。
緩存擊穿的解決方案:
- 使用互斥鎖(Mutex Key),只讓一個線程構建緩存,其他線程等待構建緩存執行完畢,重新從緩存中獲取數據。單機通過synchronized或lock來處理,分布式環境采用分布式鎖。
- 熱點數據不設置過期時間,后臺異步更新緩存,適用于不嚴格要求緩存一致性的場景。
- ”提前“使用互斥鎖(Mutex Key):在value內部設置一個比緩存(Redis)過期時間短的過期時間標識,當異步線程發現該值快過期時,馬上延長內置的這個時間,并重新從數據庫加載數據,設置到緩存中去。
小結
本文介紹了在使用緩存時經常會遇到的三種異常情況:緩存穿透、緩存雪崩和緩存擊穿。
三種異常情況從根本上來說都是因為本應該訪問緩存的,但是緩存不存在或服務異常,導致流量直接進入了數據庫層面。
其中緩存雪崩和緩存擊穿是因為數據不存在(或服務異常獲取不到),導致大量請求訪問數據庫,從而導致數據庫壓力驟增,甚至崩潰。
而緩存穿透則是由于數據本身就不存在,導致緩存沒有進行數據緩存,流量進入數據庫層。
針對不同的緩存異常場景,可選擇不同的方案來進行處理。當然,除了上述方案,我們還可以限流、降級、熔斷等服務層的措施,也可以考慮數據庫層是否可以進行橫向擴展,當緩存異常發生時,確保數據庫能夠抗住流量,不至于讓整個系統崩潰。