一闻书、緩存穿透預防及優(yōu)化?
緩存穿透是指查詢一個根本不存在的數(shù)據(jù),緩存層和存儲層都不會命中脑慧,但是出于容錯的考慮惠窄,如果從存儲層查不到數(shù)據(jù)則不寫入緩存層,如圖 11-3 所示整個過程分為如下 3 步:
緩存層不命中
存儲層不命中漾橙,所以不將空結果寫回緩存
返回空結果?
緩存穿透將導致不存在的數(shù)據(jù)每次請求都要到存儲層去查詢,失去了緩存保護后端存儲的意義楞卡。
圖-1:緩存穿透模型
緩存穿透問題可能會使后端存儲負載加大霜运,由于很多后端存儲不具備高并發(fā)性脾歇,甚至可能造成后端存儲宕掉。通程约瘢可以在程序中分別統(tǒng)計總調用數(shù)藕各、緩存層命中數(shù)、存儲層命中數(shù)焦除,如果發(fā)現(xiàn)大量存儲層空命中激况,可能就是出現(xiàn)了緩存穿透問題。
造成緩存穿透的基本有兩個膘魄。第一乌逐,業(yè)務自身代碼或者數(shù)據(jù)出現(xiàn)問題,第二创葡,一些惡意攻擊浙踢、爬蟲等造成大量空命中,下面我們來看一下如何解決緩存穿透問題灿渴。
二洛波、緩存穿透的解決方法
1)緩存空對象
如下圖所示,當?shù)?2 步存儲層不命中后骚露,仍然將空對象保留到緩存層中蹬挤,之后再訪問這個數(shù)據(jù)將會從緩存中獲取,保護了后端數(shù)據(jù)源棘幸。
緩存空對象會有兩個問題:
第一焰扳,空值做了緩存,意味著緩存層中存了更多的鍵够话,需要更多的內存空間 ( 如果是攻擊蓝翰,問題更嚴重 ),比較有效的方法是針對這類數(shù)據(jù)設置一個較短的過期時間女嘲,讓其自動剔除畜份。
第二,緩存層和存儲層的數(shù)據(jù)會有一段時間窗口的不一致欣尼,可能會對業(yè)務有一定影響爆雹。例如過期時間設置為 5 分鐘,如果此時存儲層添加了這個數(shù)據(jù)愕鼓,那此段時間就會出現(xiàn)緩存層和存儲層數(shù)據(jù)的不一致钙态,此時可以利用消息系統(tǒng)或者其他方式清除掉緩存層中的空對象。
下面給出了緩存空對象的實現(xiàn)偽代碼:
2)布隆過濾器攔截
如下圖所示菇晃,在訪問緩存層和存儲層之前册倒,將存在的 key 用布隆過濾器提前保存起來,做第一層攔截磺送。
例如: 一個個性化推薦系統(tǒng)有 4 億個用戶 ID驻子,每個小時算法工程師會根據(jù)每個用戶之前歷史行為做出來的個性化放到存儲層中灿意,但是最新的用戶由于沒有歷史行為,就會發(fā)生緩存穿透的行為崇呵,為此可以將所有有個性化推薦數(shù)據(jù)的用戶做成布隆過濾器缤剧。如果布隆過濾器認為該用戶 ID 不存在,那么就不會訪問存儲層域慷,在一定程度保護了存儲層荒辕。
開發(fā)提示:
有關布隆過濾器的相關知識,可以參考:Bloom Filter(布隆過濾器)的概念和原理
可以利用 Redis 的 Bitmaps 實現(xiàn)布隆過濾器犹褒,GitHub 上已經(jīng)開源了類似的方案抵窒,讀者可以進行參考:
https://github.com/erikdubbelboer/Redis-Lua-scaling-bloom-filter
使用布隆過濾器應對穿透問題
這種方法適用于數(shù)據(jù)命中不高,數(shù)據(jù)相對固定實時性低(通常是數(shù)據(jù)集較大)的應用場景化漆,代碼維護較為復雜估脆,但是緩存空間占用少。
兩種方案對比
前面介紹了緩存穿透問題的兩種解決方法 ( 實際上這個問題是一個開放問題座云,有很多解決方法 )疙赠,下面通過下表從適用場景和維護成本兩個方面對兩種方案進行分析。
緩存空對象和布隆過濾器方案對比
三朦拖、緩存雪崩問題優(yōu)化?
從下圖可以很清晰出什么是緩存雪崩:由于緩存層承載著大量請求圃阳,有效的保護了存儲層,但是如果緩存層由于某些原因整體不能提供服務璧帝,于是所有的請求都會達到存儲層捍岳,存儲層的調用量會暴增,造成存儲層也會掛掉的情況睬隶。緩存雪崩的英文原意是 stampeding herd(奔逃的野牛)锣夹,指的是緩存層宕掉后,流量會像奔逃的野牛一樣苏潜,打向后端存儲银萍。
緩存層不可用引起的雪崩
預防和解決緩存雪崩問題,可以從以下三個方面進行著手恤左。
1)保證緩存層服務高可用性贴唇。
和飛機都有多個引擎一樣,如果緩存層設計成高可用的飞袋,即使個別節(jié)點戳气、個別機器、甚至是機房宕掉巧鸭,依然可以提供服務瓶您,例如前面介紹過的 Redis Sentinel 和 Redis Cluster 都實現(xiàn)了高可用。
2)依賴隔離組件為后端限流并降級。
無論是緩存層還是存儲層都會有出錯的概率览闰,可以將它們視同為資源芯肤。作為并發(fā)量較大的系統(tǒng),假如有一個資源不可用压鉴,可能會造成線程全部 hang 在這個資源上,造成整個系統(tǒng)不可用锻拘。降級在高并發(fā)系統(tǒng)中是非常正常的:比如推薦服務中油吭,如果個性化推薦服務不可用,可以降級補充熱點數(shù)據(jù)署拟,不至于造成前端頁面是開天窗婉宰。
在實際項目中,我們需要對重要的資源 ( 例如 Redis推穷、 MySQL心包、 Hbase、外部接口 ) 都進行隔離馒铃,讓每種資源都單獨運行在自己的線程池中蟹腾,即使個別資源出現(xiàn)了問題,對其他服務沒有影響区宇。但是線程池如何管理娃殖,比如如何關閉資源池,開啟資源池议谷,資源池閥值管理炉爆,這些做起來還是相當復雜的,這里推薦一個 Java 依賴隔離工具 Hystrix(https://github.com/Netflix/Hystrix)卧晓,如下圖所示芬首。
Hystrix 是解決依賴隔離的利器,但是該內容已經(jīng)超出本書的范圍逼裆,同時只適用于 Java 應用郁稍,所以這里不會詳細介紹。
Hystrix 示意圖
3)提前演練波附。在項目上線前艺晴,演練緩存層宕掉后,應用以及后端的負載情況以及可能出現(xiàn)的問題掸屡,在此基礎上做一些預案設定封寞。
四、緩存熱點 key 重建優(yōu)化?
開發(fā)人員使用緩存 + 過期時間的策略既可以加速數(shù)據(jù)讀寫仅财,又保證數(shù)據(jù)的定期更新狈究,這種模式基本能夠滿足絕大部分需求。但是有兩個問題如果同時出現(xiàn)盏求,可能就會對應用造成致命的危害:
當前 key 是一個熱點 key( 例如一個熱門的娛樂新聞)抖锥,并發(fā)量非常大亿眠。
重建緩存不能在短時間完成,可能是一個復雜計算磅废,例如復雜的 SQL纳像、多次 IO、多個依賴等拯勉。
在緩存失效的瞬間竟趾,有大量線程來重建緩存 ( 如下圖),造成后端負載加大宫峦,甚至可能會讓應用崩潰岔帽。
熱點 key 失效后大量線程重建緩存
要解決這個問題也不是很復雜,但是不能為了解決這個問題給系統(tǒng)帶來更多的麻煩导绷,所以需要制定如下目標:
減少重建緩存的次數(shù)
數(shù)據(jù)盡可能一致
較少的潛在危險
1.?加鎖排隊. 限流-- 限流算法. 1.計數(shù) 2.滑動窗口 3.? 令牌桶Token Bucket?4.漏桶 leaky bucket [1]
?在緩存失效后犀勒,通過加鎖或者隊列來控制讀數(shù)據(jù)庫寫緩存的線程數(shù)量。比如對某個key只允許一個線程查詢數(shù)據(jù)和寫緩存妥曲,其他線程等待贾费。
?業(yè)界比較常用的做法,是使用mutex逾一。簡單地來說铸本,就是在緩存失效的時候(判斷拿出來的值為空),不是立即去load db遵堵,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一個mutex key箱玷,當操作返回成功時,再進行l(wèi)oad db的操作并回設緩存陌宿;否則锡足,就重試整個get緩存的方法。
SETNX壳坪,是「SET if Not eXists」的縮寫舶得,也就是只有不存在的時候才設置,可以利用它來實現(xiàn)鎖的效果爽蝴。
(1)互斥鎖 (mutex key)
此方法只允許一個線程重建緩存沐批,其他線程等待重建緩存的線程執(zhí)行完,重新從緩存獲取數(shù)據(jù)即可蝎亚,整個過程如圖 :
使用互斥鎖重建緩存
下面代碼使用 Redis 的 setnx 命令實現(xiàn)上述功能九孩。
(1) 從 Redis 獲取數(shù)據(jù),如果值不為空发框,則直接返回值躺彬,否則執(zhí)行 (2.1) 和 (2.2)。
(2) 如果 set(nx 和 ex) 結果為 true,說明此時沒有其他線程重建緩存宪拥,那么當前線程執(zhí)行緩存構建邏輯仿野。
(2.2) 如果 setnx(nx 和 ex) 結果為 false,說明此時已經(jīng)有其他線程正在執(zhí)行構建緩存的工作她君,那么當前線程將休息指定時間 ( 例如這里是 50 毫秒脚作,取決于構建緩存的速度 ) 后,重新執(zhí)行函數(shù)缔刹,直到獲取到數(shù)據(jù)鳖枕。
2.數(shù)據(jù)預熱
? 可以通過緩存reload機制,預先去更新緩存桨螺,再即將發(fā)生大并發(fā)訪問前手動觸發(fā)加載緩存不同的key,設置不同的過期時間酿秸,讓緩存失效的時間點盡量均勻
?3.做二級緩存灭翔,或者雙緩存策略。
A1為原始緩存辣苏,A2為拷貝緩存肝箱,A1失效時,可以訪問A2稀蟋,A1緩存失效時間設置為短期煌张,A2設置為長期。
4.永遠不過期
“永遠不過期”包含兩層意思:
(1)?從緩存上看退客,確實沒有設置過期時間骏融,這就保證了,不會出現(xiàn)熱點key過期問題萌狂,也就是“物理”不過期档玻。
(2)?從功能上看,如果不過期茫藏,那不就成靜態(tài)的了嗎误趴?所以我們把過期時間存在key對應的value里,如果發(fā)現(xiàn)要過期了务傲,通過一個后臺的異步線程進行緩存的構建凉当,也就是“邏輯”過期.
?從實戰(zhàn)看,這種方法對于性能非常友好售葡,唯一不足的就是構建緩存時候看杭,其余線程(非構建緩存的線程)可能訪問的是老數(shù)據(jù),但是對于一般的互聯(lián)網(wǎng)功能來說這個還是可以忍受天通。
整個過程如下圖所示:
” 永遠不過期 ” 策略
從實戰(zhàn)看泊窘,此方法有效杜絕了熱點 key 產(chǎn)生的問題,但唯一不足的就是重構緩存期間,會出現(xiàn)數(shù)據(jù)不一致的情況烘豹,這取決于應用方是否容忍這種不一致瓜贾。下面代碼使用 Redis 進行模擬:
作為一個并發(fā)量較大的應用,在使用緩存時有三個目標:第一携悯,加快用戶訪問速度祭芦,提高用戶體驗。第二憔鬼,降低后端負載龟劲,減少潛在的風險,保證系統(tǒng)平穩(wěn)轴或。第三昌跌,保證數(shù)據(jù)“盡可能”及時更新。下面將按照這三個維度對上述兩種解決方案進行分析照雁。
互斥鎖 (mutex key):這種方案思路比較簡單蚕愤,但是存在一定的隱患,如果構建緩存過程出現(xiàn)問題或者時間較長饺蚊,可能會存在死鎖和線程池阻塞的風險萍诱,但是這種方法能夠較好的降低后端存儲負載并在一致性上做的比較好。
” 永遠不過期 “:這種方案由于沒有設置真正的過期時間污呼,實際上已經(jīng)不存在熱點 key 產(chǎn)生的一系列危害裕坊,但是會存在數(shù)據(jù)不一致的情況,同時代碼復雜度會增大燕酷。
兩種解決方法對比如下表所示籍凝。
兩種熱點 key 的解決方法