厲害了,如何通過雙 key 來解決緩存併發問題?

雲棲號資訊:【點擊查看更多行業資訊
在這裏您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!

我們在使用緩存的時候,不管Redis或者是Memcached,基本上都會遇到以下3個問題:緩存穿透、緩存併發、緩存集中失效。這篇文章主要針對【緩存併發】問題展開討論,並給出具體的解決方案。

1.什麼是緩存併發?

在高併發的訪問下,當某個緩存處於過期失效的時間點時,極有可能出現多個進程同時查詢該緩存(該緩存是業務場景中非常 "熱點" 的數據,比如首頁的緩存數據)。因爲查詢DB並重新緩存需要一定的時間,而瞬時併發非常高,如果此時緩存失效了,這些併發請求都會直接訪問DB,從而導致DB服務器的CPU或者內存負載過高,服務能力下降甚至宕機,此問題即緩存併發問題。

50FE20A2_1F83_4a1a_9A94_30507E5F968A

緩存併發問題在微服務架構下凸顯更加嚴重,比如某個基礎服務A因爲上述問題出現不可用,進而導致依賴A服務的B、C服務也不可用,而B服務的不可用又導致服務E、F不可用,不可用的服務就像滾雪球一樣越滾越大,最終導致系統出現嚴重故障,此現象我們稱之爲雪崩效應。
注意緩存併發和緩存集中失效的區別在於:緩存併發指的是某一個熱點key的失效,而緩存集中失效則是一批key同時失效,兩者都可能導致雪崩問題。

2.如何解決?

針對該問題,存在以下三種解決方案:

1.加鎖:在緩存失效後,通過加鎖的方式只允許一個線程查詢數據和寫緩存,其他線程如果發現有鎖就等待,等解鎖後再返回數據。該方案會造成部分請求等待。

2.二級緩存:A1爲原始緩存,A2爲拷貝緩存。A1失效時,可以訪問A2,其中A1的緩存失效時間設置爲短期(比如5min),A2的緩存失效時間設置爲長期(比如1天)。如果緩存value很大,此方案的緩存空間利用率低。關注公衆號互聯網架構師,回覆關鍵字2T,獲取最新架構視頻

3.雙key:思路和方案2類似,不同的是雙key分別緩存過期時間(key-time)和緩存數據(key-data),其中(key-time)的緩存失效時間設置爲短期(比如5min),(key-data)的緩存失效時間設置爲長期(比如1天)。當第一個線程發現 key-time 過期不存在時,則先更新key-time,然後去查詢數據庫並更新key-data 的值;當其他線程來獲取數據時,雖然第一個線程還沒有從數據庫查詢完畢並更新緩存,但發現key-time存在,會直接讀取緩存的舊數據返回。和二級緩存的方案對比,該方案的緩存空間利用率高。

3.雙key方案的示例代碼

  1. 寫緩存的示例代碼
1public static boolean set(String key, String value, int seconds) {
 2    Jedis jedis = null;
 3    try {
 4        jedis = jedisPool.getResource();
 5        if (seconds > 0){
 6            // 添加數據緩存,緩存有效時間 = 真實時間 + 1 天
 7            jedis.set(key, seconds + 60 * 60 * 24, value);
 8
 9            // 添加過期時間緩存,緩存有效時間 = 真實時間
10            jedis.set("lock_" + key, seconds, System.currentTimeMillis() + "");
11        } else {
12            jedis.set(key, value);
13            jedis.set("lock_" + key, System.currentTimeMillis() + "");
14         }
15
16         return true;
17    } catch (JedisException e) {
18         if (jedis != null) {
19             returnBrokenResource(jedis);
20             jedis = null;
21         }
22         throw e;
23    } finally {
24         if (jedis != null) {
25             returnResource(jedis);
26         }
27    }
28}

2. 讀緩存的示例代碼

 1public static String get(String key) {
 2    Jedis jedis = null;
 3    try {
 4        jedis = jedisPool.getResource();
 5
 6        // 緩存過期 && 獲取鎖成功,setnx:原子操作
 7        if (jedis.setnx("lock_" + key, System.currentTimeMillis() + "") == 1) {
 8            /**
 9             * 將鎖的失效時間設爲60s,在60s內若查詢數據庫成功,則更新鎖的失效時間=緩存時間
10             * 如果60s內出現異常,則60s後第一個請求又會去訪問數據庫
11             * 返回null表示沒有查詢到數據庫,外層代碼會通過數據庫獲取數據並設置緩存
12             */
13            jedis.expire("lock_" + key, 60);
14            return null;
15        } else{
16            // 緩存未過期或者緩存過期但獲取鎖失敗, 則返回舊數據
17            return jedis.get(key);
18        }
19    } catch (JedisException e) {
20        if (jedis != null) {
21            returnBrokenResource(jedis);
22            jedis = null;
23        } 
24        throw e;
25    } finally {
26        if (jedis != null) {
27            returnResource(jedis);
28        }
29    }
30}

【雲棲號在線課堂】每天都有產品技術專家分享!
課程地址:https://yqh.aliyun.com/zhibo

立即加入社羣,與專家面對面,及時瞭解課程最新動態!
【雲棲號在線課堂 社羣】https://c.tb.cn/F3.Z8gvnK

原文發佈時間:2020-07-17
本文作者:互聯網架構師
本文來自:“互聯網架構師”,瞭解相關信息可以關注“互聯網架構師

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