為了應對越來越大的流量,快取便成為系統服務必不可少的一部分,但使用快取就會出現快取擊穿和快取穿透的威脅。
背景介紹
網際網路應用逐步深入到生活的各個角落,為了滿足越來越多使用者使用網際網路應用的需求,幾乎所有網際網路公司都採用快取的方案來解決瞬時流量超高,或者長期流量過高的問題。但使用快取存在風險——快取穿透和快取擊穿:簡單的講就是如果該資料原本就不存在,那麼就會發生快取穿透;如果快取內容因為各種原因失效,那麼就會發生快取擊穿。
具體一點來說,如果快取中不存在需要查詢的內容,一般情況下需要再深入一層進行查詢,一般為不能承受壓力的關係型資料庫(承壓能力為快取的1%,甚至更低),如果資料庫中不存在,則叫做快取穿透;反之,如果資料庫中存在這個資料,則叫做快取擊穿(如果同一時刻大量的快取失效叫做快取雪崩,本文暫不討論該問題)。這種查詢在流量不高的情況下,不會出現問題,如果查詢資料庫的流量過高,尤其是資料庫中不存在的情況下,嚴重時會導致資料庫不可用,連帶影響使用資料庫的其他業務,本業務也有很大的可能性受到影響。
快取穿透常見的處理方式
1、空值快取
既然該資料本身就不存在,最簡單粗暴的方式就是直接將不存在的值定義為空(視具體業務和快取的方式定義為null或者」」)。具體方式是每次查詢完資料庫,我們可以將key在快取中設定對應的值為空,短期內再次查詢這個key的時候就不用查詢資料庫了。
通常的簡單做法:
這裡需要強調注意:為了系統的最終一致性,這些key必須設定過期時間,或者必須存在更新方式,防止這個key的資料後期真實存在,但改key始終為空,導致資料不一致的情況出現。
這種方式的缺點也十分的明顯:如果key數量巨大且分散無任何規律,就會浪費大量快取空間,並且不能抗住瞬時流量衝擊(尤其是遇到惡意的攻擊的時候,有可能將快取空間打爆,影響範圍更大),需要額外配置降級開關(查詢資料庫的開關或者限流),這時本方案就顯得沒想象的那麼美好。針對不能抗住瞬時流量的情況,常見的處理方式是使用計數器,對不存在的key進行計數,當某個key在一定時間達到一定的量級,就查詢一次資料庫,按照資料庫的返回值對key進行快取。未達指定閾值數量之前,按照商定的空值返回。
故這種解決方案的建議使用場景為:key全集資料資料量級較小,並且完全可**,可以通過提前填充的方式直接將資料快取。
2、布隆過濾器(bloomfilter)
本質上布隆過濾器是一種資料結構,比較巧妙的概率型資料結構(probabilistic data structure),特點是高效地插入和查詢,可以用來告訴你 「某樣東西一定不存在或者可能存在」。相比於傳統的 list、set、map 等資料結構,它更高效、占用空間更少,但是缺點是其返回的結果是概率性的,而不是確切的。實際應用中,google bigtable,apache hbbase 和 apache cassandra 使用布隆過濾器減少對不存在的行和列的查詢。
布隆過濾器的原理如下:
假如「京東」經過hash後佔位為618,加入後,如下所示:
假如大促經過hash後佔位為為678,加入後,如下所示:
假如某寶hash後佔位256,則可以完全判斷該key不存在,直接返回null即可。但某某多hash後佔位167,則不能判斷是否存在,需要進行查庫操作進行判斷。
這裡需要強調注意的是必須使用高效的hash演算法,否則這種方式會嚴重影響系統的效能,建議的演算法包括murmurhash、fnv的穩定高效的演算法。
使用過程中,通常把有資料的key都放到bloomfilter中,每次查詢的時候都先去bloomfilter判斷,如果沒有就直接返回空。由於布隆過濾器不支援刪除操作(具體結合上圖推算刪除乙個就可以得知),對於刪除的key,查詢就會經過bloomfilter然後查詢快取再查詢資料庫,所以bloomfilter建議結合快取空值用,對於刪除的key,可以在快取中快取空。(當然有同學自行實現了可刪除的布隆過濾器——counting bloom filter,原理較為簡單,本文不做具體分析,個人感覺將簡單的問題複雜化了)。
同樣它也不支援擴容操作,這就要求布隆過濾器建立初期必須進行嚴格的推算,確保後期不需要擴容,否則重建布隆過濾器的成本可能會超乎想象。具體的推算公式如下(具體的推算過程非本文重點,請各位自行查詢):
k 為雜湊函式的個數,m 為布隆過濾器的長度,n 為插入元素的個數(需要處理的資料個數),p 為誤報率。
布隆過濾器的使用場景受key的狀態限制,如果key是動態無規律的,不建議使用該方式。作者使用布隆過濾器作為使用者是否參與活動的過濾,布隆過濾器在活動期間最大值為目前的會員數量,完全可控。
考慮真實情況下,快取的儲存空間及效能問題,在真實使用中,為了避免熱key和大key的問題,首先對使用者標識進行了hash,首先對hash按照布隆過濾器的數量進行取餘,確定使用哪個布隆過濾器,然後使用布隆過濾器。具體情況如下:
快取擊穿常見的處理方式
1、互斥鎖(mutex key)
這是比較常見的做法,是在快取失效的時候,不是立即去查詢資料庫,先搶互斥鎖(比如redis的setnx乙個mutex key),當操作返回成功時(即獲取到互斥鎖),再進行查詢資料庫的操作並回設快取;否則,就重試整個獲取快取的方法或者直接返回空。setnx,官方的解釋是只在鍵 key 不存在的情況下,將鍵 key 的值設定為 value 。若鍵 key 已經存在,則 setnx 命令不做任何動作。setnx 是『set if not exists』(如果不存在,則set)的簡寫。
但這種方式顯然存在問題,redis的setnx命令是當key不存在時設定key,但setnx不能同時完成expire設定失效時長,不能保證setnx和expire的原子性。這會導致setnx成功但expire失敗時,鎖永遠不會釋放,下面提供了一種可行的**,供大家討論:
redis還是善解人意的,從 2.6.12 起,我們可以使用set命令完成setnx和expire的操作,並且這種操作是原子操作,可以完全替代上述的**了。
這種簡單粗暴的方式有著嚴格是使用場景(換句話說就是有著嚴重的缺陷),如果快取大量失效(快取雪崩),那麼對於資料庫是一場災難;如果資料庫查詢緩慢,不僅對資料是一場災難,對於使用該快取的介面會造成執行緒阻塞,介面效能又開啟了另外一場災難!一般情況下,這種方式適用於永久快取的key,或者key偶爾丟失的情況下,其他情況請各位讀者慎重考慮或增加其他機制保護資料庫和介面(如使用hystrix限流&降級等)。
2、非同步構建快取
當快取失效時,不是立刻去查詢資料庫,而是先建立快取更新的非同步任務,然後直接返回空值。這種做法不會阻塞當前執行緒,並且對於資料庫的壓力基本可控,但犧牲了整體資料的一致性。從實際的使用看,這種方法對於效能非常友好,唯一不足的就是構建快取時候,所有查詢返回的內容均為空值,但是對於一致性要求不高的網際網路功能來說這個還是可以忍受。
總結綜上所述,針對常見的快取穿透和快取擊穿的問題,各自的優缺點如下:
redis 快取擊穿和快取穿透
布隆過濾器 快取擊穿 總結有很多使用者,請求介面。為了防止mysql壓力過大,在訪問量很大且資料變動不頻繁的情況下,我們通過增加redis快取減少mysql的壓力。正常的流程為下圖所示。redis中無資料,從mysql中查詢 mysqlserver mysqli connect 127.0.0.1 ...
Redis 快取穿透 快取雪崩和快取擊穿
快取穿透,是指查詢乙個資料庫一定不存在的資料。正常的使用快取流程大致是,資料查詢先進行快取查詢,如果key不存在或者key已經過期,再對資料庫進行查詢,並把查詢到的物件,放進快取。如果資料庫查詢物件為空,則不放進快取。流程 引數傳入物件主鍵id 根據key從快取中獲取物件 如果物件不為空,直接返回 ...
redis快取穿透 快取雪崩和快取擊穿
查詢資料庫中一定不存在的資料,使用者發出查詢請求,根據引數 主鍵id 首先根據key去查詢redis,發現為空,接著查詢資料庫發現沒有結果,然後不會往redis中存入任何資料,接下來所有的請求都會往復進行,都會訪問資料庫,造成資料庫壓力。解決快取穿透,可以在第一次查詢資料庫時,如果返回空,則在red...