1. 程式人生 > >這幾道Redis面試題都不懂,怎麽拿offer?

這幾道Redis面試題都不懂,怎麽拿offer?

sid data 自己 隊列 開始 更新 分布式 參數 惰性

一、緩存雪崩
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提前攔截,不合法就不讓這個請求到數據庫層!
當我們從數據庫找不到的時候,我們也將這個空對象設置到緩存裏邊去。下次再請求的時候,就可以從緩存裏邊獲取了。
這種情況我們一般會將空對象設置一個較短的過期時間。

三、緩存與數據庫雙寫一致
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進行更新。

這幾道Redis面試題都不懂,怎麽拿offer?