今天來聊聊redis的緩存穿透、擊穿、雪崩以及解決方案,其中解決方案包括類似於布隆過濾器這種網上一搜一大片但是實際生產部署有一定複雜度的,也有基於spring註解通過一行代碼就能解決的,其中各有優劣,咱們根據實際需要選型;
接下來會從穿透、擊穿、雪崩的基本概念、各種問題對應的解決方案以及方案的具體實施來一一介紹
緩存穿透
概念
- 緩存穿透指某一特定時間批量請求打進來並訪問了緩存和數據庫都沒有的key,此時會直接穿透緩存直達數據庫,從而造成數據庫瞬時壓力倍增導致響應速度下降甚至崩潰的風險;
解決方案
一、通過布隆過濾器解決
-
原理:將所有需要緩存的key通過hash算法全部放到布隆過濾器將對應下標對應的值置成1,這樣當請求進來時先去布隆過濾器裏找,發現對應index的key是1則去緩存拿數據爲0則直接返回,這樣就避免了去數據庫查詢
-
優缺點:布隆過濾器底層通過redis的bitMap實現是基於位的操作,所以效率高;但是需要提前將key存入過濾器,這大大增加了前置成本;並且key到index的映射通過hash算法,那麼必然會出現hash碰撞的問題;
二、通過key-null
- 原理:當請求進來從數據庫查詢回來的值爲空時,將對應的null值也存入redis,這樣下次請求時redis裏已經有數據了就不會再懟到數據庫了
- 優缺點:思路清晰,操作簡單,但是極端情況下會有N多key在數據庫和緩存中都沒有,那這種實現就造成了redis裏有過多的垃圾數據,浪費了內存空間
三、增強前置判斷
- 原理:查詢前在接口層做權限、邏輯校驗,儘可能多的判斷key的合法性
- 優缺點:使用成本低但並不足以規避掉緩存穿透的風險,所以建議只做附加的解決方案使用
緩存擊穿
概念
- 緩存擊穿指某一特定時間批量請求打進來對一個特定的值進行查詢並訪問了緩存中沒有但是數據庫裏存在的key,此時會直接擊穿緩存直達數據庫,從而造成數據庫瞬時壓力倍增導致響應速度下降甚至崩潰的風險;需要注意的是,這裏說的緩存中沒有包含兩層意思,一是緩存中本身沒有,二十緩存中有但是過期了(少量熱點key過期)
解決方案
一、設置key的過期時間隨機
- 原理:在像redis批量設置key的時候儘量做到過期時間的隨機性,以此避免某一時間會有批量key過期導致的緩存擊穿
- 優缺點:雖然解決了緩存中同一時間批量key過期導致的擊穿問題但是並不能解決緩存中壓根就不存在key而造成的擊穿
二、通過分佈式鎖解決
- 原理:在查詢數據庫的時候加一個分佈式鎖,使得某一特定時間內只有一個請求訪問數據庫,訪問成功後將查詢到的值緩存近redis,這樣在下次訪問時就不會造成擊穿的現象了;
需要注意的是,針對非海量請求的業務這裏加單機鎖也問題不大,無非就是將同一時間只有一個請求到數據改成了同一時間只有集羣數量的請求到數據庫,問題應該也不大
- 優缺點:無論是緩存中本身就沒有還是緩存中的key過期了使用加鎖的方式都能解決緩存擊穿的問題,但是卻增加了加鎖的成本
三、設置熱點key永不過期
- 原理:針對熱點key批量過期造成的穿透問題,將key設置成永不過期就能解決
- 優缺點:與一一樣只能解決key過期造成的擊穿不能解決緩存中沒有key造成的擊穿,並且熱點key的確認也是門學問,並不總能保證設置的熱點key不遺漏
四、將key部署在不同的實例
- 原理:針對集羣部署的redis,將熱點key分開部署也能避免過期造成的擊穿問題
- 優缺點:只有集羣才能使用,且也只能解決key過期造成的擊穿問題,並不總能保證設置的熱點key不遺漏
緩存雪崩
概念
- 與擊穿相比雪崩表示過期或壓根不存在的key是大量而不是一個或少量,導致大量請求落到數據庫;
解決方案
一、通過設置過期時間隨機來解決
- 設置key的時候加上隨機過期時間,儘量減少同一時間過期key的數量;
二、設置鎖
- 從緩存訪問key開始就加鎖,當緩存沒有則去數據庫查,查晚塞入緩存最後釋放鎖;如果是單機應用則直接加虛擬機鎖,集羣部署的話則需使用分佈式鎖
小結
以上的實現方案可根據自己的業務需求靈活實現,其中個人認爲布隆過濾器的落地相對複雜,成本也會更高,所以我們實際工作中並沒有使用,而是用了基於spring-cache的@Cacheable註解,某種程度上可以說是一行代碼解決了緩存擊穿、穿透問題,下面說下@Cacheable註解
@Cacheable註解
一、介紹
- @Cacheable註解是一個spring3.1後引入的緩存技術,可通過在業務代碼添加spring的緩存註解來實現緩存對象和方法的效果;而不用在業務代碼裏顯示的進行set操作,大大方便了我們的日常開發
二、使用
- 1、在springboot的啓動類添加
@EnableCaching
註解開啓緩存,否則下面的註解不會生效 - 2、直接上圖,結合圖來說明則一清二楚
說明
1、通過在方法上添加@Cacheable註解表示此方法需要走緩存,其中cacheNames的值加上方法的入參爲唯一標識,作爲緩存中的key而value則爲方法的返回值;需要注意的是,當方法的入參和返回值是對象時則需要實現序列化(Serializable)接口
2、sync參數默認缺省值爲false,表示是否是一個線程去數據庫查詢;所以當值爲true時表示只有一個線程會打到數據庫,這就解決了緩存擊穿的問題;而針對緩存穿透,之前看到資料說需要添加spring.cache.redis.cache-null-values=true的配置纔可以緩存空值,但我通過測試發現不論方法返回值是對象還是普通類型空值均可被緩存,我使用的spring版本是5.3.12;所以上面的代碼也順帶解決了緩存穿透的問題
3、如果項目併發量小,不需要考慮穿透、擊穿這些問題,可以直接寫成@Cacheable("testCache")使sync缺省即可
三、總結
- 個人認爲,針對@Cacheable已經足以解決實際開發中穿透、擊穿的問題了;當然,如果假設一個集羣有10000個實例,使用sync理論上也會有10000個請求同時抵達數據庫從而影響數據庫性能甚至崩潰,但是這種情況被我們碰到的情況很少,甚至可以說接觸不到;但是如果真接觸到了只能通過添加分佈式鎖去解決了