前言
隻有光頭才能變強。
文本已收錄至我的GitHub倉庫,歡迎Star:
https://github.com/ZhongFuCheng3y/3y
回顧前面:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL6lGWDxkVW9CXt92YuIXdn1Wauk2Lc9CX6MHc0RHaiojIsJye.png)
今天來分享一下Redis幾道常見的面試題:
- 如何解決緩存雪崩?
- 如何解決緩存穿透?
- 如何保證緩存與資料庫雙寫時一緻的問題?
一、緩存雪崩
1.1什麼是緩存雪崩?
回顧一下我們為什麼要用緩存(Redis):
現在有個問題,如果我們的緩存挂掉了,這意味着我們的全部請求都跑去資料庫了。
在前面學習我們都知道Redis不可能把所有的資料都緩存起來(記憶體昂貴且有限),是以Redis需要對資料設定過期時間,并采用的是惰性删除+定期删除兩種政策對過期鍵删除。
Redis對過期鍵的政策+持久化如果緩存資料設定的過期時間是相同的,并且Redis恰好将這部分資料全部删光了。這就會導緻在這段時間内,這些緩存同時失效,全部請求到資料庫中。
這就是緩存雪崩:
- Redis挂掉了,請求全部走資料庫。
- 對緩存資料設定相同的過期時間,導緻某段時間内緩存失效,請求全部走資料庫。
緩存雪崩如果發生了,很可能就把我們的資料庫搞垮,導緻整個服務癱瘓!
1.2如何解決緩存雪崩?
對于“對緩存資料設定相同的過期時間,導緻某段時間内緩存失效,請求全部走資料庫。”這種情況,非常好解決:
- 解決方法:在緩存的時候給過期時間加上一個随機值,這樣就會大幅度的減少緩存在同一時間過期。
對于“Redis挂掉了,請求全部走資料庫”這種情況,我們可以有以下的思路:
- 事發前:實作Redis的高可用(主從架構+Sentinel 或者Redis Cluster),盡量避免Redis挂掉這種情況發生。
- 事發中:萬一Redis真的挂了,我們可以設定本地緩存(ehcache)+限流(hystrix),盡量避免我們的資料庫被幹掉(起碼能保證我們的服務還是能正常工作的)
- 事發後:redis持久化,重新開機後自動從磁盤上加載資料,快速恢複緩存資料。
二、緩存穿透
2.1什麼是緩存穿透
比如,我們有一張資料庫表,ID都是從1開始的(正數):
但是可能有黑客想把我的資料庫搞垮,每次請求的ID都是負數。這會導緻我的緩存就沒用了,請求全部都找資料庫去了,但資料庫也沒有這個值啊,是以每次都傳回空出去。
緩存穿透是指查詢一個一定不存在的資料。由于緩存不命中,并且出于容錯考慮,如果從資料庫查不到資料則不寫入緩存,這将導緻這個不存在的資料每次請求都要到資料庫去查詢,失去了緩存的意義。
這就是緩存穿透:
- 請求的資料在緩存大量不命中,導緻請求走資料庫。
緩存穿透如果發生了,也可能把我們的資料庫搞垮,導緻整個服務癱瘓!
2.1如何解決緩存穿透?
解決緩存穿透也有兩種方案:
- 由于請求的參數是不合法的(每次都請求不存在的參數),于是我們可以使用布隆過濾器(BloomFilter)或者壓縮filter提前攔截,不合法就不讓這個請求到資料庫層!
- 當我們從資料庫找不到的時候,我們也将這個空對象設定到緩存裡邊去。下次再請求的時候,就可以從緩存裡邊擷取了。
- 這種情況我們一般會将空對象設定一個較短的過期時間。
參考資料:
- 緩存系列文章--5.緩存穿透問題
三、緩存與資料庫雙寫一緻
3.1對于讀操作,流程是這樣的
上面講緩存穿透的時候也提到了:如果從資料庫查不到資料則不寫入緩存。
一般我們對讀操作的時候有這麼一個固定的套路:
- 如果我們的資料在緩存裡邊有,那麼就直接取緩存的。
- 如果緩存裡沒有我們想要的資料,我們會先去查詢資料庫,然後将資料庫查出來的資料寫到緩存中。
- 最後将資料傳回給請求
3.2什麼是緩存與資料庫雙寫一緻問題?
如果僅僅查詢的話,緩存的資料和資料庫的資料是沒問題的。但是,當我們要更新時候呢?各種情況很可能就造成資料庫和緩存的資料不一緻了。
- 這裡不一緻指的是:資料庫的資料跟緩存的資料不一緻
從理論上說,隻要我們設定了鍵的過期時間,我們就能保證緩存和資料庫的資料最終是一緻的。因為隻要緩存資料過期了,就會被删除。随後讀的時候,因為緩存裡沒有,就可以查資料庫的資料,然後将資料庫查出來的資料寫入到緩存中。
除了設定過期時間,我們還需要做更多的措施來盡量避免資料庫與緩存處于不一緻的情況發生。
3.3對于更新操作
一般來說,執行更新操作時,我們會有兩種選擇:
- 先操作資料庫,再操作緩存
- 先操作緩存,再操作資料庫
首先,要明确的是,無論我們選擇哪個,我們都希望這兩個操作要麼同時成功,要麼同時失敗。是以,這會演變成一個分布式事務的問題。
是以,如果原子性被破壞了,可能會有以下的情況:
- 操作資料庫成功了,操作緩存失敗了。
- 操作緩存成功了,操作資料庫失敗了。
如果第一步已經失敗了,我們直接傳回Exception出去就好了,第二步根本不會執行。
下面我們具體來分析一下吧。
3.3.1操作緩存
操作緩存也有兩種方案:
- 更新緩存
- 删除緩存
一般我們都是采取删除緩存緩存政策的,原因如下:
- 高并發環境下,無論是先操作資料庫還是後操作資料庫而言,如果加上更新緩存,那就更加容易導緻資料庫與緩存資料不一緻問題。(删除緩存直接和簡單很多)
- 如果每次更新了資料庫,都要更新緩存【這裡指的是頻繁更新的場景,這會耗費一定的性能】,倒不如直接删除掉。等再次讀取時,緩存裡沒有,那我到資料庫找,在資料庫找到再寫到緩存裡邊(展現懶加載)
基于這兩點,對于緩存在更新時而言,都是建議執行删除操作!
3.3.2先更新資料庫,再删除緩存
正常的情況是這樣的:
- 先操作資料庫,成功;
- 再删除緩存,也成功;
如果原子性被破壞了:
- 第一步成功(操作資料庫),第二步失敗(删除緩存),會導緻資料庫裡是新資料,而緩存裡是舊資料。
- 如果第一步(操作資料庫)就失敗了,我們可以直接傳回錯誤(Exception),不會出現資料不一緻。
如果在高并發的場景下,出現資料庫與緩存資料不一緻的機率特别低,也不是沒有:
- 緩存剛好失效
- 線程A查詢資料庫,得一個舊值
- 線程B将新值寫入資料庫
- 線程B删除緩存
- 線程A将查到的舊值寫入緩存
要達成上述情況,還是說一句機率特别低:
因為這個條件需要發生在讀緩存時緩存失效,而且并發着有一個寫操作。而實際上資料庫的寫操作會比讀操作慢得多,而且還要鎖表,而讀操作必需在寫操作前進入資料庫操作,而又要晚于寫操作更新緩存,所有的這些條件都具備的機率基本并不大。
對于這種政策,其實是一種設計模式:
Cache Aside Pattern
删除緩存失敗的解決思路:
- 将需要删除的key發送到消息隊列中
- 自己消費消息,獲得需要删除的key
- 不斷重試删除操作,直到成功
3.3.3先删除緩存,再更新資料庫
正常情況是這樣的:
- 先删除緩存,成功;
- 再更新資料庫,也成功;
- 第一步成功(删除緩存),第二步失敗(更新資料庫),資料庫和緩存的資料還是一緻的。
- 如果第一步(删除緩存)就失敗了,我們可以直接傳回錯誤(Exception),資料庫和緩存的資料還是一緻的。
看起來是很美好,但是我們在并發場景下分析一下,就知道還是有問題的了:
- 線程A删除了緩存
- 線程B查詢,發現緩存已不存在
- 線程B去資料庫查詢得到舊值
- 線程B将舊值寫入緩存
- 線程A将新值寫入資料庫
是以也會導緻資料庫和緩存不一緻的問題。
并發下解決資料庫與緩存不一緻的思路:
- 将删除緩存、修改資料庫、讀取緩存等的操作積壓到隊列裡邊,實作串行化。
3.4對比兩種政策
我們可以發現,兩種政策各自有優缺點:
- 先删除緩存,再更新資料庫
- 在高并發下表現不如意,在原子性被破壞時表現優異
- 先更新資料庫,再删除緩存(
設計模式)Cache Aside Pattern
- 在高并發下表現優異,在原子性被破壞時表現不如意
3.5其他保障資料一緻的方案與資料
可以用databus或者阿裡的canal監聽binlog進行更新。
- 緩存更新的套路
- 如何保證緩存與資料庫雙寫時的資料一緻性?
- 分布式之資料庫和緩存雙寫一緻性方案解析
- Cache Aside Pattern
最後
這是幾道Redis常見的面試題,希望大家看完有所幫助,順利拿到offer!
樂于輸出幹貨的Java技術公衆号:Java3y。200多篇原創技術文章、海量視訊資源、精美腦圖!
精彩回顧:
覺得我的文章寫得不錯,不妨點一下贊我!