1. 程式人生 > >緩存穿透、緩存並發、熱點緩存解決方案

緩存穿透、緩存並發、熱點緩存解決方案

err 排名 發的 || 多個 總結 繼續 怎麽辦 請求

一、前言

在之前的一篇緩存穿透、緩存並發、緩存失效之思路變遷文章中介紹了關於緩存穿透、並發的一些常用思路,但是個人感覺文章中沒有明確一些思路的使用場景,本文繼續將繼續深化與大家共同探討,同時也非常感謝這段時間給我提寶貴建議的朋友們。

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

二、緩存穿透與並發方案

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

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

當並發較高的時候,其實我是不建議使用緩存過期這個策略的,我更希望緩存一直存在,通過後臺系統來更新緩存系統中的數據達到數據的一致性目的,有的朋友可能會質疑,如果緩存系統掛了怎麽辦,這樣數據庫更新了但是緩存沒有更新,沒有達到一致性的狀態。

解決問題的思路是
如果緩存是因為網絡問題沒有更新成功數據,那麽建議重試幾次,如果依然沒有更新成功則認為緩存系統出錯不可用,這時候客戶端會將數據的KEY插入到消息系統中,消息系統可以過濾相同的KEY,只需保證消息系統不存在相同的KEY,當緩存系統恢復可用的時候,依次從mq中取出KEY值然後從數據庫中讀取最新的數據更新緩存。
註意:更新緩存之前,緩存中依然有舊數據,所以不會造成緩存穿透。

下圖展示了整個思路的過程:
技術分享圖片

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

解決問題的思路
在這種場景下,客戶端從緩存中根據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));

緩存穿透、緩存並發、熱點緩存解決方案