沒遇到過這三個問題都不好意思說用過Redis
緩存是互聯(lián)網(wǎng)應(yīng)用中不可或缺的一部分。而提到緩存,就不得不提它的三個經(jīng)典問題——緩存穿透、緩存擊穿和緩存雪崩,我稱它們?yōu)榫彺鎲栴}三兄弟。
緩存的作用主要有兩個:一來提升訪問速度;二來保護數(shù)據(jù)庫。在業(yè)務(wù)量不大的時候,通常沒什么大問題。但當業(yè)務(wù)量起來以后,如果緩存使用不合理,三兄弟一定會如約而至,讓你體驗一下現(xiàn)實的殘酷。
三兄弟不來則已,一來輕則影響系統(tǒng)性能,重則直接拖垮數(shù)據(jù)庫,導(dǎo)致系統(tǒng)癱瘓。因此,我們不可掉以輕心,要防患于未然。
緩存穿透
一個請求到達服務(wù)器時,正常情況下是按照如下流程進行的。

即按照如下步驟:
查詢緩存,如果命中則返回
緩存未命中,則查詢數(shù)據(jù)庫
將從數(shù)據(jù)庫中查詢到的數(shù)據(jù)寫入緩存并返回
如果每次都是這樣按部就班的處理,倒也相安無事。但是,凡事就怕但是。但是總會有例外,假如請求方對一個(數(shù)據(jù)庫中)根本不存在的數(shù)據(jù)進行訪問,那么按照上面的流程,緩存就形同虛設(shè)了。因為不存在,所以不會被寫入緩存,這樣請求每次都會打到數(shù)據(jù)庫,這個現(xiàn)象就是所謂的「緩存穿透」了。
如果只是因為個別請求去查詢不存在的數(shù)據(jù),那其實也沒什么大事。但緩存穿透通常是伴隨一些「惡意請求」而來,通常是在短時間內(nèi)涌入大量請求。如果放任不管,就等著數(shù)據(jù)庫宕機吧。
如何解決
了解了導(dǎo)致緩存穿透的原因,那么解決方案也就明了了??梢詮膬蓚€方面下手:
緩存不存在的記錄
過濾不存在的請求
啥?不存在的記錄咋緩存?其實很簡單,如果數(shù)據(jù)庫中也查不到,那就將緩存的 value 設(shè)置成 null 即可(注意要根據(jù)業(yè)務(wù)特性設(shè)置合理的過期時間)。
過濾不存在的請求,當一個請求到達服務(wù)器,比如:
GET /api/user/1
過濾器會先判斷該資源是否存在,如果存在則放行,不存在則直接返回,從而起到保護系統(tǒng)的作用。
這種方式也有比較成熟的方案。比如布隆過濾器和布谷鳥過濾器(升級版布隆布隆過濾器)。
雙重加固
不管請求不存在的資源是有意還是無意,都不是我們想要的。所以,我們可以設(shè)定一個訪問頻率,一定時間內(nèi)頻繁(超出正常用戶的極限)訪問,可以對請求方加以限制(如 IP 限制)。另外,一些接口可以加入認證,必須登錄才能訪問。
緩存擊穿
通常情況,我們會為緩存設(shè)置一個過期時間。而如果在一個資源的緩存過期以后(或者還未來得及緩存),瞬間涌入大量查詢該資源的請求,那么這些請求就都會一股腦的奔向數(shù)據(jù)庫,這時,我們的數(shù)據(jù)庫可就慘了,可能秒秒鐘掛掉。這種情況我們稱之為緩存擊穿。
如何解決
要解決緩存擊穿也有兩種思路:
永不過期
加鎖
先看第一種,短時間內(nèi)被大量訪問的通常是熱點資源,針對這類資源我們可以不設(shè)置過期時間(永不過期),當資源有變化時通過程序去更新緩存。
再來看第二種,我們可以使用加鎖的方式(一般 JVM 級別的鎖即可)來避免擊穿。當緩存過期之后,進來的請求,先要獲得一把鎖(也就是去數(shù)據(jù)庫查詢的資格),然后再去查詢數(shù)據(jù)庫,最后將數(shù)據(jù)添加到緩存。這樣就可以保證同一時刻(一個服務(wù)實例)只會有一個請求去查庫了,其他線程等緩存有值以后,再去緩存取。
加鎖偽代碼示例:
public String getData() throws InterruptedException {
? ?// 從緩存取值
? ?String result = getFromCache();
? ?// 取到直接返回
? ?if (Objects.nonNull(result)) {
? ? ? ?return result;
? ?}
? ?
? ?// 嘗試獲取鎖
? ?if (!lock.tryLock()) {
? ? ? ?// 加鎖失敗則休息一會
? ? ? ?Thread.sleep(10);
? ? ? ?return getData();
? ?}
? ?// 加鎖成功則去數(shù)據(jù)庫取值
? ?result = getFromDB();
? ?// 取回后放入緩存
? ?setFromCache();
? ?return result;
}
緩存雪崩
緩存雪崩指的是,緩存中大量的 key 在同一時刻集體過期,導(dǎo)致大量請求涌入到數(shù)據(jù)庫。
有人把緩存服務(wù)由于一些原因不可用稱為緩存雪崩,我覺得這么叫不太合適。
你想象一下什么是雪崩,大量的雪花集體從山上往下跳就是雪崩。那么對應(yīng)到緩存的場景,我們可以把 Redis 看做是山,而 Redis 里的 key 就是雪花。Redis 中大量的 key 同時失效,就好比是山上大量的雪花同時往下掉是一樣的。所以雪崩用來比喻大量 key 集中失效的情況明顯更貼切。而緩存服務(wù)掛掉應(yīng)該屬于緩存服務(wù)故障,可以采取緩存集群的方式來提高可用性。
如何解決
要解決緩存雪崩的問題,有兩種思路:
分散過期時間
永不過期
分散過期時間很容易想到,既然雪崩是因為 key 集體過期導(dǎo)致的,那么把它們過期的時間分散開就可以避免這種問題了。
另一種思路,跟解決緩存擊穿一樣,將緩存設(shè)置為永不過期。
永不過期的方案有一定的局限性,要看具體的業(yè)務(wù),不能粗暴的將所有緩存都設(shè)置成不過期。
總結(jié)
每種技術(shù)方案都有其適用的業(yè)務(wù)場景,也都有其局限性。沒有一個方案能夠應(yīng)對所有問題,合適即是好。但從上面的方案中還是能看到一些通用的思想的,比如:盡早返回。咋理解呢?就是讓調(diào)用鏈盡量的短,能攔在應(yīng)用服務(wù)之前的絕不放行(布隆過濾);能從緩存取到的絕不再去查庫。
- 完 -
更多獨家精彩內(nèi)容盡在我的新書《Spring Boot趣味實戰(zhàn)課》中。
