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