從 Bitmap 到布隆過濾器,再到高併發緩存設計策略!

前言:怎麼能把風馬牛不相及的概念串在一塊,就得看筆者的本事了。

bitmap和布隆過濾器

海量整數中是否存在某個值--bitmap

在一個程序中,經常有讓我們判斷一個集合中是否存在某個數的case;大多數情況下,只需要用map或是list這樣簡單的數據結構,如果使用的是高級語言,還能乘上快車調用幾個封裝好的api,加幾個if else,兩三行代碼就可以在控制檯看自己“完美”而又“健壯”的代碼跑起來了。

但是,事無完美,在高併發環境下,所有的case都會極端化,如果這是一個十分龐大的集合(給這個龐大一個具體的值吧,一個億),簡單的一個hash map,不考慮鏈表所需的指針內存空間,一億個int類型的整數,就需要380多M(4byte × 10 ^8),十億的話就是4個G,不考慮性能,光算算這內存開銷,即使現在滿地都是128G的服務器,也不好喫下這一壺。

bitmap則使用位數代表數的大小,bit中存儲的0或者1來標識該整數是否存在,具體模型如下:

這是一個能標識0-9的“bitmap”,其中4321這四個數存在

計算一下bitmap的內存開銷,如果是1億以內的數據查找,我們只需要1億個bit = 12MB左右的內存空間,就可以完成海量數據查找了,是不是極其誘人的一個內存縮減,以下爲Java實現的bitmap代碼:

public class MyBitMap {
 
    private byte[] bytes;
    private int initSize;
 
    public MyBitMap(int size) {
        if (size <= 0) {
            return;
        }
        initSize = size / (8) + 1;
        bytes = new byte[initSize];
    }
 
    public void set(int number) {
        //相當於對一個數字進行右移動3位,相當於除以8
        int index = number >> 3;
        //相當於 number % 8 獲取到byte[index]的位置
        int position = number & 0x07;
        //進行|或運算  參加運算的兩個對象只要有一個爲1,其值爲1。
        bytes[index] |= 1 << position;
    }
 
 
    public boolean contain(int number) {
        int index = number >> 3;
        int position = number & 0x07;
        return (bytes[index] & (1 << position)) != 0;
    }
 
    public static void main(String[] args) {
        MyBitMap myBitMap = new MyBitMap(32);
        myBitMap.set(30);
        myBitMap.set(13);
        myBitMap.set(24);
        System.out.println(myBitMap.contain(2));
    }
 
}

使用簡單的byte數組和位運算,就能做到時間與空間的完美均衡,是不是美美噠,wrong!試想一下,如果我們明確這是一個一億以內,但是數量級只有10的集合,我們使用bitmap,同樣需要開銷12M的數據,如果是10億以內的數據,開銷就會漲到120M,bitmap的空間開銷永遠是和他的數據取值範圍掛鉤的,只有在海量數據下,他才能夠大顯身手。

再說說剛剛提到的那個極端case,假設這個數據量在一千萬,但是取值範圍好死不死就在十個億以內,那我們不可避免還是要面對120M的開銷,有方法應對麼?

布隆過濾器

如果面對筆者說的以上問題,我們結合一下常規的解決方案,譬如說hash一下,我將十億以內的某個數據,hash成一億內的某個值,再去bitmap中查怎麼樣,如下圖,布隆過濾器就是這麼幹的:

利用多個hash算法得到的值,減小hash碰撞的概率

像上面的圖注所說,我們可以利用多個hash算法減小碰撞概率,但只要存在碰撞,就一定會有錯誤判斷,我們無法百分百確定一個值是否真的存在,但是hash算法的魅力在於,我不能確定你是否存在,但是我可以確定你是否真的不存在,這也就是以上的實現爲什麼稱之“過濾器”的原因了。

高併發緩存設計策略

why cache??

如果讀者是一個計算機專業的同學,cache這個詞應該是能達到讓耳朵起繭的出現頻次。在計算機體系中,cache是介於cpu以及內存之間,用來緩和cpu和內存處理速度差距的那麼一個和事佬;在OS中,page cache又是內存和IO之間的和事佬。

cache是個和事老??聽着似乎怪怪的,但是也蠻形象的啦。

前面講了大半截的算法理論,爲了防止讀者犯困,直接進入下半部分主題,高併發緩存設計。

即使是在軟件層,我們同樣需要這麼一個和事老,從最簡單的服務架構開始,通常我們在服務端發起請求,然後CURD某個關係型數據庫例如Mysql。但是,類似這樣的架構都需要有一個磁盤作爲終端持久化,即使增加索引,使用B+樹的這種數據結構進行優化查詢,效率還是會卡在需要頻繁尋道的IO上。

這個時候,一個和事老的作用就十分明顯了,我們會添加一些內存操作,來緩和IO處理速度慢帶來的壓力。cache is not a problem,how to use it is actually a problem。

緩存一致性問題

緩存處理的機制有以下幾種:

  • cache aside;
  • read through;
  • write through;
  • write behind caching;

緩存穿透問題

所謂的緩存擊穿,就是當請求發出,而無法在緩存中讀到數據時,請求還是會作用到database,這樣的話,緩存減壓的效果就不復存在了。

設想這麼一個場景,如果一個用戶,使用大流量惡意頻繁地去查詢一條數據庫中沒有的記錄,一直擊穿緩存,勢必會把database打死,如何避免緩存擊穿,這就是一個問題了。

有兩種方案,第一種,在緩存中添加空值,如果在database中查詢無果,我們大可以把值設置爲null,防止下次再次訪問數據庫,這樣做簡單便捷,但是多少有些浪費空間。

第二種方案,就是使用布隆過濾器(點題),在cache與web服務器中間加一層布隆過濾器,對訪問的key做記錄,如此以來,同樣可以解決緩存擊穿的問題。

緩存雪崩問題

緩存雪崩發生於在某個時間點,緩存同時失效,例如緩存設置了失效時間,這會聯動的導致大量緩存擊穿問題。

加分佈式鎖是一種解決方案,只有拿到鎖的請求才能訪問database。但是這樣治標不治本,當請求量過多時,大量的線程阻塞,也會把內存撐壞的。

預熱數據,分散地設置失效時間,這樣可以減少緩存雪崩發生的概率。

提高緩存可用性,cache的單點一樣是會是緩存雪崩的隱患,大部分緩存中間件都提供高可用架構,如redis的主從+哨兵架構。

原文鏈接:https://blog.csdn.net/that_is_cool/article/details/91346356

版權聲明:本文爲CSDN博主「that_is_cool」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2021最新版)

2.終於靠開源項目弄到 IntelliJ IDEA 激活碼了,真香!

3.阿里 Mock 工具正式開源,幹掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式發佈,全新顛覆性版本!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!

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