更多內容,歡迎關注微信公眾號:全菜工程師小輝~
緩存穿透
緩存系統,一般流程都是按照key去查詢緩存,如果不存在對應的value,就去后端系統(例如:持久層數據庫)查找。如果key對應的value是一定不存在的,并且對該key并發請求量很大,就會對后端系統造成很大的壓力,這就叫做緩存穿透。
正常請求:
正常請求
緩存擊穿時:
緩存擊穿
如何避免
1. 緩存空結果
對查詢結果為空的情況進行緩存,緩存時間設置短一點,或者該key對應的數據insert了之后清理緩存。
2. 布隆過濾器
采用布隆過濾器,guava有實現api,或者使用redis的bitmap。將所有可能存在的數據哈希到一個足夠大的bitmap中,一個一定不存在的數據會被這個bitmap攔截掉,從而避免了對底層存儲系統的查詢壓力。布隆過濾器對于固定的數據可以起到很好的效果,但是對于頻繁更新的數據,布隆過濾器的構建會面臨很多問題。另外布隆過濾器是有判斷誤差的,網上有很多詳細的介紹,請讀者自行搜索即可。
布隆過濾器
緩存雪崩
當緩存服務器重啟或者大量緩存集中在某一個時間段失效,這樣在失效的時候,也會給后端系統(比如DB)帶來很大壓力。
如何避免
1. 互斥鎖
在緩存失效后,通過加鎖或者隊列來控制讀數據庫寫緩存的線程數量。比如對某個key只允許一個線程查詢數據和寫緩存,其他線程等待。
互斥鎖
如果是單機,可以用synchronized或者lock來處理,如果是分布式環境就需要使用分布式鎖。
使用互斥鎖,代碼如下,僅適用redis2.6.1以后支持setnx的版本。在緩存失效的時候(判斷拿出來的值為空),不是立即去load db,而是先使用redis的setnx操作去set一個mutex key。當操作返回成功時,再進行load db的操作并回設緩存,否則,就重試整個get緩存的方法。
互斥鎖
public String get(key) { List<String> resultList = (List<String>)redisTemplate.opsForValue().get(key); if(CollectionUtils.isEmpty(resultList)){ final String mutexKey = key + "_lock"; boolean isLock = (Boolean) redisTemplate.execute(new RedisCallback() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { //只在鍵key不存在的情況下,將鍵key的值設置為value,若鍵key已經存在,則 SETNX 命令不做任何動作 //命令在設置成功時返回 1 , 設置失敗時返回 0 return connection.setNX(mutexKey.getBytes(),"1".getBytes()); } }); if(isLock){ //設置成1秒過期 redisTemplate.expire(mutexKey, 1000, TimeUnit.MILLISECONDS); resultList = getValueBySql(key); redisTemplate.opsForValue().set(key, resultList, 1000, TimeUnit.SECONDS); redisTemplate.delete(mutexKey); }else{ //線程休息50毫秒后重試 Thread.sleep(50); retryCount--; System.out.println("=====進行重試,當前次數:" + retryCount); if(retryCount == 0){ System.out.println("====這里發郵件或者記錄下獲取不到數據的日志,并為key設置一個空置防止重復獲取"); List<String> list = Lists.newArrayList("no find"); redisTemplate.opsForValue().set(key, list, 1000, TimeUnit.SECONDS); return list; } return getCacheSave2(key,retryCount); } } return resultList; }
2. 設置隨機過期時間
不同的key,設置不同的過期時間,讓緩存失效時間分散開,比如可以在原有的失效時間基礎上增加一個隨機值,比如1-5分鐘隨機,這樣每一個緩存的過期時間的重復率就會降低。
3. 設置二級緩存
做二級緩存,A1為原始緩存,A2為拷貝緩存,A1失效時,可以訪問A2,A1緩存失效時間設置為短期,A2設置為長期
4. “永遠不過期”
“永遠不過期”包含兩層意思:
- 從redis上看,確實沒有設置過期時間,這就保證了,不會出現熱點key過期問題,也就是“物理”不過期。
- 從功能上看,把過期時間存在key對應的value里,如果發現要過期了,通過一個后臺的異步線程進行緩存的構建,也就是“邏輯”過期。
“永遠不過期”
這種方法對于性能非常友好,唯一不足的就是構建緩存時候,其余線程(非構建緩存的線程)可能訪問的是老數據,但是對于一般的互聯網功能來說這個還是可以忍受。
緩存預熱
有效應對緩存的擊穿和雪崩的一種方式是緩存預熱。
緩存預熱就是系統上線前,將相關的緩存數據直接加載到緩存系統。這樣就可以避免在用戶請求的時候,先查詢數據庫,然后再將數據緩存的問題,用戶直接查詢事先被預熱的緩存數據。
解決思路
- 直接寫個緩存刷新頁面,上線時手工操作下。
- 數據量不大,可以在項目啟動的時候自動進行加載。
- 定時刷新緩存。
限流
有效應對緩存的擊穿和雪崩的另一種方式是限流。
在緩存失效后,通過隊列來控制讀數據庫寫緩存的線程數量。比如對某個key只允許一個線程查詢數據和寫緩存,其他線程等待。
常見的限流算法
- 固定時間窗口算法(計數器)
- 滑動時間窗口算法
- 令牌桶算法
- 漏桶算法
有關限流算法的詳細介紹,請點擊查看高并發系統的限流算法與實現
總結
緩存穿透、擊穿和雪崩是以預防為主、補救為輔,而在應對緩存的問題其實也沒有一個完全完美的方案,只有最適合自己業務系統的方案。
更多內容,歡迎關注微信公眾號:全菜工程師小輝~