1.拋磚引玉
有些項目中,緩存可能是這樣設計的:
前端用戶查詢數據時:
- 先去緩存或nosql(redis mongodb等)里面查。如果能找到,就直接把數據返回給用戶。
- 如果緩存里面也沒有(緩存沒命中),才去數據庫中查找。
上面這個設計的目的,是為了用緩存給mysql降低訪問壓力。
緩存命中率越高, 需要查詢mysql的可能性就越小,mysql壓力就越小。
那么現在問題來了, 如果攻擊者,經常查詢一些不會存在的數據, 比如查詢商品id= -1,那么緩存里面不可能會有商品id=-1的數據,緩存沒命中, 最終要到mysql里面查詢, 加重了mysql的負擔。 繞過緩存給你的mysql增加訪問壓力, 構成了緩存穿透, 嚴重的可能直接壓爆數據庫。
解決:
其實,我們可以在訪問mysql之前,先訪問一下布隆過濾器。布隆過濾器能夠判斷某個值的存在情況, 如果布隆過濾器說-1這個值不存在, 那么這個肯定就不存在,這時候, 我們就沒必要訪問mysql了。
這樣就成功把這些對mysql的惡意攻擊進行過濾。
2.布隆過濾器場景
- Google著名的分布式數據庫Bigtable以及Hbase使用了布隆過濾器來查找不存在的行或列,以減少磁盤查找的IO次數。
- 檢查垃圾郵件地址 假定我們存儲一億個電子郵件地址,我們先建立一個十六億二進制(比特),即兩億字節的向量,然后將這十六億個二進制全部設置為零。對於每一個電子郵件地址 X,我們用八個不同的隨機數產生器(F1,F2,...,F8) 產生八個信息指紋(f1, f2, ..., f8)。再用一個隨機數產生器 G 把這八個信息指紋映射到 1 到十六億中的八個自然數 g1, g2, ...,g8。現在我們把這八個位置的二進制全部設置為一。當我們對這一億個 email 地址都進行這樣的處理后。一個針對這些 email 地址的布隆過濾器就建成了。
- Google chrome 瀏覽器使用bloom filter識別惡意鏈接(能夠用較少的存儲空間表示較大的數據集合,簡單的想就是把每一個URL都可以映射成為一個bit)
- 文檔存儲檢索系統也采用布隆過濾器來檢測先前存儲的數據
- 爬蟲URL地址去重 A,B 兩個文件,各存放 50 億條 URL,每條 URL 占用 64 字節,內存限制是 4G,讓你找出 A,B文件共同的 URL。如果是三個乃至 n 個文件呢? 分析 :如果允許有一定的錯誤率,可以使用 Bloom filter,4G 內存大概可以表示 340 億 bit。將其中一個文件中的 url 使用 Bloom filter 映射為這 340 億 bit,然后挨個讀取另外一個文件的 url,檢查是否與 Bloom filter,如果是,那么該 url 應該是共同的 url(注意會有一定的錯誤率)。
- 解決緩存穿透問題 緩存穿透是指查詢一個一定不存在的數據,由於緩存是不命中時被動寫的,並且出於容錯考慮,如果從存儲層查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到存儲層去查詢,失去了緩存的意義。在流量大時,可能DB就掛掉了,要是有人利用不存在的key頻繁攻擊我們的應用,這就是漏洞。
其它:
- 在比特幣中用來判斷是不是屬於錢包
- 敏感詞判斷,在布隆過濾器中查找敏感詞
3.布隆過濾器原理
3.1 常規思路(不使用布隆過濾器如何解決數據過濾問題)
- 數組
- 哈希表
雖然上面描述的這幾種數據結構配合常見的排序、二分搜索可以快速高效的處理絕大部分判斷元素是否存在集合中的需求。但是當集合里面的元素數量足夠大,如果有500萬條記錄甚至1億條記錄呢?這個時候常規的數據結構的問題就凸顯出來了。數組、哈希等數據結構會存儲元素的內容,一旦數據量過大,消耗的內存也會呈現線性增長,最終達到瓶頸。有的可能會問,哈希表不是效率很高嗎?查詢效率可以達到O(1)。但是哈希表需要消耗的內存依然很高。
3.2 哈希函數
哈希函數的概念是:將任意大小的數據轉換成特定大小的數據的函數,轉換后的數據稱為哈希值或哈希編碼。下面是一幅示意圖:
可以明顯的看到,原始數據經過哈希函數的映射后稱為了一個個的哈希編碼,數據得到壓縮。哈希函數是實現哈希表和布隆過濾器的基礎。
試驗:
分別往BloomFilter和HashSet中插入UUID,總計插入100w個UUID,BloomFilter誤判率為默認值0.03。每插入5w個統計下各自的占用空間。結果如下,橫軸是數據量大小,縱軸是存儲空間,單位kb。
可以看到BloomFilter存儲空間一直都沒有變,這里和它的實現有關,事實上你在告訴它總共要插入多少條數據時BloomFilter就計算並申請好了內存空間,所以BloomFilter占用內存不會隨插入數據的多少而變化。相反,HashSet在插入數據越來越多時,其占用的內存空間也會越來越多,最終在插入完100w條數據后,其內存占用為BloomFilter的100多倍。
3.3 布隆過濾器原理
布隆過濾器(Bloom Filter)的核心實現是一個超大的位數組和幾個哈希函數。假設位數組的長度為m,哈希函數的個數為k
以上圖為例,具體的操作流程:假設集合里面有3個元素{x, y, z},哈希函數的個數為3。首先將位數組進行初始化,將里面每個位都設置位0。對於集合里面的每一個元素,將元素依次通過3個哈希函數進行映射,每次映射都會產生一個哈希值,這個值對應位數組上面的一個點,然后將位數組對應的位置標記為1。查詢W元素是否存在集合中的時候,同樣的方法將W通過哈希映射到位數組上的3個點。如果3個點的其中有一個點不為1,則可以判斷該元素一定不存在集合中。反之,如果3個點都為1,則該元素可能存在集合中。注意:此處不能判斷該元素是否一定存在集合中,可能存在一定的誤判率。可以從圖中可以看到:假設某個元素通過映射對應下標為4,5,6這3個點。雖然這3個點都為1,但是很明顯這3個點是不同元素經過哈希得到的位置,因此這種情況說明元素雖然不在集合中,也可能對應的都是1,這是誤判率存在的原因。
3.3.1 添加元素
- 將要添加的元素給k個哈希函數
- 得到對應於位數組上的k個位置
- 將這k個位置設為1
3.3.2 查詢元素
- 將要查詢的元素給k個哈希函數
- 得到對應於位數組上的k個位置
- 如果k個位置有一個為0,則肯定不在集合中
- 如果k個位置全部為1,則可能在集合中
3.4 布隆過濾器特點
- 巴頓.布隆於一九七零年提出
- 一個很長的二進制向量 (位數組)
- 一系列隨機函數 (哈希)
- 空間效率和查詢效率高
- 有一定的誤判率(哈希表是精確匹配)
3.5 布隆過濾器誤判率統計
如果m代表位向量長度,n代表需要判斷的元素個數, k代表hash函數個數。
下表是m與n比值在k個hash函數下面的誤判率
誤判率統計:
m/n | k | k=1 | k=2 | k=3 | k=4 | k=5 | k=6 | k=7 | k=8 |
---|---|---|---|---|---|---|---|---|---|
2 | 1.39 | 0.393 | 0.400 | ||||||
3 | 2.08 | 0.283 | 0.237 | 0.253 | |||||
4 | 2.77 | 0.221 | 0.155 | 0.147 | 0.160 | ||||
5 | 3.46 | 0.181 | 0.109 | 0.092 | 0.092 | 0.101 | |||
6 | 4.16 | 0.154 | 0.0804 | 0.0609 | 0.0561 | 0.0578 | 0.0638 | ||
7 | 4.85 | 0.133 | 0.0618 | 0.0423 | 0.0359 | 0.0347 | 0.0364 | ||
8 | 5.55 | 0.118 | 0.0489 | 0.0306 | 0.024 | 0.0217 | 0.0216 | 0.0229 | |
9 | 6.24 | 0.105 | 0.0397 | 0.0228 | 0.0166 | 0.0141 | 0.0133 | 0.0135 | 0.0145 |
10 | 6.93 | 0.0952 | 0.0329 | 0.0174 | 0.0118 | 0.00943 | 0.00844 | 0.00819 | 0.00846 |
11 | 7.62 | 0.0869 | 0.0276 | 0.0136 | 0.00864 | 0.0065 | 0.00552 | 0.00513 | 0.00509 |
12 | 8.32 | 0.08 | 0.0236 | 0.0108 | 0.00646 | 0.00459 | 0.00371 | 0.00329 | 0.00314 |
13 | 9.01 | 0.074 | 0.0203 | 0.00875 | 0.00492 | 0.00332 | 0.00255 | 0.00217 | 0.00199 |
14 | 9.7 | 0.0689 | 0.0177 | 0.00718 | 0.00381 | 0.00244 | 0.00179 | 0.00146 | 0.00129 |
15 | 10.4 | 0.0645 | 0.0156 | 0.00596 | 0.003 | 0.00183 | 0.00128 | 0.001 | 0.000852 |
16 | 11.1 | 0.0606 | 0.0138 | 0.005 | 0.00239 | 0.00139 | 0.000935 | 0.000702 | 0.000574 |
17 | 11.8 | 0.0571 | 0.0123 | 0.00423 | 0.00193 | 0.00107 | 0.000692 | 0.000499 | 0.000394 |
18 | 12.5 | 0.054 | 0.0111 | 0.00362 | 0.00158 | 0.000839 | 0.000519 | 0.00036 | 0.000275 |
19 | 13.2 | 0.0513 | 0.00998 | 0.00312 | 0.0013 | 0.000663 | 0.000394 | 0.000264 | 0.000194 |
20 | 13.9 | 0.0488 | 0.00906 | 0.0027 | 0.00108 | 0.00053 | 0.000303 | 0.000196 | 0.00014 |
21 | 14.6 | 0.0465 | 0.00825 | 0.00236 | 0.000905 | 0.000427 | 0.000236 | 0.000147 | 0.000101 |
22 | 15.2 | 0.0444 | 0.00755 | 0.00207 | 0.000764 | 0.000347 | 0.000185 | 0.000112 | 7.46e-05 |
23 | 15.9 | 0.0425 | 0.00694 | 0.00183 | 0.000649 | 0.000285 | 0.000147 | 8.56e-05 | 5.55e-05 |
24 | 16.6 | 0.0408 | 0.00639 | 0.00162 | 0.000555 | 0.000235 | 0.000117 | 6.63e-05 | 4.17e-05 |
25 | 17.3 | 0.0392 | 0.00591 | 0.00145 | 0.000478 | 0.000196 | 9.44e-05 | 5.18e-05 | 3.16e-05 |
26 | 18 | 0.0377 | 0.00548 | 0.00129 | 0.000413 | 0.000164 | 7.66e-05 | 4.08e-05 | 2.42e-05 |
27 | 18.7 | 0.0364 | 0.0051 | 0.00116 | 0.000359 | 0.000138 | 6.26e-05 | 3.24e-05 | 1.87e-05 |
28 | 19.4 | 0.0351 | 0.00475 | 0.00105 | 0.000314 | 0.000117 | 5.15e-05 | 2.59e-05 | 1.46e-05 |
29 | 20.1 | 0.0339 | 0.00444 | 0.000949 | 0.000276 | 9.96e-05 | 4.26e-05 | 2.09e-05 | 1.14e-05 |
30 | 20.8 | 0.0328 | 0.00416 | 0.000862 | 0.000243 | 8.53e-05 | 3.55e-05 | 1.69e-05 | 9.01e-06 |
31 | 21.5 | 0.0317 | 0.0039 | 0.000785 | 0.000215 | 7.33e-05 | 2.97e-05 | 1.38e-05 | 7.16e-06 |
32 | 22.2 | 0.0308 | 0.00367 | 0.000717 | 0.000191 | 6.33e-05 | 2.5e-05 | 1.13e-05 | 5.73e-06 |
大家可以根據上表,結合需求進行設計一個和需求吻合又不怎么占用資源的布隆過濾器
4.布隆過濾器設計分析
以mysql+ 布隆過濾器為例
4.1 總體圖:
4.2 圖解:
新增數據:
查詢數據:
5.布隆過濾器實現
5.1用redis插件bloom-filter實現
下載最新的包
wget https://github.com/RedisBloom/RedisBloom/archive/refs/tags/v2.2.4.tar.gz
解壓包
tar -zxvf v2.2.4.tar.gz
進入文件然后安裝,生成so文件
cd RedisBloom-2.2.4
make
將模塊加載到redis.conf文件里
vim redis.conf
.
.
.
#加上模塊
loadmodule /root/RedisBloom-2.2.4/redisbloom.so
保存后,重啟服務即可
布隆過濾器主要命令:
bf.add 添加元素
bf.exists 查詢元素是否存在
bf.madd 一次添加多個元素
在 redis 中有兩個值決定布隆過濾器的准確率:
error_rate:允許布隆過濾器的錯誤率,這個值越低過濾器的位數組的大小越大,占用空間也就越大。
initial_size:布隆過濾器可以儲存的元素個數,當實際存儲的元素個數超過這個值之后,過濾器的准確率會下降。
redis 中有一個命令可以來設置這兩個值:
bf.reserve test 0.1 100000000
第一個值是過濾器的名字。
第二個值為 error_rate 的值。
第三個值為 initial_size 的值。
注意必須在add之前使用bf.reserve指令顯式創建,如果對應的 key 已經存在,bf.reserve會報錯。同時設置的錯誤率越低,需要的空間越大。如果不使用 bf.reserve,默認的error_rate是 0.01,默認的initial_size是 100。
示例:
#新建一個過濾器
bf.reserve test 0.1 100000000 # test是布隆過濾器名稱,0.1是誤判率,100000000是位向量長度
#向過濾器中添加元素
127.0.0.1:6379> bf.add test abc123 #test是布隆過濾器名稱,abc123是需要判斷的元素
#判斷元素是否在過濾器中
127.0.0.1:6379> bf.exists test abc123 #test是布隆過濾器名稱,abc123是需要判斷的元素
1
PHP代碼就簡單一點跟執行redis命令一樣即可:
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$re = $redis->rawCommand('bf.exists', 'qqq', 'nihao');
var_dump($re);