Redis緩存能夠有效地加速應用的讀寫速度,就DB來說,Redis成績已經很驚人了,且不說memcachedb和Tokyo Cabinet之流,就說原版的memcached,速度似乎也只能達到這個級別。今天主要講講在使用Redis時經常遇到的幾個問題。緩存雪崩、緩存擊穿、緩存穿透、緩存預熱、緩存更新、緩存降級。
v緩存雪崩
緩存雪崩,是指在某一個時間段,緩存集中過期失效。所有原本應該訪問緩存的請求都去查詢數據庫了,而對數據庫CPU和內存造成巨大壓力,嚴重的會造成數據庫宕機。從而形成一系列連鎖反應,造成整個系統崩潰。
緩存雪崩示意圖:
緩存失效時的雪崩效應對底層系統的沖擊非常致命,那么應對Redis緩存雪崩有哪些方案呢?
1.1 加鎖或者隊列
可以考慮用加鎖或者隊列的方式防止大量線程對數據庫的一次性進行讀寫,避免緩存失效時對數據庫造成的巨大沖擊。
以上效果還可以考慮接入Redis鎖實現,具體可以參考《SpringBoot進階教程(二十七)整合Redis之分布式鎖》 加鎖或者隊列都是一個非常淺顯的辦法。雖然能夠在一定的程度上緩解了數據庫的壓力,但同時也極大的降低了系統的吞吐量。
1.2 協調Redis過期時間
分析用戶行為,盡量讓緩存失效的時間均勻分布,最次也得隨機分布,尤其是一些訪問大的接口
@Override public UserDetails getUserInfoById(Integer uid){ String key = String.format("user_info_id:%d",uid); UserDetails userDetails = (UserDetails)templateRedis.opsForValue().get(key); if(userDetails != null){ return userDetails; }else{ userDetails = userDetailsMapper.getUserDetailsByUid(uid); Random random = new Random(); int time = 600; // type: 1: 大V用戶 2: 網紅 3: 普通用戶 if(userDetails != null){ if(userDetails.getType() == 1){ time = 3600 + random.nextInt(3600); // 如果有其他邏輯 }else if(userDetails.getType() == 2){ time = 1200 + random.nextInt(1200); // 如果有其他邏輯 }else{ // 如果有其他邏輯 } redisTemplate.opsForValue().set(key, userDetails, time, TimeUnit.SECONDS); } } return userDetails; }
這里主要還是結合業務場景讓緩存失效的時間均勻分布,比如上面這段代碼中,大V用戶和網紅用戶一般粉絲都是上百萬,所以可以緩存長點的時間也是可以的。
1.3 二級緩存
做二級緩存,A1為原始緩存,A2為拷貝緩存,A1失效時,可以訪問A2,A1緩存失效時間設置為短期,A2設置為長期。
1.4 保證緩存層服務高可用性
保證緩存層服務高可用性。如果緩存層設計成高可用的,即使個別節點、個別機器、甚至是機房宕掉,依然可以提供服務。
關於這一塊,可以看看前面寫到的《SpringBoot進階教程(三十)整合Redis之Sentinel哨兵模式》和《詳解Redis Cluster集群》。
1.5 依賴隔離組件為后端限流並降級。
需要對重要的資源(例如Redis、MySQL、外部接口)都進行隔離,讓每種資源都單獨運行在自己的線程池中。即使個別資源出現了問題,對其他服務沒有影響。但是線程池如何管理,比如如何關閉資源池、開啟資源池、資源池閥值管理,這些做起來還是相當復雜的。
v緩存穿透
緩存穿透是指查詢一個一定不存在的數據。由於緩存不命中,並且出於容錯考慮,如果從數據庫查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到數據庫去查詢,失去了緩存的意義。
緩存穿透示意圖:
如何解決緩存穿透?對應的幾個參考方案:
2.1 布隆過濾器(BloomFilter)
采用布隆過濾器,將所有可能存在的數據哈希到一個足夠大的bitmap中,一個一定不存在的數據會被這個bitmap攔截掉,從而避免了對底層存儲系統的查詢壓力。由於請求的參數是不合法的(每次都請求不存在的參數),於是我們可以使用布隆過濾器(BloomFilter)或者壓縮filter提前攔截,不合法就不讓這個請求到數據庫層。
BloomFilter就類似於一個hash set,用於快速判某個元素是否存在於集合中,其典型的應用場景就是快速判斷一個key是否存在於某容器,不存在就直接返回。布隆過濾器的關鍵就在於hash算法和容器大小,
2.2 將空對象記錄在緩存中。
如果數據庫返回信息為null,也可以將這個空對象設置到緩存里邊去。下次再請求的時候,就可以從緩存里邊獲取了,將空對象設置一個較短的過期時間。如1.1中的代碼示例所示。
v緩存擊穿
緩存擊穿指的是熱點key在某個特殊的場景時間內恰好失效了,恰好有大量並發請求過來了,造成DB壓力。
其實緩存擊穿和緩存雪崩從概念上來講差不多,只是緩存擊穿是某些熱點key,而雪崩指的是大規模的key。
如何解決緩存擊穿,對應的幾個參考方案:
3.1 與1.1中類似,通過加鎖或者隊列的方式防止大量請求透過redis到DB中。
3.2 對於一些熱點key,過期時間可以無限調長
將熱點key過期時間無限調長,然后通過job服務來管理這些熱點key不會過期,保證熱點key(尤其是像排行榜、首頁熱度等需要大量計算的熱點key)的穩定性。需要注意的是,job服務本身也存在不穩定性,比如部署job的服務掛了之類的。這里可以看看之前的一篇文章。《詳解Supervisor進程守護監控》
v緩存降級
緩存降級是指當訪問量劇增、服務出現問題(如響應時間慢或不響應)或非核心服務影響到核心流程的性能時,仍然需要保證服務還是可用的,即使是有損服務。系統可以根據一些關鍵數據進行自動降級,也可以配置開關實現人工降級。
降級的最終目的是保證核心服務可用,即使是有損的。而且有些服務是無法降級的(如加入購物車、結算)。
參加過去年天貓雙11的朋友應該很清楚的能感受到降級,當時被吐槽的最狠的降級應該就是加入購物車,在結算的時候無法更改收貨地址。只能使用默認收貨地址,這得成就多少"前男友前女友"啊。
在進行降級之前要對系統進行梳理,看看系統是不是可以丟卒保帥;從而梳理出哪些必須誓死保護,哪些可降級;比如可以參考日志級別設置預案:
ps:強於"馬爸爸",在雙11的海量並發面前,也得降級,無可厚非。只是大家在做降級的時候,一定得考慮好取舍。這反而是降級最大的難度。
v緩存預熱
上初中第一次做化學實驗的時候,大家就知道試管加熱前需要先預熱。緩存預熱也是一個比較常見的概念,緩存預熱就是系統上線后,將相關的緩存數據直接加載到緩存系統。這樣就可以避免在用戶請求的時候,先查詢數據庫,然后再將數據緩存的問題。用戶直接查詢事先被預熱的緩存數據。
緩存預熱思路:
對於一些計算量非常大的接口,緩存預熱肯定是得有的。
v緩存更新
除了緩存服務器自帶的緩存失效策略之外(Redis默認的有6中策略可供選擇),我們還可以根據具體的業務需求進行自定義的緩存淘汰,常見的策略有兩種:
上面的這幾個方案各有優劣,第一種的缺點是維護大量緩存的key是比較麻煩的;第二種人工成本太高;第三種的缺點就是每次用戶請求過來都要判斷緩存失效,邏輯相對比較復雜。具體使用場景還得結合業務來區分對待。
v博客總結
Redis的出現確實很大程度上解決了sql的壓力,善用Redis的各種機制已經成為一個必不可少的技能之一。
v源碼地址
https://github.com/toutouge/javademo/tree/master/hellospringboot
作 者:請叫我頭頭哥
出 處:http://www.cnblogs.com/toutou/
關於作者:專注於基礎平台的項目開發。如有問題或建議,請多多賜教!
版權聲明:本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
特此聲明:所有評論和私信都會在第一時間回復。也歡迎園子的大大們指正錯誤,共同進步。或者直接私信我
聲援博主:如果您覺得文章對您有幫助,可以點擊文章右下角【推薦】一下。您的鼓勵是作者堅持原創和持續寫作的最大動力!