缓存三大问题及解决方案
?
1. 緩存來由
隨著互聯網系統發展的逐步完善,提高系統的qps,目前的絕大部分系統都增加了緩存機制從而避免請求過多的直接與數據庫操作從而造成系統瓶頸,極大的提升了用戶體驗和系統穩定性。
2. 緩存問題
雖然使用緩存給系統帶來了一定的質的提升,但同時也帶來了一些需要注意的問題。
2.1 緩存穿透
緩存穿透是指查詢一個一定不存在的數據,因為緩存中也無該數據的信息,則會直接去數據庫層進行查詢,從系統層面來看像是穿透了緩存層直接達到db,從而稱為緩存穿透,沒有了緩存層的保護,這種查詢一定不存在的數據對系統來說可能是一種危險,如果有人惡意用這種一定不存在的數據來頻繁請求系統,不,準確的說是攻擊系統,請求都會到達數據庫層導致db癱瘓從而引起系統故障。
2.2 解決方案
緩存穿透業內的解決方案已經比較成熟,主要常用的有以下幾種:
- bloom filter:類似于哈希表的一種算法,用所有可能的查詢條件生成一個bitmap,在進行數據庫查詢之前會使用這個bitmap進行過濾,如果不在其中則直接過濾,從而減輕數據庫層面的壓力。
- 空值緩存:一種比較簡單的解決辦法,在第一次查詢完不存在的數據后,將該key與對應的空值也放入緩存中,只不過設定為較短的失效時間,例如幾分鐘,這樣則可以應對短時間的大量的該key攻擊,設置為較短的失效時間是因為該值可能業務無關,存在意義不大,且該次的查詢也未必是攻擊者發起,無過久存儲的必要,故可以早點失效。
2.3 緩存雪崩
在普通的緩存系統中一般例如redis、memcache等中,我們會給緩存設置一個失效時間,但是如果所有的緩存的失效時間相同,那么在同一時間失效時,所有系統的請求都會發送到數據庫層,db可能無法承受如此大的壓力導致系統崩潰。
2.4 解決方案
- 線程互斥:只讓一個線程構建緩存,其他線程等待構建緩存的線程執行完,重新從緩存獲取數據才可以,每個時刻只有一個線程在執行請求,減輕了db的壓力,但缺點也很明顯,降低了系統的qps。
- 交錯失效時間:這種方法時間比較簡單粗暴,既然在同一時間失效會造成請求過多雪崩,那我們錯開不同的失效時間即可從一定長度上避免這種問題,在緩存進行失效時間設置的時候,從某個適當的值域中隨機一個時間作為失效時間即可。
2.5 緩存擊穿
緩存擊穿實際上是緩存雪崩的一個特例,大家使用過微博的應該都知道,微博有一個熱門話題的功能,用戶對于熱門話題的搜索量往往在一些時刻會大大的高于其他話題,這種我們成為系統的“熱點“,由于系統中對這些熱點的數據緩存也存在失效時間,在熱點的緩存到達失效時間時,此時可能依然會有大量的請求到達系統,沒有了緩存層的保護,這些請求同樣的會到達db從而可能引起故障。擊穿與雪崩的區別即在于擊穿是對于特定的熱點數據來說,而雪崩是全部數據。
2.6 解決方案
1.使用互斥鎖(mutex key)
業界比較常用的做法,是使用mutex。簡單地來說,就是在緩存失效的時候(判斷拿出來的值為空),不是立即去load db,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操作返回成功時,再進行load db的操作并回設緩存;否則,就重試整個get緩存的方法。
SETNX,是「SET if Not eXists」的縮寫,也就是只有不存在的時候才設置,可以利用它來實現鎖的效果。在redis2.6.1之前版本未實現setnx的過期時間,所以這里給出兩種版本代碼參考:
//2.6.1前單機版本鎖
String get(String key) {
String value = redis.get(key);
if (value == null) {
if (redis.setnx(key_mutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(key_mutex, 3 * 60)
value = db.get(key);
redis.set(key, value);
redis.delete(key_mutex);
} else {
//其他線程休息50毫秒后重試
Thread.sleep(50);
get(key);
}
}
}
最新版本代碼:
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表緩存值過期
//設置3min的超時,防止del操作失敗的時候,下次緩存過期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表設置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //這個時候代表同時候的其他線程已經load db并回設到緩存了,這時候重試獲取緩存值即可
sleep(50);
get(key); //重試
}
} else {
return value;
}
}
2:"永遠不過期": ?
這里的“永遠不過期”包含兩層意思:
(1) 從redis上看,確實沒有設置過期時間,這就保證了,不會出現熱點key過期問題,也就是“物理”不過期。
(2) 從功能上看,如果不過期,那不就成靜態的了嗎?所以我們把過期時間存在key對應的value里,如果發現要過期了,通過一個后臺的異步線程進行緩存的構建,也就是“邏輯”過期
? ? ? ? 從實戰看,這種方法對于性能非常友好,唯一不足的就是構建緩存時候,其余線程(非構建緩存的線程)可能訪問的是老數據,但是對于一般的互聯網功能來說這個還是可以忍受。
String get(final String key) {
V v = redis.get(key);
String value = v.getValue();
long timeout = v.getTimeout();
if (v.timeout <= System.currentTimeMillis()) {
// 異步更新后臺異常執行
threadPool.execute(new Runnable() {
public void run() {
String keyMutex = "mutex:" + key;
if (redis.setnx(keyMutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(keyMutex, 3 * 60);
String dbValue = db.get(key);
redis.set(key, dbValue);
redis.delete(keyMutex);
}
}
});
}
return value;
}
轉載于:https://www.cnblogs.com/zyy1688/p/10794989.html
總結
以上是生活随笔為你收集整理的缓存三大问题及解决方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 功能网站
- 下一篇: SPL 关联优化技巧