天天看點

緩存穿透、緩存并發、熱點緩存之最佳招式

一、前言

在之前的一篇

緩存穿透、緩存并發、緩存失效之思路變遷

文章中介紹了關于緩存穿透、并發的一些常用思路,但是個人感覺文章中沒有明确一些思路的使用場景,本文繼續将繼續深化與大家共同探讨,同時也非常感謝這段時間給我提寶貴建議的朋友們。

說明:本文中提到的緩存可以了解為Redis。

二、緩存穿透與并發方案

相信不少朋友之前看過很多類似的文章,但是歸根結底就是二個問題:

  • 如何解決穿透
  • 如何解決并發

當并發較高的時候,其實我是不建議使用緩存過期這個政策的,我更希望緩存一直存在,通過背景系統來更新緩存系統中的資料達到資料的一緻性目的,有的朋友可能會質疑,如果緩存系統挂了怎麼辦,這樣資料庫更新了但是緩存沒有更新,沒有達到一緻性的狀态。

解決問題的思路是:

如果緩存是因為網絡問題沒有更新成功資料,那麼建議重試幾次,如果依然沒有更新成功則認為緩存系統出錯不可用,這時候用戶端會将資料的KEY插入到消息系統中,消息系統可以過濾相同的KEY,隻需保證消息系統不存在相同的KEY,當緩存系統恢複可用的時候,依次從mq中取出KEY值然後從資料庫中讀取最新的資料更新緩存。

注意:更新緩存之前,緩存中依然有舊資料,是以不會造成緩存穿透。

下圖展示了整個思路的過程:

Paste_Image.png

看完上面的方案以後,又會有不少朋友提出疑問,如果我是第一次使用緩存或者緩存中暫時沒有我需要的資料,那又該如何處理呢?

解決問題的思路:

在這種場景下,用戶端從緩存中根據KEY讀取資料,如果讀到了資料則流程結束,如果沒有讀到資料(可能會有多個并發都沒有讀到資料),這時候使用緩存系統中的setNX方法設定一個值(這種方法類似加個鎖),沒有設定成功的請求則sleep一段時間,設定成功的請求讀取資料庫擷取值,如果擷取到則更新緩存,流程結束,之前sleep的請求這時候喚醒後直接再從緩存中讀取資料,此時流程結束。

在看完這個流程後,我想這裡面會有一個漏洞,如果資料庫中沒有我們需要的資料該怎麼處理,如果不處理則請求會造成死循環,不斷的在緩存和資料庫中查詢,這時候我們會沿用我之前文章中的如果沒有讀到資料則往緩存中插入一個NULL字元串的思路,這樣其他請求直接就可以根據“NULL”進行處理,直到背景系統在資料庫成功插入資料後同步更新清理NULL資料和更新緩存。

流程圖如下所示:

總結:

在實際工作中,我們往往将上面二個方案組合使用才能達到最佳效果,雖然第二種方案也會造成請求阻塞,但是隻是在第一次使用或者緩存暫時沒有資料的情況下才會産生,在生産中經過檢驗在TPS沒有上萬的情況下是不會造成問題的。

三、熱點緩存解決方案

1、緩存使用背景:

我們拿使用者中心的一個案例來說明:

每個使用者都會首先擷取自己的使用者資訊,然後再進行其他相關的操作,有可能會有如下一些場景情況:

  • 會有大量相同使用者重複通路該項目。
  • 會有同一使用者頻繁通路同一子產品。

2、思路解析

  • 因為使用者本身是不固定的而且使用者數量也有幾百萬尤其上千萬,我們不可能把所有的使用者資訊全部緩存起來,通過第一個場景情況可以看到一些規律,那就是有大量的相同使用者重複通路,但是究竟是哪些使用者重複通路我們也并不知道。
  • 如果有一個使用者頻繁重新整理讀取項目,那麼對資料庫本身也會造成較大壓力,當然我們也會有相關的保護機制來确實惡意攻擊,可以從前端控制,也可以有采黑名單等機制,這裡不在贅述。如果用緩存的話,我們又該如何控制同一使用者繁重讀取使用者資訊呢。

請看下圖:

我們會通過緩存系統做一個排序隊列,比如1000個使用者,系統會根據使用者的通路時間更新使用者資訊的時間,越是最近通路的使用者排名越排前,系統會定期過濾掉排名最後的200個使用者,然後再從資料庫中随機取出200個使用者加入隊列,這樣請求每次到達的時候,會先從隊列中擷取使用者資訊,如果命中則根據userId,再從另一個緩存資料結構中讀取使用者資訊,如果沒有命中則說明該使用者請求頻率不高。

JAVA僞代碼如下所示:

for (int i = 0; i < times; i++) {
            user = new ExternalUser();
            user.setId(i+"");
            user.setUpdateTime(new Date(System.currentTimeMillis()));
            CacheUtil.zadd(sortKey, user.getUpdateTime().getTime(), user.getId());
            CacheUtil.putAndThrowError(userKey+user.getId(), JSON.toJSONString(user));
        }
        
        Set<String> userSet = CacheUtil.zrange(sortKey, 0, -1);
        System.out.println("[sortedSet] - " + JSON.toJSONString(userSet) );
        if(userSet == null || userSet.size() == 0)
            return;
        
        Set<Tuple> userSetS = CacheUtil.zrangeWithScores(sortKey, 0, -1);
        StringBuffer sb = new StringBuffer();
        for(Tuple t:userSetS){
            sb.append("{member: ").append(t.getElement()).append(", score: ").append(t.getScore()).append("}, ");
        }
        
        System.out.println("[sortedcollect] - " + sb.toString().substring(0, sb.length() - 2));
        
        Set<String> members = new HashSet<String>();
        for(String uid:userSet){
            String key = userKey + uid;
            members.add(uid);
            ExternalUser user2 = CacheUtil.getObject(key, ExternalUser.class);
            System.out.println("[user] - " + JSON.toJSONString(user2) );
        }
        System.out.println("[user] - "  + System.currentTimeMillis());
        
        String[] keys = new String[members.size()];
        members.toArray(keys);
        
        Long rem = CacheUtil.zrem(sortKey, keys);
        System.out.println("[rem] - " + rem);
        userSet = CacheUtil.zrange(sortKey, 0, -1);
        System.out.println("[remove - sortedSet] - " + JSON.toJSONString(userSet));