前言
對于從事後端開發的同學來說,
緩存
已經變成的項目中必不可少的技術之一。
沒錯,緩存能給我們系統顯著的提升性能。但如果你使用不好,或者缺乏相關經驗,它也會帶來很多意想不到的問題。
今天我們一起聊聊如果在項目中引入了緩存,可能會給我們帶來的下面這三大問題。看看你中招了沒?
1. 緩存穿透問題
大部分情況下,加緩存的目的是:為了減輕資料庫的壓力,提升系統的性能。
1.1 我們是如何用緩存的?
一般情況下,如果有使用者請求過來,先查緩存,如果緩存中存在資料,則直接傳回。如果緩存中不存在,則再查資料庫,如果資料庫中存在,則将資料放入緩存,然後傳回。如果資料庫中也不存在,則直接傳回失敗。
流程圖如下:
上面的這張圖小夥們肯定再熟悉不過了,因為大部分緩存都是這樣用的。
1.2 什麼是緩存穿透?
但如果出現以下這兩種特殊情況,比如:
- 使用者請求的id在緩存中不存在。
- 惡意使用者僞造不存在的id發起請求。
這樣的使用者請求導緻的結果是:每次從緩存中都查不到資料,而需要查詢資料庫,同時資料庫中也沒有查到該資料,也沒法放入緩存。也就是說,每次這個使用者請求過來的時候,都要查詢一次資料庫。
圖中标紅的箭頭表示每次走的路線。
很顯然,緩存根本沒起作用,好像被
穿透
了一樣,每次都會去通路資料庫。
這就是我們所說的:
緩存穿透問題
。
如果此時穿透了緩存,而直接資料庫的請求數量非常多,資料庫可能因為扛不住壓力而挂掉。嗚嗚嗚。
那麼問題來了,如何解決這個問題呢?
1.3 校驗參數
我們可以對使用者id做檢驗。
比如你的合法id是15xxxxxx,以15開頭的。如果使用者傳入了16開頭的id,比如:16232323,則參數校驗失敗,直接把相關請求攔截掉。這樣可以過濾掉一部分惡意僞造的使用者id。
1.4 布隆過濾器
如果資料比較少,我們可以把資料庫中的資料,全部放到記憶體的一個map中。
這樣能夠非常快速的識别,資料在緩存中是否存在。如果存在,則讓其通路緩存。如果不存在,則直接拒絕該請求。
但如果資料量太多了,有數千萬或者上億的資料,全都放到記憶體中,很顯然會占用太多的記憶體空間。
那麼,有沒有辦法減少記憶體空間呢?
答:這就需要使用
布隆過濾器
了。
布隆過濾器底層使用bit數組存儲資料,該數組中的元素預設值是0。
布隆過濾器第一次初始化的時候,會把資料庫中所有已存在的key,經過一些列的hash算法(比如:三次hash算法)計算,每個key都會計算出多個位置,然後把這些位置上的元素值設定成1。
之後,有使用者key請求過來的時候,再用相同的hash算法計算位置。
- 如果多個位置中的元素值都是1,則說明該key在資料庫中已存在。這時允許繼續往後面操作。
- 如果有1個以上的位置上的元素值是0,則說明該key在資料庫中不存在。這時可以拒絕該請求,而直接傳回。
使用布隆過濾器确實可以解決緩存穿透問題,但同時也帶來了兩個問題:
- 存在誤判的情況。
- 存在資料更新問題。
先看看為什麼會存在誤判呢?
上面我已經說過,初始化資料時,針對每個key都是通過多次hash算法,計算出一些位置,然後把這些位置上的元素值設定成1。
但我們都知道hash算法是會出現hash沖突的,也就是說不同的key,可能會計算出相同的位置。
上圖中的下标為2的位置就出現了hash沖突,key1和key2計算出了一個相同的位置。
如果有幾千萬或者上億的資料,布隆過濾器中的hash沖突會非常明顯。
如果某個使用者key,經過多次hash計算出的位置,其元素值,恰好都被其他的key初始化成了1。此時,就出現了誤判,原本這個key在資料庫中是不存在的,但布隆過濾器确認為存在。
如果布隆過濾器判斷出某個key存在,可能出現誤判。如果判斷某個key不存在,則它在資料庫中一定不存在。
通常情況下,布隆過濾器的誤判率還是比較少的。即使有少部分誤判的請求,直接通路了資料庫,但如果通路量并不大,對資料庫影響也不大。
此外,如果想減少誤判率,可以适當增加hash函數,圖中用的3次hash,可以增加到5次。
其實,布隆過濾器最緻命的問題是:如果資料庫中的資料更新了,需要同步更新布隆過濾器。但它跟資料庫是兩個資料源,就可能存在資料不一緻的情況。
比如:資料庫中新增了一個使用者,該使用者資料需要實時同步到布隆過濾。但由于網絡異常,同步失敗了。
這時剛好該使用者請求過來了,由于布隆過濾器沒有該key的資料,是以直接拒絕了該請求。但這個是正常的使用者,也被
攔截
很顯然,如果出現了這種正常使用者被攔截了情況,有些業務是無法容忍的。是以,布隆過濾器要看實際業務場景再決定是否使用,它幫我們解決了緩存穿透問題,但同時了帶來了新的問題。
1.5 緩存空值
上面使用布隆過濾器,雖說可以過濾掉很多不存在的使用者id請求。但它除了增加系統的複雜度之外,會帶來兩個問題:
- 布隆過濾器存在誤殺的情況,可能會把少部分正常使用者的請求也過濾了。
- 如果使用者資訊有變化,需要實時同步到布隆過濾器,不然會有問題。
是以,通常情況下,我們很少用布隆過濾器解決緩存穿透問題。其實,還有另外一種更簡單的方案,即:
緩存空值
當某個使用者id在緩存中查不到,在資料庫中也查不到時,也需要将該使用者id緩存起來,隻不過值是空的。這樣後面的請求,再拿相同的使用者id發起請求時,就能從緩存中擷取空資料,直接傳回了,而無需再去查一次資料庫。
優化之後的流程圖如下:
關鍵點是不管從資料庫有沒有查到資料,都将結果放入緩存中,隻是如果沒有查到資料,緩存中的值是空的罷了。
2. 緩存擊穿問題
2.1 什麼是緩存擊穿?
有時候,我們在通路熱點資料時。比如:我們在某個商城購買某個熱門商品。
為了保證通路速度,通常情況下,商城系統會把商品資訊放到緩存中。但如果某個時刻,該商品到了過期時間失效了。
此時,如果有大量的使用者請求同一個商品,但該商品在緩存中失效了,一下子這些使用者請求都直接怼到資料庫,可能會造成瞬間資料庫壓力過大,而直接挂掉。
那麼,如何解決這個問題呢?
2.2 加鎖
資料庫壓力過大的根源是,因為同一時刻太多的請求通路了資料庫。
如果我們能夠限制,同一時刻隻有一個請求才能通路某個productId的資料庫商品資訊,不就能解決問題了?
答:沒錯,我們可以用
加鎖
的方式,實作上面的功能。
僞代碼如下:
try {
String result = jedis.set(productId, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return queryProductFromDbById(productId);
}
} finally{
unlock(productId,requestId);
}
return null;
在通路資料庫時加鎖,防止多個相同productId的請求同時通路資料庫。
然後,還需要一段代碼,把從資料庫中查詢到的結果,又重新放入緩存中。辦法挺多的,在這裡我就不展開了。
2.3 自動續期
出現緩存擊穿問題是由于key過期了導緻的。那麼,我們換一種思路,在key快要過期之前,就自動給它續期,不就OK了?
答:沒錯,我們可以用job給指定key自動續期。
比如說,我們有個分類功能,設定的緩存過期時間是30分鐘。但有個job每隔20分鐘執行一次,自動更新緩存,重新設定過期時間為30分鐘。
這樣就能保證,分類緩存不會失效。
此外,在很多請求第三方平台接口時,我們往往需要先調用一個擷取token的接口,然後用這個token作為參數,請求真正的業務接口。一般擷取到的token是有有效期的,比如24小時之後失效。
如果我們每次請求對方的業務接口,都要先調用一次擷取token接口,顯然比較麻煩,而且性能不太好。
這時候,我們可以把第一次擷取到的token緩存起來,請求對方業務接口時從緩存中擷取token。
同時,有一個job每隔一段時間,比如每隔12個小時請求一次擷取token接口,不停重新整理token,重新設定token的過期時間。
2.4 緩存不失效
此外,對于很多熱門key,其實是可以不用設定過期時間,讓其永久有效的。
比如參與秒殺活動的熱門商品,由于這類商品id并不多,在緩存中我們可以不設定過期時間。
在秒殺活動開始前,我們先用一個程式提前從資料庫中查詢出商品的資料,然後同步到緩存中,提前做
預熱
等秒殺活動結束一段時間之後,我們再
手動删除
這些無用的緩存即可。
3. 緩存雪崩問題
3.1 什麼是緩存雪崩?
前面已經聊過緩存擊穿問題了。
而緩存雪崩是緩存擊穿的更新版,緩存擊穿說的是某一個熱門key失效了,而緩存雪崩說的是有多個熱門key同時失效。看起來,如果發生緩存雪崩,問題更嚴重。
緩存雪崩目前有兩種:
- 有大量的熱門緩存,同時失效。會導緻大量的請求,通路資料庫。而資料庫很有可能因為扛不住壓力,而直接挂掉。
- 緩存伺服器down機了,可能是機器硬體問題,或者機房網絡問題。總之,造成了整個緩存的不可用。
歸根結底都是有大量的請求,透過緩存,而直接通路資料庫了。
那麼,要如何解決這個問題呢?
3.2 過期時間加随機數
為了解決緩存雪崩問題,我們首先要盡量避免緩存同時失效的情況發生。
這就要求我們不要設定相同的過期時間。
可以在設定的過期時間基礎上,再加個1~60秒的随機數。
實際過期時間 = 過期時間 + 1~60秒的随機數
這樣即使在高并發的情況下,多個請求同時設定過期時間,由于有随機數的存在,也不會出現太多相同的過期key。
3.3 高可用
針對緩存伺服器down機的情況,在前期做系統設計時,可以做一些高可用架構。
比如:如果使用了redis,可以使用哨兵模式,或者叢集模式,避免出現單節點故障導緻整個redis服務不可用的情況。
使用哨兵模式之後,當某個master服務下線時,自動将該master下的某個slave服務更新為master服務,替代已下線的master服務繼續處理請求。
3.4 服務降級
如果做了高可用架構,redis服務還是挂了,該怎麼辦呢?
這時候,就需要做服務降級了。
我們需要配置一些預設的兜底資料。
程式中有個全局開關,比如有10個請求在最近一分鐘内,從redis中擷取資料失敗,則全局開關打開。後面的新請求,就直接從配置中心中擷取預設的資料。
當然,還需要有個job,每隔一定時間去從redis中擷取資料,如果在最近一分鐘内可以擷取到兩次資料(這個參數可以自己定),則把全局開關關閉。後面來的請求,又可以正常從redis中擷取資料了。
需要特别說一句,該方案并非所有的場景都适用,需要根據實際業務場景決定。
最後說一句(求關注,别白嫖我)
如果這篇文章對您有所幫助,或者有所啟發的話,幫忙掃描下發二維碼關注一下,您的支援是我堅持寫作最大的動力。
求一鍵三連:點贊、轉發、在看。
關注公衆号:【蘇三說技術】,在公衆号中回複:面試、代碼神器、開發手冊、時間管理有超贊的粉絲福利,另外回複:加群,可以跟很多BAT大廠的前輩交流和學習。