1. 程式人生 > >快取穿透、快取併發、熱點快取解決方案

快取穿透、快取併發、熱點快取解決方案

快取穿透、快取併發、熱點快取

一、前言

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

說明:本文中提到的快取可以理解為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));

一、前言

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

說明:本文中提到的快取可以理解為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));