談談redis緩存三大問題(一)- 緩存穿透

這幾天抽時間和大家一起聊聊redis緩存在生產環境使用中的幾大問題,以及如何去優化代碼去避免他!

首先我們來看看緩存使用過程的一個問題:緩存穿透

何爲緩存穿透?
如下是一個正常的業務流程圖:比如我一個正常的中國移動用戶(非廣告)進行一個業務請求,比如根據手機號去查賬單信息,正常流程是,手機號輸入進去,調用http請求,服務端收到請求後先根據手機號關鍵字去查詢緩存,如果查到了直接返回,如果查不到去查一下數據庫。試想一下移動這麼大用戶羣體,如果每個人請求來了都直接查數據庫,那麼數據庫壓力可想而知!
在這裏插入圖片描述
那麼這跟緩存穿透有什麼關係呢?
如下:比如哪天老王覺得移動不爽了,搞個代理ip服務器,然後寫個程序隨機生成非移動手機號,然後去查 “賬單” ,很明顯這賬單是不是一直不存在的,那麼緩存是不是會一直查不到!很明顯是,那麼這麼多請求都會全跑到數據庫上,要是老王搞個幾十臺機器,整個浙江省移動的數據庫怕是也扛不住這麼大壓力的!
在這裏插入圖片描述
如上只是一個簡單的小案例說明下什麼是緩存穿透!既然有問題,那麼肯定是有解決方案的!
沒錯,現在業界用的比較多的,應對這種緩存穿透的方案就是使用過濾器!那麼何爲過濾器呢?
如下模型:首先講數據庫數據全部緩存起來,然後在查詢緩存後,如果緩存不存在,先看下過濾器,如果過濾器中存在,說明數據庫中有相關賬號信息,可以進行查詢操作,如果不存在,那麼直接返回即可!
在這裏插入圖片描述
好!現在我們知道過濾器是什麼了,那麼就好辦了,直接搞個jvm緩存不就好了!但是也不好辦,一般熱點數據,幾十甚至上百G,直接緩存在服務器內存裏,顯然是不可能實現的!
那麼怎麼辦呢!其實有方法,現在市面上也有很多成品的過濾器工具給我們直接使用,比如布隆過濾器!有一個google開發的叫guava的項目,就幫我們實現了布隆過濾器,我們拿下來可以直接使用,當然這個項目還有很多其他功能,這邊不做贅述!
在這裏插入圖片描述
pom中直接引用如下依賴即可

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>19.0</version>
</dependency>
//設置bloom filter 初始化大小,可以
private static final Integer size = 1000000;

//設置bloom filter 的誤判率,該該值必須設置,如果設爲0會報錯,誤判率越低,消耗的內存越多,誤判率越高,消耗的內存越少
private static final double fpp = 0.01;

private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(),size,fpp);

public static void main(String[] args) {
    for (int i=0;i<size;i++){
        bloomFilter.put(i);
    }
    List<Integer> result = new ArrayList<>();
    for (int i=size;i<size+10000;i++){
       if (bloomFilter.mightContain(i)){
           result.add(i);
       }
    }
    System.out.println(String.format("bloom filter error result size:[%s]",result.size()));
}

在這裏插入圖片描述
如上測試方法,我們定義bloom filter ,往裏面插入1000000數據,然後設置誤判率爲0.01,然後去取10000個不在bloom filter裏面的數據,發現取出來的數據是98個,這說明符合我們設置的誤判率!在我們生產環境中,比如我們設置容錯率0.01,那麼10000個請求,只有十個最終會誤查數據庫,這個比例其實是完全可以接受的!

關於bloom工作原理,其實我們可以把他理解爲我們java裏面的list,只是這個list底層採用的是BitSet數組結構,Java BitSet可以按位存儲,計算機中一個字節(byte)佔8位(bit),而BitSet是位操作的對象,值只有0或1(即true 和 false),基本原理是,用1位來表示一個數據是否出現過,1表示出現過,0爲沒有出現過。使用用的時候既可根據某一個是否爲0表示此數是否出現過。內部維護一個long數組,初始化只有一個long segement,所以BitSet最小的size是64;隨着存儲的元素越來越多,BitSet內部會自動擴充,一次擴充64位,最終內部是由N個long segement 來存儲,默認情況下,BitSet所有位都是0即false;一個1G的空間,有 8102410241024=8.5810^9bit,也就是可以表示85億個不同的數。這個數據量,相信對於絕大多數企業來說是完全夠用的!

詳細工作原理(模型)如下:
在這裏插入圖片描述
如圖:
1代表bloom filter的bitset,初始值都是0,且支撐動態擴展(數組長度根據期望容錯率來動態生成,期望容錯率越低,需要的動態數組長度越大,消耗內存越多)
2代表向bloom filter中添加一個元素,三根線代表三個hash函數(hash函數個數可以根據期望容錯率動態設置,期望容錯率越低,hash函數越個數多,時間複雜度越高),每個hash計算出一個下標,然後將bitset中對應下標的位改成1
3,表示判斷一個元素是否存在於bloom filter,當有一個值需要判斷時,會使用跟put時候完全一樣的三個hash函數,然後判斷對應下標是否爲1,只有都爲1的時候,才認爲可能存在(這裏僅僅是可能存在,存在誤判問題),如果只要有一個下標爲0,則認爲肯定不存在。
在這裏插入圖片描述
在這裏插入圖片描述
如上圖,我添加大概20十幾個value到bloom filter,然後右邊做判斷,可以看到圖1,我1右邊是沒有添加過的,但是會判斷是可能存在,原因就是因爲:hash衝突。導致1計算的hash值下標都是1,。
但是圖二,只要計算出來有一個下標值爲0,則認爲肯定不存在了。
在這裏插入圖片描述
如上圖,在創建bloom filter的時候:com.google.common.hash.BloomFilter#create(com.google.common.hash.Funnel<? super T>, long, double, com.google.common.hash.BloomFilter.Strategy) 就會去根據你的期望容錯率和期望數據量,計算一個初始化的bitset大小,以及需要hash的hash函數個數。

雖然bloom filter看起來好像是可以解決相關緩存穿透的問題,但是缺點也很明顯:
1,維護麻煩,每次數據庫數據新增修改,都需要維護bloom filter
2,只能新增不能刪除,因爲存在hash衝突,如果刪除有可能存在誤刪(例:如果數值a1和a2計算結果都有一個共同的index,如果刪除a1的時候需要吧index的位置置爲0,這樣相當於把a2也給刪除了,所以是不可取的)
3,這種實現只支持jvm緩存,簡單的說就是不支持分佈式,等於如果你一個微服務有十臺機器,那麼同樣的緩存就是10分完全一樣的(後面找機會寫寫基於redis的分佈式bloom過濾器)

好了關於緩存穿透的相關問題就先說到這,有不當之處歡迎留言指正!

發佈了47 篇原創文章 · 獲贊 97 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章