Redis(開發與運維):57---緩存設計之(熱點key重建優化)

  • 開發人員使用“緩存+過期時間”的策略既可以加速數據讀寫,又保證數據的定期更新,這種模式基本能夠滿足絕大部分需求。但是有兩個問題如果同時出現,可能就會對應用造成致命的危害:
    • 當前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產生的一系列危害,但是會存在數據不一致的情況,同時代碼複雜度會增大
  • 兩種解決方法對比如下圖所示:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章