今天來分享一下Redis幾道常見的面試題:面試
如何解決緩存雪崩?redis
如何解決緩存穿透?數據庫
如何保證緩存與數據庫雙寫時一致的問題?設計模式
回顧一下咱們爲何要用緩存(Redis):緩存
如今有個問題,若是咱們的緩存掛掉了,這意味着咱們的所有請求都跑去數據庫了。架構
在前面學習咱們都知道Redis不可能把全部的數據都緩存起來(內存昂貴且有限),因此Redis須要對數據設置過時時間,並採用的是惰性刪除+按期刪除兩種策略對過時鍵刪除。併發
若是緩存數據設置的過時時間是相同的,而且Redis剛好將這部分數據所有刪光了。這就會致使在這段時間內,這些緩存同時失效,所有請求到數據庫中。分佈式
這就是緩存雪崩:ide
Redis掛掉了,請求所有走數據庫。高併發
對緩存數據設置相同的過時時間,致使某段時間內緩存失效,請求所有走數據庫。
緩存雪崩若是發生了,極可能就把咱們的數據庫搞垮,致使整個服務癱瘓!
對於「對緩存數據設置相同的過時時間,致使某段時間內緩存失效,請求所有走數據庫。」這種狀況,很是好解決:
解決方法:在緩存的時候給過時時間加上一個隨機值,這樣就會大幅度的減小緩存在同一時間過時。
對於「Redis掛掉了,請求所有走數據庫」這種狀況,咱們能夠有如下的思路:
事發前:實現Redis的高可用(主從架構+Sentinel 或者Redis Cluster),儘可能避免Redis掛掉這種狀況發生。
事發中:萬一Redis真的掛了,咱們能夠設置本地緩存(ehcache)+限流(hystrix),儘可能避免咱們的數據庫被幹掉(起碼能保證咱們的服務仍是能正常工做的)
事發後:redis持久化,重啓後自動從磁盤上加載數據,快速恢復緩存數據。
好比,咱們有一張數據庫表,ID都是從1開始的(正數):
可是可能有黑客想把個人數據庫搞垮,每次請求的ID都是負數。這會致使個人緩存就沒用了,請求所有都找數據庫去了,但數據庫也沒有這個值啊,因此每次都返回空出去。
緩存穿透是指查詢一個必定不存在的數據。因爲緩存不命中,而且出於容錯考慮,若是從數據庫查不到數據則不寫入緩存,這將致使這個不存在的數據每次請求都要到數據庫去查詢,失去了緩存的意義。
這就是緩存穿透:
請求的數據在緩存大量不命中,致使請求走數據庫。
緩存穿透若是發生了,也可能把咱們的數據庫搞垮,致使整個服務癱瘓!
解決緩存穿透也有兩種方案:
因爲請求的參數是不合法的(每次都請求不存在的參數),因而咱們可使用布隆過濾器(BloomFilter)或者壓縮filter提早攔截,不合法就不讓這個請求到數據庫層!
當咱們從數據庫找不到的時候,咱們也將這個空對象設置到緩存裏邊去。下次再請求的時候,就能夠從緩存裏邊獲取了。
這種狀況咱們通常會將空對象設置一個較短的過時時間。
上面講緩存穿透的時候也提到了:若是從數據庫查不到數據則不寫入緩存。
通常咱們對讀操做的時候有這麼一個固定的套路:
若是咱們的數據在緩存裏邊有,那麼就直接取緩存的。
若是緩存裏沒有咱們想要的數據,咱們會先去查詢數據庫,而後將數據庫查出來的數據寫到緩存中。
最後將數據返回給請求
若是僅僅查詢的話,緩存的數據和數據庫的數據是沒問題的。可是,當咱們要更新時候呢?各類狀況極可能就形成數據庫和緩存的數據不一致了。
這裏不一致指的是:數據庫的數據跟緩存的數據不一致
從理論上說,只要咱們設置了鍵的過時時間,咱們就能保證緩存和數據庫的數據最終是一致的。由於只要緩存數據過時了,就會被刪除。隨後讀的時候,由於緩存裏沒有,就能夠查數據庫的數據,而後將數據庫查出來的數據寫入到緩存中。
除了設置過時時間,咱們還須要作更多的措施來儘可能避免數據庫與緩存處於不一致的狀況發生。
通常來講,執行更新操做時,咱們會有兩種選擇:
先操做數據庫,再操做緩存
先操做緩存,再操做數據庫
首先,要明確的是,不管咱們選擇哪一個,咱們都但願這兩個操做要麼同時成功,要麼同時失敗。因此,這會演變成一個分佈式事務的問題。
因此,若是原子性被破壞了,可能會有如下的狀況:
操做數據庫成功了,操做緩存失敗了。
操做緩存成功了,操做數據庫失敗了。
若是第一步已經失敗了,咱們直接返回Exception出去就行了,第二步根本不會執行。
下面咱們具體來分析一下吧。
操做緩存也有兩種方案:
更新緩存
刪除緩存
通常咱們都是採起刪除緩存緩存策略的,緣由以下:
高併發環境下,不管是先操做數據庫仍是後操做數據庫而言,若是加上更新緩存,那就更加容易致使數據庫與緩存數據不一致問題。(刪除緩存直接和簡單不少)
若是每次更新了數據庫,都要更新緩存【這裏指的是頻繁更新的場景,這會耗費必定的性能】,倒不如直接刪除掉。等再次讀取時,緩存裏沒有,那我到數據庫找,在數據庫找到再寫到緩存裏邊(體現懶加載)
基於這兩點,對於緩存在更新時而言,都是建議執行刪除操做!
正常的狀況是這樣的:
先操做數據庫,成功;
再刪除緩存,也成功;
若是原子性被破壞了:
第一步成功(操做數據庫),第二步失敗(刪除緩存),會致使數據庫裏是新數據,而緩存裏是舊數據。
若是第一步(操做數據庫)就失敗了,咱們能夠直接返回錯誤(Exception),不會出現數據不一致。
若是在高併發的場景下,出現數據庫與緩存數據不一致的機率特別低,也不是沒有:
緩存恰好失效
線程A查詢數據庫,得一箇舊值
線程B將新值寫入數據庫
線程B刪除緩存
線程A將查到的舊值寫入緩存
要達成上述狀況,仍是說一句機率特別低:
由於這個條件須要發生在讀緩存時緩存失效,並且併發着有一個寫操做。而實際上數據庫的寫操做會比讀操做慢得多,並且還要鎖表,而讀操做必需在寫操做前進入數據庫操做,而又要晚於寫操做更新緩存,全部的這些條件都具有的機率基本並不大。
對於這種策略,實際上是一種設計模式:Cache Aside Pattern
刪除緩存失敗的解決思路:
將須要刪除的key發送到消息隊列中
本身消費消息,得到須要刪除的key
不斷重試刪除操做,直到成功
正常狀況是這樣的:
先刪除緩存,成功;
再更新數據庫,也成功;
若是原子性被破壞了:
第一步成功(刪除緩存),第二步失敗(更新數據庫),數據庫和緩存的數據仍是一致的。
若是第一步(刪除緩存)就失敗了,咱們能夠直接返回錯誤(Exception),數據庫和緩存的數據仍是一致的。
看起來是很美好,可是咱們在併發場景下分析一下,就知道仍是有問題的了:
線程A刪除了緩存
線程B查詢,發現緩存已不存在
線程B去數據庫查詢獲得舊值
線程B將舊值寫入緩存
線程A將新值寫入數據庫
因此也會致使數據庫和緩存不一致的問題。
併發下解決數據庫與緩存不一致的思路:
將刪除緩存、修改數據庫、讀取緩存等的操做積壓到隊列裏邊,實現串行化。
咱們能夠發現,兩種策略各自有優缺點:
先刪除緩存,再更新數據庫
在高併發下表現不如意,在原子性被破壞時表現優異
先更新數據庫,再刪除緩存(Cache Aside Pattern設計模式)
在高併發下表現優異,在原子性被破壞時表現不如意
能夠用databus或者阿里的canal監聽binlog進行更新。
最後
這是幾道Redis常見的面試題,但願你們看完有所幫助,順利拿到offer!