緩存系列文章--8.熱點key問題(mutex key)


   轉載請註明出處哈:http://carlosfu.iteye.com/blog/2269678


 一、引出熱點key問題

 

       我們通常使用 緩存 + 過期時間的策略來幫助我們加速接口的訪問速度,減少了後端負載,同時保證功能的更新,一般情況下這種模式已經基本滿足要求了。

       但是有兩個問題如果同時出現,可能就會對系統造成致命的危害:

      (1) 這個key是一個熱點key(例如一個重要的新聞,一個熱門的八卦新聞等等),所以這種key訪問量可能非常大。

      (2) 緩存的構建是需要一定時間的。(可能是一個複雜計算,例如複雜的sql、多次IO、多個依賴(各種接口)等等

 

       於是就會出現一個致命問題:在緩存失效的瞬間,有大量線程來構建緩存(見下圖),造成後端負載加大,甚至可能會讓系統崩潰 。

 

    

         

 

 

二、四種解決方案(註釋:第1,2種方法來自Tim Yang博客

 

我們的目標是:儘量少的線程構建緩存(甚至是一個) + 數據一致性 + 較少的潛在危險,下面會介紹四種方法來解決這個問題:

 

1. 使用互斥鎖(mutex key): 這種解決方案思路比較簡單,就是隻讓一個線程構建緩存,其他線程等待構建緩存的線程執行完,重新從緩存獲取數據就可以了(如下圖)

     如果是單機,可以用synchronized或者lock來處理,如果是分佈式環境可以用分佈式鎖就可以了(分佈式鎖,可以用memcache的add, redis的setnx, zookeeper的添加節點操作)。

     下面是Tim yang博客的代碼,是memcache的僞代碼實現

      

if (memcache.get(key) == null) {
    // 3 min timeout to avoid mutex holder crash
    if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
        value = db.get(key);
        memcache.set(key, value);
        memcache.delete(key_mutex);
    } else {
        sleep(50);
        retry();
    }
}
     

 

      如果換成redis,就是:

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);
    }
  }
}
 

 

       

2. "提前"使用互斥鎖(mutex key):

   在value內部設置1個超時值(timeout1), timeout1比實際的memcache timeout(timeout2)小。當從cache讀取到timeout1發現它已經過期時候,馬上延長timeout1並重新設置到cache。然後再從數據庫加載數據並設置到cache中。僞代碼如下:

 

v = memcache.get(key);
if (v == null) {
    if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
        value = db.get(key);
        memcache.set(key, value);
        memcache.delete(key_mutex);
    } else {
        sleep(50);
        retry();
    }
} else {
    if (v.timeout <= now()) {
        if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
            // extend the timeout for other threads
            v.timeout += 3 * 60 * 1000;
            memcache.set(key, v, KEY_TIMEOUT * 2);

            // load the latest value from db
            v = db.get(key);
            v.timeout = KEY_TIMEOUT;
            memcache.set(key, value, KEY_TIMEOUT * 2);
            memcache.delete(key_mutex);
        } else {
            sleep(50);
            retry();
        }
    }
}

 

 

 

3. "永遠不過期"

    

    這裏的“永遠不過期”包含兩層意思:

    (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;
    }

 

 

 

4. 資源保護

       之前在緩存雪崩那篇文章提到了netflix的hystrix,可以做資源的隔離保護主線程池,如果把這個應用到緩存的構建也未嘗不可。

 

 

 

三、四種方案對比:

 

      作爲一個併發量較大的互聯網應用,我們的目標有3個:

      1. 加快用戶訪問速度,提高用戶體驗。

      2. 降低後端負載,保證系統平穩。

      3. 保證數據“儘可能”及時更新(要不要完全一致,取決於業務,而不是技術。)

      所以第二節中提到的四種方法,可以做如下比較,還是那就話:沒有最好,只有最合適。 

解決方案 優點 缺點
簡單分佈式鎖(Tim yang)

 1. 思路簡單

2. 保證一致性

1. 代碼複雜度增大

2. 存在死鎖的風險

3. 存在線程池阻塞的風險

加另外一個過期時間(Tim yang)  1. 保證一致性 同上 
不過期(本文)

1. 異步構建緩存,不會阻塞線程池

1. 不保證一致性。

2. 代碼複雜度增大(每個value都要維護一個timekey)。

3. 佔用一定的內存空間(每個value都要維護一個timekey)。

資源隔離組件hystrix(本文)

1. hystrix技術成熟,有效保證後端。

2. hystrix監控強大。

 

 

1. 部分訪問存在降級策略。

 

 

四、總結

 

   1.  熱點key + 過期時間 + 複雜的構建緩存過程 => mutex key問題

   2. 構建緩存一個線程做就可以了。

   3. 四種解決方案:沒有最佳只有最合適。

 

 

 

五、參考文獻:(本文部分代碼和圖來自如下兩篇博客)

 

  1. Memcache mutex設計模式(Tim Yang)
  2. cache中的key mutex問題解決及延伸應用
  3. 談談Redis的SETNX
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章