對於快取,大家肯定都不陌生,不管是前端還是服務端開發,快取幾乎都是必不可少的優化方式之一。在實際生產環境中,快取的使用規範也是一直備受重視的,如果使用的不好,很容易就遇到快取擊穿、雪崩等嚴重異常情景,從而給系統帶來難以預料的災害。
為了避免快取使用不當帶來的損失,我們有必要了解每種異常產生的原因和解決辦法,從而做出更好的預防措施。
快取穿透
而快取穿透是指快取和資料庫中都沒有的資料,這樣每次請求都會去查庫,不會查快取,如果同一時間有大量請求進來的話,就會給資料庫造成巨大的查詢壓力,甚至擊垮db系統。
比如說查詢id為-1的商品,這樣的id在商品表裡肯定不存在,如果沒做特殊處理的話,攻擊者很容易可以讓系統奔潰,那我們該如何避免這種情況發生呢?
一般來說,快取穿透常用的解決方案大概有兩種:
一、快取空物件
當快取和資料都查不到對應key的資料時,可以將返回的空物件寫到快取中,這樣下次請求該key時直接從快取中查詢返回空物件,就不用走db了。當然,為了避免儲存過多空物件,通常會給空物件設定一個比較短的過期時間,就比如像這樣給key設定30秒的過期時間:
redisTemplate.opsForValue().set(key, null, 30, TimeUnit.SECONDS);
這種方法會存在兩個問題:
如果有大量的key穿透,快取空物件會佔用寶貴的記憶體空間。 空物件的key設定了過期時間,這段時間內可能資料庫剛好有了該key的資料,從而導致資料不一致的情況。
這種情況下,我們可以用更好的解決方案,也就是布隆過濾器
二、Bloom Filter
布隆過濾器(Bloom Filter)是1970年由一個叫布隆的小夥子提出的,是一種由一個很長的二進位制向量和一系列隨機對映函式構成的概率型資料結構,這種資料結構的空間效率非常高,可以用於檢索集合中是否存在特定的元素。
設計思想
布隆過濾器由一個長度為m位元的位陣列(bit array)與k個雜湊函式(hash function)組成的資料結構。原理是當一個元素被加入集合時,通過K個雜湊函式將這個元素對映成一個位陣列中的K個點,把它們置為1。檢索時,我們只要看看這些點是不是都是1就大約知道集合中有沒有它了,也就是說,如果這些點有任何一個0,則被檢元素一定不在;如果都是1,則被檢元素很可能在。
至於說為什麼都是1的情況只是可能存在檢索元素,這是因為不同的元素計算的雜湊值有可能一樣,會出現雜湊碰撞,導致一個不存在的元素有可能對應的位元位為1。
舉個例子:下圖是一個布隆過濾器,共有18個位元位,3個雜湊函式。當查詢某個元素w時,通過三個雜湊函式計算,發現有一個位元位的值為0,可以肯定認為該元素不在集合中。
優缺點
優點:
節省空間:不需要儲存資料本身,只需要儲存資料對應hash位元位 時間複雜度低:基於雜湊演算法來查詢元素,插入和查詢的時間複雜度都為O(k),k為雜湊函式的個數
缺點:
準確率有誤:布隆過濾器判斷存在,可能出現元素不在集合中;判斷準確率取決於雜湊函式的個數 不能刪除元素:如果一個元素被刪除,但是卻不能從布隆過濾器中刪除,這樣進一步導致了不存在的元素也會顯示1的情況。
適用場景
爬蟲系統url去重 垃圾郵件過濾 黑名單
快取擊穿
快取擊穿從字面上看很容易讓人跟穿透搞混,這也是很多面試官喜歡埋坑的地方,當然,只要我們對知識點了然於心的話,面試的時候也不會那麼被糊弄
簡單來說,快取擊穿是指一個key非常熱點,在不停的扛著大併發,大併發集中對這一個點進行訪問,當這個key在失效的瞬間,持續的大併發就穿破快取,直接請求資料庫,就好像堤壩突然破了一個口,大量洪水洶湧而入。
當發生快取擊穿的時候,資料庫的查詢壓力會倍增,導致大量的請求阻塞。
解決辦法也不難,既然是熱點key,那麼說明該key會一直被訪問,既然如此,我們就不對這個key設定失效時間了,如果資料需要更新的話,我們可以後臺開啟一個非同步執行緒,發現過期的key直接重寫快取即可。
當然,這種解決方案只適用於不要求資料嚴格一致性的情況,因為當後臺執行緒在構建快取的時候,其他的執行緒很有可能也在讀取資料,這樣就會訪問到舊資料了。
如果要嚴格保證資料一致的話,可以用互斥鎖
互斥鎖
互斥鎖就是說,當key失效的時候,讓一個執行緒讀取資料並構建到快取中,其他執行緒就先等待,直到快取構建完後重新讀取快取即可。
如果是單機系統,用JDK本身的同步工具Synchronized或ReentrantLock就可以實現,但一般來說,都達到防止快取擊穿的流量了誰還搞什麼單機系統,肯定是分散式高大上點啊,這種情況我們就可以用分散式鎖來做互斥效果。
為了你們能更懂流程,作為暖男的我還是一如既往的給你們準備了虛擬碼啦:
public String getData(String key){
String data = redisTemplate.opsForValue().get(key);
if (StringUtils.isNotEmpty(data)){
return data;
}
String lockKey = this.getClass().getName() + ":" + key;
RLock lock = redissonClient.getLock(lockKey);
try {
boolean boo = lock.tryLock(5, 5, TimeUnit.SECONDS);
if (!boo) {
// 休眠一會兒,然後再請求
Thread.sleep(200L);
data = getData(key);
}
// 讀取資料庫的資料
data = getDataByDB(key);
if (StringUtils.isNotEmpty(data)){
// 把資料構建到快取中
setDataToRedis(key,data);
}
} catch (InterruptedException e) {
// 異常處理,記錄日誌或者拋異常什麼的
}finally {
if (lock != null && lock.isLocked()){
lock.unlock();
}
}
return data;
}
當然,採用互斥鎖的方案也是有缺陷的,當快取失效的時候,同一時間只有一個執行緒讀資料庫然後回寫快取,其他執行緒都處於阻塞狀態。如果是高併發場景,大量執行緒阻塞勢必會降低吞吐量。這種情況該如何處理呢?我只能說沒什麼設計是完美的,你又想資料一致,又想保證吞吐量,哪有那麼好的事,為了系統能更加健全,必要的時候犧牲下效能也是可以採取的措施,兩者之間怎麼取捨要根據實際業務場景來決定,萬能的技術方案什麼的根本不存在。
快取雪崩
快取雪崩也是key失效後大量請求打到資料庫的異常情況,不過,跟快取擊穿不同的是,快取擊穿因為指一個熱點key失效導致的情況,而快取雪崩是指快取中大批量的資料同時過期,巨大的請求量直接落到db層,引起db壓力過大甚至當機,這也符合字面上的“雪崩”說法。
解決方案
快取雪崩的解決方案和擊穿的思路一致,可以設定key不過期或者互斥鎖的方式。
除此之外,因為是預防大面積的key同時失效,可以給不同的key過期時間加上隨機值,讓快取失效的時間點儘量均勻 ,這樣可以保證資料不會在同一時間大面積失效。
redisTemplate.opsForValue().set(Key, value, time + Math.random() * 1000, TimeUnit.SECONDS);
同時還可以結合主備快取策略來讓互斥鎖的方式更加的可靠,
主快取:有效期按照經驗值設定,設定為主讀取的快取,主快取失效後從資料庫載入最新值。
備份快取:有效期長,獲取鎖失敗時讀取的快取,主快取更新時需要同步更新備份快取。
一般來說,上面三種快取異常場景問的比較多,瞭解這幾種基本就夠了,但有些面試官可能喜歡劍走偏鋒,進一步延伸其他的異常情景做詢問,以防萬一,我們也加個菜,介紹下另外兩種常見快取異常。
快取預熱
快取預熱就是系統上線後,先將相關的資料構建到快取中,這樣就可以避免使用者請求的時候直接查庫。
這部分預熱的資料主要取決於訪問量和資料量大小,如果資料的訪問量不大的話,那麼就沒必要做預熱,都沒什麼多少請求了,直接按正常的快取讀取流程執行就好。
訪問量大的話,也要看資料的大小來做預熱措施。
資料量不大的時候,工程啟動的時候進行載入快取動作,這種資料一般可以是電商首頁的運營位之類的資訊; 資料量大的時候,設定一個定時任務指令碼,進行快取的重新整理; 資料量太大的時候,優先保證熱點資料進行提前載入到快取,並且確保訪問期間不能更改快取,比如用定時器在秒殺活動前30分鐘就把商品資訊之類的重新整理到快取,同時規定後臺運營人員不能在秒殺期間更改商品屬性。
快取降級
快取降級是指快取失效或快取伺服器掛掉的情況下,不去訪問資料庫,直接返回預設資料或訪問服務的記憶體資料。
在專案實戰中通常會將部分熱點資料快取到服務的記憶體中,類似HashMap、Guava這樣的工具,一旦快取出現異常,可以直接使用服務的記憶體資料,從而避免資料庫遭受巨大壓力。
當然,這樣的操作對於業務是有損害的,分散式系統中很容易就出現資料不一致的問題,所以,一般這種情況下,我們都優先保證從運維角度確保快取伺服器的高可用性,比如Redis的部署採用叢集方式,同時做好備份,總之,儘量避免出現降級的影響。
最後
關於快取的幾大異常處理我們就講解到這了,雖然每種異常我們都給出瞭解決的方案,但不是說這玩意直接套上就能用了。現實開發過程中還是要根據實際情況來針對快取做相應措施,比如用布隆過濾器預防快取穿透雖然很有效,但並不算特別常用,這年頭,防止惡意攻擊什麼的都是先在運維層面做限制,業務程式碼層面更多的是對引數和資料做校驗。
如果每個使用快取的地方都要考慮的這麼複雜的話,那工作量無疑會更加繁雜,過度設計只會讓程式碼維護起來也麻煩,而且實用性還不一定強,沒必要啊。程式設計師嘛,給自己增添煩惱的事情越少越好,畢竟我們最大的敵人不是996,而是那珍貴的髮量啊。
更多精彩文章歡迎關注我的公眾號,掃描下方二維碼或者微信搜尋鄙人薛某即可,回覆【電子書】還能獲取學習資料哦~~~我們下期再見!