- 緩存雪崩 Cache Avalanche
- 緩存穿透 Cache Penetration
- 緩存擊穿 Hotspot Invalid
淺談緩存系統的三個問題
一.無處不在的緩存
緩存在計算機系統是無處不在,在CPU層面有L1-L3的Cache,在Linux中有TLB加速虛擬地址和物理地址的轉換,在應用層有Redis等內存數據庫緩存、在瀏覽器有本地緩存、手機有本地文件緩存等等。
可見,緩存在計算機系統中有非常重要的地位,主要作用就是提高響應速度、減少磁盤讀取等,本文主要討論在高並發系統中的緩存系統。一句話概括緩存系統在高並發系統中的地位的話,就是:如果高並發系統是烤羊肉串,那么緩存系統就是那一撮孜然......
二.高並發系統中的緩存- 緩存系統的作用
緩存系統在高並發系統的作用巨大,沒有緩存系統很難支撐C50K(或許這個值已經非常樂觀了)以上的場景。
基於機械磁盤或SSD的數據庫系統,讀寫的速度遠慢於內存,因此單純磁盤介質的數據庫無法支撐高並發,你可以認為緩存就是為了保護磁盤數據庫、是磁盤數據庫的屏障。- 緩存系統和數據庫系統的訪問
以讀多寫少的場景為例(實際場景也是讀多寫少),看看請求是如何得到響應的,簡單流程:請求到達之后,業務線程首先訪問緩存,如果緩存命中則返回,如果未命中則繼續請求磁盤數據庫系統,並將結果回寫到緩存系統且增加老化時間,這樣在超時時間內再有相同請求到達時則可以命中緩存。以上是高並發系統中緩存和磁盤數據庫系統、客戶端請求之間的交互過程,后續的問題分析,也是基於此過程展開的。
三.緩存系統的三大問題
網絡上對於緩存三大問題的文章很多,提到的三個問題主要是:- 緩存雪崩 Cache Avalanche
- 緩存穿透 Cache Penetration
- 緩存擊穿 Hotspot Invalid
對於上面的三個名詞我一直分不清楚,腦海中並沒有清晰的區別,於是想到去谷歌看看歪果仁是怎么說的,然而英文表述就是上面的英文,基本上和漢語翻譯是一樣的,所以只能強記了。
A.緩存雪崩問題
所謂雪崩就是原來有所支撐的冰雪,某一瞬間失去依托,瞬間涌下來。這個場景讓我想起了2011年上映的柯南劇場版《沉默的十五分鍾》,柯南在北澤村水庫為了拯救村庄制造的雪崩:
注:圖為柯南被雪崩所埋,黃金搶救期時毛利蘭在尋找他的鏡頭可見雪崩確實很可怕,回到高並發系統,如果緩存系統故障,大量的請求無法從緩存完成數據請求,因此就全量洶涌沖向磁盤數據庫系統,導致數據庫被打死,整個系統徹底崩潰。
緩存雪崩解決方案
從原因來看主要是緩存系統不夠高可用,因此提高緩存系統的穩定性和可用性十分必要,對於使用Redis作為緩存的系統而言需要使用Sentinel哨兵機制、集群化、持久化等來提高緩存系統的HA。
另一方面除了保證緩存系統的HA之外,服務本身也需要支持降級,可以使用奈飛的Hystrix來實現服務的熔斷、降級、限流來降低出現雪崩時的故障程度。說白了就是別讓服務徹底死掉就行,就像大雪封高速肯定徹底不能通行了,堵車就是慢一些至少可以走,如果還不清楚,回想一下每年你回鄉的車票是怎么從12306搶回來的。
B.緩存穿透問題
穿透形象一點就是:請求過來了 轉了一圈 一無所獲 就像穿過透明地帶一樣。
在高並發系統中緩存穿透,其實是這樣的如果一個req需要請求的key在緩存中沒有,這時業務線程就會訪問磁盤數據庫系統,然而磁盤數據庫也沒有這個key,無奈業務線程只能返回null,白白處理一圈。
小概率事件在高並發系統幾乎要成為必然,也就是如果某時段有大量惡意的不存在的key的爆破請求,那么服務將一直處理這些根本不存在的請求,導致正常請求無法被處理,從而出現問題。
舉個栗子:拉面館的服務員和廚師不允許拒絕已經進來的消費者,但是拉面館的經營范圍有限,此時惡意消費者點了一只5斤的澳洲龍蝦,經過服務員和廚師都無法響應這個需求,因此被最終被拒絕,此時輪流來了1000個這樣的惡意消費者,拉面館基本要歇菜了。
緩存穿透解決方案
高並發系統也是如此,有效甄別是否存在這個key再決定是否讀取很重要,常見的做法有:
a.把不存在的key寫一下null,這樣再來就相當於命中了,其實這種方法局限性很大,今天是5斤龍蝦,明天改成6斤的螃蟹,緩存系統和數據庫中存儲大量無用key本身是無意義的,所以一般不建議。
b.另外一種思路,轉換為查找問題,類似於在海量數據中查找某個key是否存在,考慮空間復雜度和時間復雜度,一般選用布隆過濾器來實現。緩布隆過濾器簡介
布隆過濾器是個好東西,在1970年由布隆提出的,它實際上是一個很長的二進制向量和一系列隨機映射函數。
布隆可以實現的系統包括:垃圾郵件識別、搜索蜘蛛爬蟲url去重等,主要借助K個哈希函數和一個超大的bit數組來降低哈希沖突本身帶來的誤判,從而提高識別准確性。
布隆過濾器也存在一定的誤判,假如判斷存在可能不一定存在,但是假如判斷不存在就一定不存在,因此剛好用在解決緩存穿透的key查找場景,事實上很多系統都是基於布隆過濾器來解決緩存穿透問題的。C.緩存擊穿問題
緩存擊穿是這樣一種情況:由於緩存系統中的熱點數據都有過期時間,如果沒有過期時間就造成了主存和緩存的數據不一致,因此過期時間一般都不會太長,設想某時刻一批熱點數據同時在緩存系統中過期失效,那么這部分數據就都將請求磁盤數據庫系統。
從描述上來看有點像微小規模的雪崩,但是對數據庫的壓力就很小了,只不過會影響並發性能,然而在多線程場景中緩存擊穿卻是經常發生的,相反緩存穿透和雪崩頻率不如緩存擊穿,因此研究擊穿的現實意義更大一些。緩存擊穿解決方案
可以采用的方案大概有幾種:
a. 在設置熱點數據過期時間時盡量分散,比如設置100ms的基礎值,在此基礎上正負浮動10ms,從而降低相同時刻出現CacheMiss的key的數量。
b. 另外一種做法是多線程加鎖,其中第一個線程發現CacheMiss之后進行加鎖,再從數據庫獲取內容之后寫到緩存中,其他線程獲取鎖失敗則阻塞數ms之后再進行緩存讀取,這樣可以降低訪問數據數據庫的線程數,需要注意在單機和集群需要使用不同的鎖,集群環境使用分布式鎖來實現,但是由於鎖的存在也會影響並發效率。
c.還有一種辦法是使用類似於Redis的SETNX命令,這種貌似存在問題,存在一個gap間隙請求處於無法從緩存獲取數據也無法從主存獲取數據的未決狀態。
d. 最后一種方法是在業務層對使用的熱點數據查看是否即將過期,如果即將過期則去數據庫獲取最新數據進行更新並延長該熱點key在緩存系統中的時間,從而避免后面的過期CacheMiss,相當於把事情提前解決了。緩存擊穿的解決方法都有一定的權衡,實際中根據自己的需求來解決,不過緩存擊穿的影響一般來說並不會太大,或許在你的服務跑了很久之后你才意識到會有緩存擊穿問題。
