- 開發人員使用“緩存+過期時間”的策略既可以加速數據讀寫,又保證數據的定期更新,這種模式基本能夠滿足絕大部分需求。但是有兩個問題如果同時出現,可能就會對應用造成致命的危害:
- 當前key是一個熱點key(例如一個熱門的娛樂新聞),併發量非常 大
- 重建緩存不能在短時間完成,可能是一個複雜計算,例如複雜的SQL、多次IO、多個依賴等
- 在緩存失效的瞬間,有大量線程來重建緩存(如下圖所示),造成 後端負載加大,甚至可能會讓應用崩潰
- 要解決這個問題也不是很複雜,但是不能爲了解決這個問題給系統帶來更多的麻煩,所以需要制定如下目標:
- 減少重建緩存的次數
- 數據儘可能一致
- 較少的潛在危險
一、互斥鎖(mutex)
- 此方法只允許一個線程重建緩存,其他線程等待重建緩存的線程執行完,重新從緩存獲取數據即可,整個過程如下圖所示
- 下面代碼使用Redis的setnx命令實現上述功能:
- 1)從Redis獲取數據,如果值不爲空,則直接返回值;否則執行下面的2.1)和2.2)步驟
- 2.1)如果set(nx和ex)結果爲true,說明此時沒有其他線程重建緩存, 那麼當前線程執行緩存構建邏輯
- 2.2)如果set(nx和ex)結果爲false,說明此時已經有其他線程正在執 行構建緩存的工作,那麼當前線程將休息指定時間(例如這裏是50毫秒,取 決於構建緩存的速度)後,重新執行函數,直到獲取到數據
String get(String key) {
// 從Redis中獲取數據
String value = redis.get(key);
// 如果value爲空,則開始重構緩存
if (value == null) {
// 只允許一個線程重構緩存,使用nx,並設置過期時間ex
String mutexKey = "mutext:key:" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
// 從數據源獲取數據
value = db.get(key);
// 回寫Redis,並設置過期時間
redis.setex(key, timeout, value);
// 刪除key_mutex
redis.delete(mutexKey);
}
// 其他線程休息50毫秒後重試
else {
Thread.sleep(50);
get(key);
}
}
return value;
}
二、永遠不過期
- “永遠不過期”包含兩層意思:
- 從緩存層面來看,確實沒有設置過期時間,所以不會出現熱點key過期 後產生的問題,也就是“物理”不過期
- 從功能層面來看,爲每個value設置一個邏輯過期時間,當發現超過邏 輯過期時間後,會使用單獨的線程去構建緩存
- 整個過程如下圖所示:
- 從實戰看,此方法有效杜絕了熱點key產生的問題,但唯一不足的就是重構緩存期間,會出現數據不一致的情況,這取決於應用方是否容忍這種不 一致
- 下面代碼使用Redis進行模擬:
String get(final String key) {
V v = redis.get(key);
String value = v.getValue();
// 邏輯過期時間
long logicTimeout = v.getLogicTimeout();
// 如果邏輯過期時間小於當前時間,開始後臺構建
if (v.logicTimeout <= System.currentTimeMillis()) {
String mutexKey = "mutex:key:" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
// 重構緩存
threadPool.execute(new Runnable() {
public void run() {
String dbValue = db.get(key);
redis.set(key, (dbvalue,newLogicTimeout));
redis.delete(mutexKey);
}
});
}
}
return value;
}
三、總結
- 作爲一個併發量較大的應用,在使用緩存時有三個目標:
- 第一,加快用戶訪問速度,提高用戶體驗
- 第二,降低後端負載,減少潛在的風險,保證系統平穩
- 第三,保證數據“儘可能”及時更新
-
下面將按照這三個維度對上 述兩種解決方案進行分析:
- 互斥鎖(mutex key):這種方案思路比較簡單,但是存在一定的隱患,如果構建緩存過程出現問題或者時間較長,可能會存在死鎖和線程池阻塞的風險,但是這種方法能夠較好地降低後端存儲負載,並在一致性上做得比較好
- “永遠不過期”:這種方案由於沒有設置真正的過期時間,實際上已經 不存在熱點key產生的一系列危害,但是會存在數據不一致的情況,同時代碼複雜度會增大
- 兩種解決方法對比如下圖所示: