如果第二次看到我的文章,歡迎點選文末連結掃碼訂閱我個人的公眾號(跨界架構師)喲~
本文長度為2805字,建議閱讀8分鐘。
堅持原創,每一篇都是用心之作~
有句話說得好,欲要使其毀滅,先要使其瘋狂。當你沉浸在快取所帶來的系統tps飆升的喜悅中時,使你係統毀滅的種子也已經埋在其中。
而且,你所承載的tps越高,它所帶來的毀滅性更大。
在前兩篇《360°全方位解讀「快取」》和《先寫DB還是「快取」?》中,我們已經對快取有了一定的認識,並且知道了關於快取相關的「一致性」問題的最佳實踐。
這次,我們就來聊聊隱藏在快取中的毀滅性種子是什麼?
我們從前一篇文章《先寫DB還是「快取」?》中多次提到的「cache miss」說起。
快取雪崩
在前一篇文章《先寫DB還是「快取」?》中,我們多次提到了「cache miss」這個詞,利用「cache miss」來更好的保障DB和快取之間的資料一致性。
然而,任何事物都是有兩面性的,「cache miss」在提供便利的同時,也帶來了一個潛在風險。
這個風險就是「快取雪崩」。
在圖中的第二步,大量的請求併發進入,這裡的一次「cache miss」就有可能導致產生「快取雪崩」。
不過,雖然「cache miss」會產生「快取雪崩」,但「快取雪崩」並不僅僅產生於「cache miss」。
雪崩一詞源於「雪崩效應」,是指像「多米勒骨牌」這樣的級聯反應。前面沒頂住,導致影響後面,如此蔓延。(關於對應雪崩的方式參考之前的文章,文末放連結)
所以「快取雪崩」的根本問題是:快取由於某些原因未起到預期的緩衝效果,導致請求全部流轉到資料庫,造成資料庫壓力過重。
因此,流量激增、高併發下的快取過期、甚至快取系統當機都有可能產生「快取雪崩」問題。
怎麼解決這個問題呢?當機可以通過做高可用來解決(可以參考之前的文章,文末放連結)。而在“流量激增”、“高併發下的快取過期”這兩種場景下,也有兩種方式可以來解決。
加鎖排隊
通過加鎖或者排隊機制來限制讀資料庫寫快取的執行緒數量。比如,下面的虛擬碼就是對某個key只允許一個執行緒進入的效果。
key = "aaa";
var cacheValue = cache.read(key);
if (cacheValue != null) {
return cacheValue;
}
else {
lock(key) {
cacheValue = cache.read(key);
if (cacheValue != null) {
return cacheValue;
}
else {
cacheValue = db.read(key);
cache.set(key,cacheValue);
}
}
return cacheValue;
} 複製程式碼
這個比較好理解,就不廢話了。
快取時間增加隨機值
這個主要針對的是「快取定時過期」機制下的取巧方案。它的目的是避免多個快取key在同一時間失效,導致壓力更加集中。
比如,你有10個key,他們的過期時間都是30分鐘的話,那麼30分鐘後這10個key的所有請求會同時流到db去。
而這裡說的這種方式就是將這10個key的過期時間打亂,比如設定成25、26、27、...、34分的過期時間,這樣壓力就被分散了,每分鐘只有一個key過期。
最簡單粗暴的方式就是在設定「過期時間」的時候加一個隨機數字。
cache.set(key,cacheValue,30+random())複製程式碼
總體來看,相比後者,前者的適用面更廣,所以Z哥建議你用「加鎖排隊」作為預設的通用方案不失為一個不錯的選擇。
「快取穿透」、「快取雪崩」傻傻分不清楚?
如果你聽說過「快取穿透」的話,可能會問:「快取雪崩」和「快取穿透」一樣嗎?
從產生的效果上看是一樣的,但是過程不同。
來舉個例子。例子純屬虛構,別太在意合理性~
在一個方圓一萬里的地區內,只有一個修手機的老師傅。他收了一個徒弟,希望徒弟能幫他分擔掉一部分的工作壓力。這裡的老師傅可以看作是DB,徒弟看作是快取。
老師傅對徒弟說,如果遇到你不會做的事你來請教我。
然後,一個客戶過來說要修一下他的衛星電話,徒弟去請教老師傅,老師傅說他也不會,先拒絕了吧。
但是由於沒告訴他後續遇到修衛星電話的人該怎麼做,所以後續這個客戶一直來問,徒弟每次都又去請教老師傅。最終,在修衛星電話這件事上,徒弟並沒有幫老師傅緩解任何的壓力,快被煩死了。
上面這個故事就好比「快取穿透」。
而「快取雪崩」則是,由於徒弟年輕力壯,精力充沛,1小時能修20個手機,老師傅只能修10個(但是手藝好,更考究)。
然後,有一天徒弟請假了,但恰巧這天來了2000個修手機的,老師傅修不過來就被累垮了。
所以,「快取穿透」和「快取雪崩」最終產生的效果是一樣的,就是因為大量請求流到DB後,把DB拖垮(正如前面故事中的老師傅)。
兩者最大的不同在於,「快取雪崩」問題只要資料從db中找到並放入快取就能恢復正常(徒弟休假歸來),而「快取穿透」指的是所需的資料在DB中一直不存在的情況(老師傅也不會修)。並且,由於DB中資料不存在,所以自然每次從快取中也找不到(徒弟也不會修)。
清楚了兩者的區別之後,我們下面就來聊聊「快取穿透」的常見應對方式。
快取穿透
「快取穿透」有時也叫做「快取擊穿」,產生的邏輯過程是這樣,一直在虛線範圍內流轉。
在這種場景下,快取的作用完全失效,每次請求都“穿透”到了DB中。
可能你會想,為什麼會存在大量的這種db中資料不存在的情況呢?其實,任何依賴外部引數進行查詢的地方都可能有這個問題的存在。比如,一個文字輸入框,本來是讓你輸入使用者名稱的,但是手誤輸入了密碼,自然就找不到資料咯。更主要的問題是,會有惡意分子利用這種機制來對你的系統進行攻擊,擊穿快取搞垮你的資料庫,導致整個系統全面癱瘓。
同樣也有兩種方式來解決這個問題。
布隆過濾器(bloomfilter)
布隆過濾器就是由一個很長的二進位制向量和一系列隨機對映函式組成,將確定不存在的資料構建到過濾器中,用它來過濾請求。這裡就放個圖,具體就不展開了,後續我們再聊(有興趣的可以先到搜尋引擎搜《Space time trade-offs in hash coding with allowable errors》找到bloom的原始論文)。
實現程式碼其實並不很複雜,參考論文或者網上其他作者的一些實現就可以寫出來。
不過,布隆過濾器有一個最大的缺點,也是其為了高效利用記憶體而付出的代價,就是無法確保100%的準確率。
所以,如果你的場景要求是100%準確的,就只能用下面這種方式了。
快取空物件
其實就是哪怕從db中取出的資料是“空(null)”,也把它丟失到快取中。
這樣一來,雖然快取中存在著一個value為空的資料,但是至少他能表示“資料庫裡也沒有不用找了”。
其實這個思路和布隆過濾器有些類似,但是它對記憶體的消耗會大很多,畢竟布隆過濾器是利用的bit位來儲存。不過這種方式的優勢是前面提到的,不會出現誤差,而布隆過濾器的錯誤率會隨著「位數」的增加而減少,會不斷趨近於0,但不會為0。
總結
好了,我們一起總結一下。
這次呢,Z哥主要和你聊了隱藏在快取中的兩顆具有“毀滅性”的種子,「快取雪崩」和「快取穿透」,以及應對這兩顆種子的常用方式。
而且,順便幫你區分清楚了「快取雪崩」和「快取穿透」的差異。
希望對你有所啟發。
相關文章:
作者:Zachary
出處:www.cnblogs.com/Zachary-Fan…
如果你喜歡這篇文章,可以點一下左側的「大拇指」哦~。
這樣可以給我一點反饋。: )
謝謝你的舉手之勞。
▶關於作者:張帆(Zachary,個人微訊號:Zachary-ZF)。堅持用心打磨每一篇高質量原創。本文首發於公眾號:「跨界架構師」(ID:Zachary_ZF)。<-- 點選後閱讀熱門文章
定期發表原創內容:架構設計丨分散式系統丨產品丨運營丨一些思考。
如果你是初級程式設計師,想提升但不知道如何下手。又或者做程式設計師多年,陷入了一些瓶頸想拓寬一下視野。歡迎關注我的公眾號「跨界架構師」,回覆「技術」,送你一份我長期收集和整理的思維導圖。
如果你是運營,面對不斷變化的市場束手無策。又或者想了解主流的運營策略,以豐富自己的“倉庫”。歡迎關注我的公眾號「跨界架構師」,回覆「運營」,送你一份我長期收集和整理的思維導圖。