原始資料儲存在 DB 中(如 MySQL、Hbase 等),但 DB 的讀寫效能低、延遲高。
比如 MySQL 在 4 核 8G 上的 TPS = 5000,QPS = 10000 左右,讀寫平均耗時 10~100 ms。
用 Redis 作為快取系統正好可以彌補 DB 的不足,「碼哥」在自己的 MacBook Pro 2019 上執行 Redis 效能測試如下:
$ redis-benchmark -t set,get -n 100000 -q
SET: 107758.62 requests per second, p50=0.239 msec
GET: 108813.92 requests per second, p50=0.239 msec
TPS 和 QPS 達到 10 萬,於是乎我們就引入快取架構,在資料庫中儲存原始資料,同時在快取總儲存一份。
當請求進來的時候,先從快取中去資料,如果有則直接返回快取中的資料。
如果快取中沒資料,就去資料庫中讀取資料並寫到快取中,再返回結果。
這樣就天衣無縫了麼?快取的設計不當,將會導致嚴重後果,本文將介紹快取使用中常見的三個問題和解決方案:
- 快取擊穿(失效);
- 快取穿透;
- 快取雪崩。
快取擊穿(失效)
高併發流量,訪問的這個資料是熱點資料,請求的資料在 DB 中存在,但是 Redis 存的那一份已經過期,後端需要從 DB 從載入資料並寫到 Redis。
關鍵字:單一熱點資料、高併發、資料失效
但是由於高併發,可能會把 DB 壓垮,導致服務不可用。如下圖所示:
解決方案
過期時間 + 隨機值
對於熱點資料,我們不設定過期時間,這樣就可以把請求都放在快取中處理,充分把 Redis 高吞吐量效能利用起來。
或者過期時間再加一個隨機值。
設計快取的過期時間時,使用公式:過期時間=baes 時間+隨機時間。
即相同業務資料寫快取時,在基礎過期時間之上,再加一個隨機的過期時間,讓資料在未來一段時間內慢慢過期,避免瞬時全部過期,對 DB 造成過大壓力
預熱
預先把熱門資料提前存入 Redis 中,並設熱門資料的過期時間超大值。
使用鎖
當發現快取失效的時候,不是立即從資料庫載入資料。
而是先獲取分散式鎖,獲取鎖成功才執行資料庫查詢和寫資料到快取的操作,獲取鎖失敗,則說明當前有執行緒在執行資料庫查詢操作,當前執行緒睡眠一段時間在重試。
這樣只讓一個請求去資料庫讀取資料。
虛擬碼如下:
public Object getData(String id) {
String desc = redis.get(id);
// 快取為空,過期了
if (desc == null) {
// 互斥鎖,只有一個請求可以成功
if (redis(lockName)) {
try
// 從資料庫取出資料
desc = getFromDB(id);
// 寫到 Redis
redis.set(id, desc, 60 * 60 * 24);
} catch (Exception ex) {
LogHelper.error(ex);
} finally {
// 確保最後刪除,釋放鎖
redis.del(lockName);
return desc;
}
} else {
// 否則睡眠200ms,接著獲取鎖
Thread.sleep(200);
return getData(id);
}
}
}
快取穿透
快取穿透:意味著有特殊請求在查詢一個不存在的資料,即不資料存在 Redis 也不存在於資料庫。
導致每次請求都會穿透到資料庫,快取成了擺設,對資料庫產生很大壓力從而影響正常服務。
如圖所示:
解決方案
- 快取空值:當請求的資料不存在 Redis 也不存在資料庫的時候,設定一個預設值(比如:None)。當後續再次進行查詢則直接返回空值或者預設值。
- 布隆過濾器:在資料寫入資料庫的同時將這個 ID 同步到到布隆過濾器中,當請求的 id 不存在布隆過濾器中則說明該請求查詢的資料一定沒有在資料庫中儲存,就不要去資料庫查詢了。
BloomFilter 要快取全量的 key,這就要求全量的 key 數量不大,10 億 條資料以內最佳,因為 10 億 條資料大概要佔用 1.2GB 的記憶體。
說下布隆過濾器的原理吧
BloomFilter 的演算法是,首先分配一塊記憶體空間做 bit 陣列,陣列的 bit 位初始值全部設為 0。
加入元素時,採用 k 個相互獨立的 Hash 函式計算,然後將元素 Hash 對映的 K 個位置全部設定為 1。
檢測 key 是否存在,仍然用這 k 個 Hash 函式計算出 k 個位置,如果位置全部為 1,則表明 key 存在,否則不存在。
如下圖所示:
雜湊函式會出現碰撞,所以布隆過濾器會存在誤判。
這裡的誤判率是指,BloomFilter 判斷某個 key 存在,但它實際不存在的概率,因為它存的是 key 的 Hash 值,而非 key 的值。
所以有概率存在這樣的 key,它們內容不同,但多次 Hash 後的 Hash 值都相同。
對於 BloomFilter 判斷不存在的 key ,則是 100% 不存在的,反證法,如果這個 key 存在,那它每次 Hash 後對應的 Hash 值位置肯定是 1,而不會是 0。布隆過濾器判斷存在不一定真的存在。
快取雪崩
快取雪崩指的是大量的請求無法在 Redis 快取系統中處理,請求全部打到資料庫,導致資料庫壓力激增,甚至當機。
出現該原因主要有兩種:
- 大量熱點資料同時過期,導致大量請求需要查詢資料庫並寫到快取;
- Redis 故障當機,快取系統異常。
快取大量資料同時過期
資料儲存在快取系統並設定了過期時間,但是由於在同時一刻,大量資料同時過期。
系統就把請求全部打到資料庫獲取資料,併發量大的話就會導致資料庫壓力激增。
快取雪崩是發生在大量資料同時失效的場景,而快取擊穿(失效)是在某個熱點資料失效的場景,這是他們最大的區別。
如下圖:
解決方案
過期時間新增隨機值
要避免給大量的資料設定一樣的過期時間,過期時間 = baes 時間+ 隨機時間(較小的隨機數,比如隨機增加 1~5 分鐘)。
這樣一來,就不會導致同一時刻熱點資料全部失效,同時過期時間差別也不會太大,既保證了相近時間失效,又能滿足業務需求。
介面限流
當訪問的不是核心資料的時候,在查詢的方法上加上介面限流保護。比如設定 10000 req/s。
如果訪問的是核心資料介面,快取不存在允許從資料庫中查詢並設定到快取中。
這樣的話,只有部分請求會傳送到資料庫,減少了壓力。
限流,就是指,我們在業務系統的請求入口前端控制每秒進入系統的請求數,避免過多的請求被髮送到資料庫。
如下圖所示:
Redis 故障當機
一個 Redis 例項能支撐 10 萬的 QPS,而一個資料庫例項只有 1000 QPS。
一旦 Redis 當機,會導致大量請求打到資料庫,從而發生快取雪崩。
解決方案
對於快取系統故障導致的快取雪崩的解決方案有兩種:
- 服務熔斷和介面限流;
- 構建高可用快取叢集系統。
服務熔斷和限流
在業務系統中,針對高併發的使用服務熔斷來有損提供服務從而保證系統的可用性。
服務熔斷就是當從快取獲取資料發現異常,則直接返回錯誤資料給前端,防止所有流量打到資料庫導致當機。
服務熔斷和限流屬於在發生了快取雪崩,如何降低雪崩對資料庫造成的影響的方案。
構建高可用的快取叢集
所以,快取系統一定要構建一套 Redis 高可用叢集,比如 《Redis 哨兵叢集》或者 《Redis Cluster 叢集》,如果 Redis 的主節點故障當機了,從節點還可以切換成為主節點,繼續提供快取服務,避免了由於快取例項當機而導致的快取雪崩問題。
總結
- 快取穿透指的是資料庫本就沒有這個資料,請求直奔資料庫,快取系統形同虛設。
- 快取擊穿(失效)指的是資料庫有資料,快取本應該也有資料,但是快取過期了,Redis 這層流量防護屏障被擊穿了,請求直奔資料庫。
- 快取雪崩指的是大量的熱點資料無法在 Redis 快取中處理(大面積熱點資料快取失效、Redis 當機),流量全部打到資料庫,導致資料庫極大壓力。
參考資料
https://segmentfault.com/a/1190000039688578
https://cloud.tencent.com/developer/article/1824584