文章目錄
- 概述
- 一、緩存穿透
- 1.1 緩存穿透是什么
- 1.2 解決方案
- 二、緩存擊穿
- 2.1 緩存擊穿是什么
- 2.2 解決方案
- 三、緩存雪崩
- 3.1 緩存雪崩是什么
- 3.2 解決方案
- 四、拓展
- 4.1 緩存預熱
- 4.2 緩存降級
- 五、結語
把今天最好的表現當作明天最新的起點…….~
概述
??在實際的業務場景中,Redis 一般和其他數據庫搭配使用,比如和關系型數據庫 MySQL 配合使用,用來減輕后端數據庫的壓力。Redis 會把 MySQL 中經常被查詢的數據緩存起來,比如熱點數據,這樣當用戶來訪問的時候,就不需要到 MySQL 中去查詢,而是直接獲取 Redis 中的緩存數據,從而降低了后端數據庫的讀取壓力。如果說用戶查詢的數據在 Redis 沒有找到,那么用戶的查詢請求就會被轉到 MySQL 數據庫。當 MySQL 將查詢到的數據返回給客戶端時,同時也會將數據緩存到 Redis 中,這樣用戶再次讀取時,就可以直接從 Redis 中獲取數據。流程圖如下所示:
??在使用 Redis 作為緩存數據庫的過程中,有時也會遇到一些棘手問題,比如常見緩存穿透、緩存擊穿和緩存雪崩等問題,如下圖所示。本文中將對這些問題做簡單地說明,并且提供有效的解決方案。
一、緩存穿透
1.1 緩存穿透是什么
??一般的緩存系統,都是按照key去緩存查詢,如果不存在對應的value,那么就去數據庫去查找。當用戶查詢某個數據時,Redis 中不存在該數據,也就是緩存沒有命中,此時查詢請求就會轉向數據庫,結果發現數據庫中也不存在該數據,數據庫只能返回一個空對象(相當于進行了兩次無用的查詢)。用戶拿不到數據時,就會一直發請求查詢數據庫,這樣會對數據庫的訪問造成很大的壓力。如果這種類請求非常多,或者用戶利用這種請求進行惡意攻擊,就會給數據庫造成很大壓力,甚至于崩潰,這種現象就叫緩存穿透。
??這種現象的原因其實很好理解,當客戶端訪問不存在的數據時,先請求 Redis,但是此時 Redis 中并沒有數據,此時會訪問到數據庫,但是數據庫中也沒有數據,這個數據穿透了緩存,直擊數據庫。我們都知道數據庫能夠承載的并發不如 Redis 這么高,如果大量的請求同時過來訪問這種不存在的數據,這些請求就都會訪問到數據庫。
1.2 解決方案
??簡單的解決方案就是當Redis、數據庫中都沒有值返回空對象時, 可以在 Redis 中存放一個空值,同時為其設置一個過期時間。這樣,當用戶再次發起相同請求訪問這個不存在的數據,那么就會從緩存中拿到一個空對象,用戶的請求被阻斷在了緩存層。這樣就可以減少重復查詢空值引起的系統壓力增大,從而從而保護了后端數據庫。示例代碼如下:
private String queryMessager(String key){// 從緩存中獲取數據String message = getFromCache(key);// 如果緩存中沒有 從數據庫中查找if(StringUtils.isBlank(message)){message = getFromDb(key);// 如果數據庫中也沒有數據 就設置短時間的緩存if(StringUtils.isBlank(message)){// 設置緩存時間(緩存的key,緩存的值,失效時間:單位秒)redisClient.setNxEx(key,null,60);} else {redisClient.setNxEx(key,message,1800);}}return message;
}
??這種做法雖然優化了緩存穿透問題,但也存在一些問題。雖然請求進不了數據庫,但是會占用 Redis 的緩存空間。而大量的空緩存導致資源的浪費,也有可能導致 Redis 和數據庫中的數據不一致。
二、緩存擊穿
2.1 緩存擊穿是什么
??我們的業務通常會有幾個數據會被頻繁地訪問,比如秒殺活動,這類被頻地訪問的數據被稱為熱點數據。比如某個熱點數據,它無時無刻都在接受大量的并發訪問,如果在某一時刻忽然過期了,此時大量的請求訪問了該熱點數據,就無法從緩存中讀取,導致大量的并發請求直接訪問數據庫,就像在一個完好無損的桶上鑿開了一個洞,引起數據庫壓力瞬間增大,這種現象被稱為緩存擊穿。
??緩存擊穿一般出現在高并發系統中,是大量并發用戶同時請求到緩存中沒有但數據庫中有的數據,也就是同時讀緩存沒讀到數據,又同時去數據庫去取數據。由于請大量請求同時過來,來不及更新緩存就全部打到數據庫那邊,引起數據庫壓力瞬間增大。
2.2 解決方案
- 將熱點數據設置加上互斥鎖
-
此方法只允許一個線程重建緩存,其他線程等待重建緩存的線程執行完,重新從緩存獲取數據即可。當第一個數據庫查詢請求發起后,就將緩存中該數據上鎖;此時到達緩存的其他查詢請求將無法查詢該字段,從而被阻塞等待;當第一個請求完成數據庫查詢,并將數據更新值緩存后,釋放鎖;此時其他被阻塞的查詢請求將可以直接從緩存中查到該數據。
private ReentrantLock reentrantLock = new ReentrantLock(); public static String getData(String key) throws InterruptedException {// 從 Redis 查詢數據String result = getDataByKey(key);// 參數校驗if (StringUtils.isBlank(result)) {// 獲取鎖if (reentrantLock.tryLock()) {// 去數據庫查詢result = getDataByDB(key);// 校驗if (StringUtils.isNotBlank(result)) {// 搞進緩存setDataToKey(key, result);}// 釋放鎖,正常會在finally中釋放reentrantLock.unlock();} else {// 稍等一下Thread.sleep(100L);result = getData(key);}}return result; }
-
當某一個熱點數據失效后,只有第一個數據庫查詢請求發往數據庫,其余所有的查詢請求均被阻塞,從而保護了數據庫。但是,由于采用了互斥鎖,其他請求將會阻塞等待,可能會存在死鎖和線程池阻塞的風險,此時系統的吞吐量將會下降,這需要結合實際的業務考慮是否允許這么做。
-
- 將熱點數據設置為永遠不過期
-
當向緩存中存儲這些數據的時候,可以將他們的緩存失效時間錯開,這樣能夠避免同時失效。如在一個基礎時間上加/減一個隨機數,從而將這些緩存的失效時間錯開。
private void setRandomTimeForReidsKey(String redisKey,String value){//隨機函數Random rand = new Random();//隨機獲取30分鐘內(30*60)的隨機數int times = rand.nextInt(1800);//設置緩存時間(緩存的key,緩存的值,失效時間:單位秒)redisClient.setNxEx(redisKey,value,times); }
-
這種方案由于沒有設置真正的過期時間,實際上已經不存在熱點key產生的一系列危害,但是會存在數據不一致的情況,同時代碼復雜度會增大。
-
三、緩存雪崩
3.1 緩存雪崩是什么
??通常,為了保證 Redis 中的數據與數據庫中的數據一致性,通常會給 Redis 里的數據設置過期時間。當緩存數據過期后,用戶訪問的數據如果不在 Redis 里,業務系統需要重新生成緩存,因此就會訪問數據庫,并將數據更新到 Redis 里,這樣后續請求都可以直接命中緩存。
??但當Redis 故障宕機或者緩存中大批量的數據同一時間過期(失效),而此時數據訪問量又非常大,無法在 Redis 中處理,于是全部直接訪問數據庫,從而導致數據庫壓力突然暴增,嚴重時甚至可能導致數據庫崩潰。就像雪崩一樣,引發一系列連鎖效應,從而波及整個系統崩潰,這種現象被稱為緩存雪崩。如下圖所示:
??假設當時每秒6000個請求,本來緩存在可以扛住每秒5000個請求,但是緩存當時所有的Key都失效了。此時1秒6000個請求全部落數據庫,數據庫必然扛不住,可能DBA都沒反應過來就直接掛了,即便是重啟數據庫,但是數據庫立馬又被新的流量給打死了。以秒殺系統為例,圖示說明:
??它和緩存擊穿不同,緩存擊穿是在并發量特別大時,某一個熱點 key 突然過期,而緩存雪崩則是大量的 key 同時過期,因此它們根本不是一個量級。
3.2 解決方案
??出現上述情況的常見原因主要有以下兩點:
- 大量緩存數據同時過期,導致本應請求到緩存的需重新從數據庫中獲取數據。
- Redis 本身出現故障,無法處理請求,那自然會再請求到數據庫那里。
??針對上面出現故障的情況,可以從以下幾點出發解決:
- 事前:構建高可用的集群,實現主 Redis 實例掛掉后,能有其他從庫快速切換為主庫,繼續提供服務,避免全盤崩潰。
- 事中:在往 Redis 存數據時,可以通過隨機、微調、均勻設置等方式設置過期時間,這樣可以保證數據不會在同一時間大面積失效。如果事情已經發生了,那就要為了防止數據庫被大量的請求搞崩潰,可以采用服務熔斷或者請求限流的方法。當然服務熔斷相對粗暴一些,停止服務直到redis服務恢復;而請求限流相對溫和一些,保證一些請求可以處理,不過還是看具體業務情況選擇合適的處理方案。
- 事后:redis持久化,一旦重啟,自動從磁盤上加載數據,快速恢復緩存數據。
四、拓展
4.1 緩存預熱
??緩存預熱就是系統上線前后,將相關的緩存數據直接加載到緩存系統中去,而不依賴用戶。這樣就可以避免在用戶請求的時候,先查詢數據庫,然后再將數據緩存的問題。用戶直接查詢事先被預熱的緩存數據,這樣可以避免那么系統上線初期,對于高并發的流量,都會訪問到數據庫中, 對數據庫造成流量的壓力。根據數據不同量級,可以有以下幾種做法:
- 數據量不大:項目啟動的時候自動進行加載。
- 數據量較大:后臺定時刷新緩存。
- 數據量極大:只針對熱點數據進行預加載緩存操作。
4.2 緩存降級
??緩存降級是指當緩存失效或緩存服務出現問題時,為了防止緩存服務故障,導致數據庫跟著一起發生雪崩問題,所以也不去訪問數據庫,但因為一些原因,仍然想要保證服務還是基本可用的,雖然肯定會是有損服務。因此,對于不重要的緩存數據,我們可以采取服務降級策略。一般做法有以下兩種:
- 直接訪問內存部分的數據緩存。
- 直接返回系統設置的默認值。
五、結語
??Redis 緩存異常會面臨的三個問題:緩存雪崩、擊穿和穿透。其中,緩存雪崩和緩存擊穿主要原因是數據不在緩存中,而導致大量請求訪問了數據庫,數據庫壓力驟增,容易引發一系列連鎖反應,導致系統奔潰。不過,一旦數據被重新加載回緩存,應用又可以從緩存快速讀取數據,不再繼續訪問數據庫,數據庫的壓力也會瞬間降下來。因此,緩存雪崩和緩存擊穿應對的方案比較類似。而緩存穿透主要原因是數據既不在緩存也不在數據庫中。因此,緩存穿透與緩存雪崩、擊穿應對的方案不太一樣。
??Redis 緩存在互聯網中至關重要,可以很大的提升系統效率。 本文介紹的緩存異常以及解決思路有可能不夠全面,但也提供相應的解決思路和代碼大體實現,希望可以為大家提供一些遇到緩存問題時的解決思路。如果有不足的地方,也請幫忙指出,大家共同進步。