1、使用場景java
1.布隆過濾器的特性是:去重,多數去重場景都跟這個特性有關。好比爬蟲的時候去掉相同的URL,推送消息去掉相同的消息等。git
2.解決緩存擊穿的問題。github
3.反垃圾郵件,從數十億個垃圾郵件列表中判斷某郵箱是否垃圾郵箱(同理,垃圾短信).web
2、概念redis
其內部維護一個全爲0的bit數組,須要說明的是,布隆過濾器有一個誤判率的概念,誤判率越低,則數組越長,所佔空間越大。誤判率越高則數組越小,所佔的空間越小。算法
咱們能夠經過一個int型的整數的32比特位來存儲32個10進制的數字,那麼這樣所帶來的好處是內存佔用少、效率很高(不須要比較和位移)好比咱們要存儲5(101)、3(11)四個數字,那麼咱們申請int型的內存空間,會有32個比特位。這四個數字的二進制分別對應從右往左開始數,好比第一個數字是5,對應的二進制數據是101, 那麼從右往左數到第5位,把對應的二進制數據存儲到32個比特位上。spring
第一個5就是 00000000000000000000000000101000docker
輸入3時候 00000000000000000000000000001100數據庫
如何生成一個布隆過濾器?數組
原理以下假設集合裏面有3個元素{x, y, z},哈希函數的個數爲3。首先將位數組進行初始化,將裏面每一個位都設置位0。對於集合裏面的每個元素,將元素依次經過3個哈希函數進行映射,每次映射都會產生一個哈希值,這個值對應位數組上面的一個點,而後將位數組對應的位置標記爲1。接下來按照該方法處理全部的輸入對象,每一個對象均可能把bitMap中一些白位置塗黑,也可能會遇到已經塗黑的位置,遇到已經爲黑的讓他繼續爲黑便可。處理完全部的輸入對象以後,在bitMap中可能已經有至關多的位置已經被塗黑。至此,一個布隆過濾器生成完成,這個布隆過濾器表明以前全部輸入對象組成的集合。(向布隆過濾器中添加 key 時,會使用多個 hash 函數對 key 進行 hash 算得一個整數索引值而後對位數組長度進行取模運算獲得一個位置,每一個 hash 函數都會算得一個不一樣的位置。再把位數組的這幾個位置都置爲 1 就完成了 add 操做。)
如何去判斷一個元素是否存在bit array中呢?
原理是同樣,根據k個哈希函數去獲得的結果,若是全部的結果都是1,表示這個元素可能(假設某個元素經過映射對應下標爲4,5,6這3個點。雖然這3個點都爲1,可是很明顯這3個點是不一樣元素通過哈希獲得的位置,所以這種狀況說明元素雖然不在集合中,也可能對應的都是1)存在。若是一旦發現其中一個比特位的元素是0,表示這個元素必定不存在至於k個哈希函數的取值爲多少,可以最大化的下降錯誤率(由於哈希函數越多,映射衝突會越少),這個地方就會涉及到最優的哈希函數個數的一個算法邏輯。(向布隆過濾器詢問 key 是否存在時,跟 add 同樣,也會把 hash 的幾個位置都算出來,看看位數組中這幾個位置是否都爲 1,只要有一個位爲 0,那麼說明布隆過濾器中這個 key 不存在。若是都是 1,這並不能說明這個 key 就必定存在,只是極有可能存在,由於這些位被置爲 1 多是由於其它的 key 存在所致。若是這個位數組比較稀疏,判斷正確的機率就會很大,若是這個位數組比較擁擠,判斷正確的機率就會下降。)
它的優勢是空間效率和查詢時間都遠遠超過通常的算法,缺點是有必定的誤識別率和刪除困難。
3、項目實戰
1.命令模式,Redis 官方提供的布隆過濾器到了 Redis 4.0 提供了插件功能以後。
可使用docker容器進行安裝,docker容器安裝能夠參照以前的文章。
# 拉取鏡像docker pull redislabs/rebloom# 運行容器docker run -p 6379:6379 redislabs/rebloom# 鏈接容器中的 redis 服務docker exec -it 1a7ca288bcbe redis-cli
命令:
bf.add boolean:aaron:test user1bf.add boolean:aaron:test user2bf.add boolean:aaron:test user3bf.exists boolean:aaron:test user1#批量操做bf.madd boolean:aaron:test user4 user5 user6bf.mexists boolean:aaron:test user4 user5 user6 user7
2.py代碼
import redis#redis 鏈接pool = redis.ConnectionPool(host='192.168.XXX.XXX', port=6379)r = redis.Redis(connection_pool=pool)#布隆過濾器def boolean_test():r.delete("boolean:aaron:test")#指定錯誤率r.execute_command("bf.reserve", "boolean:aaron:test", 0.001, 500000)#正式操做的命令 add 和 existsfor i in range(10000):r.execute_command("bf.add", "boolean:aaron:test", "user%d" % i)ret = r.execute_command("bf.exists", "boolean:aaron:test", "user%d" % i)if ret == 0:print(i)breakprint(ret)#主函數,執行行數if __name__ == '__main__':boolean_test()
3.Java代碼,以解決解決緩存擊穿的問題爲例。
4.3.1 闡述緣由和解決思路
什麼是緩存穿透?
正常狀況下,咱們去查詢數據都是存在。那麼請求去查詢一條壓根兒數據庫中根本就不存在的數據,也就是緩存和數據庫都查詢不到這條數據,可是請求每次都會打到數據庫上面去。這種查詢不存在數據的現象咱們稱爲緩存穿透。
穿透帶來的問題
試想一下,若是有黑客會對你的系統進行攻擊,拿一個不存在的id 去查詢數據,會產生大量的請求到數據庫去查詢。可能會致使你的數據庫因爲壓力過大而宕掉。(以前項目就是這樣作的,沒有考慮到!!!)
4.3.2 解決思路:
緩存空值
之因此會發生穿透,就是由於緩存中沒有存儲這些空數據的key。從而致使每次查詢都到數據庫去了。那麼咱們就能夠爲這些key對應的值設置爲null 丟到緩存裏面去。後面再出現查詢這個key 的請求的時候,直接返回null 。這樣,就不用在到數據庫中去走一圈了,可是別忘了設置過時時間。
BloomFilterBloomFilter
相似於一個hbase set 用來判斷某個元素(key)是否存在於某個集合中。這種方式在大數據場景應用比較多,好比 Hbase 中使用它去判斷數據是否在磁盤上。還有在爬蟲場景判斷url 是否已經被爬取過。這種方案能夠加在第一種方案中,在緩存以前在加一層 BloomFilter ,在查詢的時候先去 BloomFilter 去查詢 key 是否存在,若是不存在就直接返回,存在再走查緩存 -> 查 DB。
4.3.3 代碼
a.使用redisTemplate
經過查看網上資料和查看官網,還有直接導入源碼並無直接相關的API。有兩種方式能夠間接使用redisTemplate達到布隆過濾器。
第一種方式,集合Lua腳本。
package com.example.redis.zfr.demoredis.mq;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import java.util.Collections;@Controllerpublic class RedisController {@Autowiredprivate RedisTemplate<String, String> redisTemplate;/*** 布隆過濾器* @param id* @return*/@RequestMapping("/lua/{id}")public String sendLua(@PathVariable String id) {//添加key值String script = "return redis.call('bf.add',KEYS[1],ARGV[1])";DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);Boolean user4 = redisTemplate.execute(redisScript, Collections.singletonList("boolean:aaron:test"), String.valueOf("user"+id));System.out.println(user4);//判斷是否存在String scriptEx = "return redis.call('bf.exists',KEYS[1],ARGV[1])";DefaultRedisScript<Boolean> redisScript1 = new DefaultRedisScript<>(scriptEx, Boolean.class);Boolean user6 = redisTemplate.execute(redisScript1, Collections.singletonList("boolean:aaron:test"), String.valueOf("user"+id));System.out.println(user6);return "";}}
第一次打印結果是:true true 。
第二次打印結果是:false true 。由於user11已經存在了。
第二種方式,網上大部分實現方式,經過使用Google的布隆過濾器,而後結合redisTemplate。
Google布隆過濾器工具類:
1. pom座標
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>22.0</version></dependency>
2.代碼
package com.example.redis.zfr.demoredis.booleanfilter;import com.google.common.hash.BloomFilter;import com.google.common.hash.Funnels;import java.util.ArrayList;import java.util.List;/*** @author 繁榮Aaron*/public class GoogleTest {private static int size = 1000000;private static BloomFilter<Integer> bloomFilter =BloomFilter.create(Funnels.integerFunnel(), size);public static void main(String[] args) {for (int i = 0; i < size; i++) {bloomFilter.put(i);}List<Integer> list = new ArrayList<Integer>(1000);//故意取10000個不在過濾器裏的值,看看有多少個會被認爲在過濾器裏for (int i = size + 10000; i < size + 20000; i++) {if (bloomFilter.mightContain(i)) {list.add(i);}}System.out.println("誤判的數量:" + list.size());}}
b.Bloom filter library,須要使用到Jedis客戶端,將不重點介紹(實際項目使用的a方案的第一種方案)
資料地址:(https://github.com/Baqend/Orestes-Bloomfilter)
4、思考和總結
1.誤判率
隨着存入的元素數量增長,誤算率隨之增長。可是若是元素數量太少,則使用散列表足矣。
a.Google布隆過濾器
對於Google布隆過濾器來講,在不作任何設置的狀況下,默認的誤判率爲0.03,我想下降誤判率怎麼作?
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size,0.01);
即,此時誤判率爲0.01。在這種狀況下,其實咱們能夠調試跟蹤一下查看底層維護的bit數組的長度,得出的結論是:誤判率越低,則底層維護的數組越長,佔用空間越大。所以,誤判率實際取值,根據服務器所可以承受的負載來決定。
b.redis
其實咱們能夠在 add 以前使用bf.reserve指令顯式建立。若是對應的 key 已經存在,bf.reserve會報錯。bf.reserve有三個參數,分別是 key, error_rate和initial_size。錯誤率越低,須要的空間越大。initial_size參數表示預計放入的元素數量,當實際數量超出這個數值時,誤判率會上升。因此須要提早設置一個較大的數值避免超出致使誤判率升高。若是不使用 bf.reserve,默認的error_rate是 0.01,默認的initial_size是 100。相關命令推薦地址:(https://github.com/RedisLabsModules/redisbloom/blob/master/docs/Bloom_Commands.md)
BF.RESERVE <key> <error_rate> <size>
這將建立一個名爲<key>的過濾器,該過濾器最多可容納<size>項,目標錯誤率爲<error_rate>。一旦溢出原始<size>估計值,過濾器將自動增加。
#例子:bf.reserve boolean:aaron:test 0.001 50000
注意:
布隆過濾器的initial_size估計的過大,會浪費存儲空間,估計的太小,就會影響準確率,用戶在使用以前必定要儘量地精確估計好元素數量,還須要加上必定的冗餘空間以免實際元素可能會意外高出估計值不少。
布隆過濾器的error_rate越小,須要的存儲空間就越大,對於不須要過於精確的場合,error_rate設置稍大一點也無傷大雅。
2.空間佔用估計
布隆過濾器的空間佔用有一個簡單的計算公式,布隆過濾器有兩個參數,第一個是預計元素的數量 n,第二個是錯誤率 f。公式根據這兩個輸入獲得兩個輸出,第一個輸出是位數組的長度 l,也就是須要的存儲空間大小 (bit),第二個輸出是 hash 函數的最佳數量 k。hash 函數的數量也會直接影響到錯誤率,最佳的數量會有最低的錯誤率。
k=0.7*(l/n) # 約等於
f=0.6185^(l/n) # ^ 表示次方計算,也就是 math.pow
從公式中能夠看出:
1.位數組相對越長 (l/n),錯誤率 f 越低,這個和直觀上理解是一致的。
2.位數組相對越長 (l/n),hash 函數須要的最佳數量也越多,影響計算效率。
3.當一個元素平均須要 1 個字節 (8bit) 的指紋空間時 (l/n=8),錯誤率大約爲 2%
4.錯誤率爲 10%,一個元素須要的平均指紋空間爲 4.792 個 bit,大約爲 5bit。
5.錯誤率爲 1%,一個元素須要的平均指紋空間爲 9.585 個 bit,大約爲 10bit
6.錯誤率爲 0.1%,一個元素須要的平均指紋空間爲 14.377 個 bit,大約爲 15bit。
若是一個元素須要佔據 15 個 bit,那相對 set 集合的空間優點是否是就沒有那麼明顯了?這裏須要明確的是,set 中會存儲每一個元素的內容,而布隆過濾器僅僅存儲元素的指紋。元素的內容大小就是字符串的長度,它通常會有多個字節,甚至是幾十個上百個字節,每一個元素自己還須要一個指針被 set 集合來引用,這個指針又會佔去 4 個字節或 8 個字節,取決於系統是 32bit 仍是 64bit。而指紋空間只有接近 2 個字節,因此布隆過濾器的空間優點仍是很是明顯的。
3.用不上 Redis4.0 怎麼辦?
Redis 4.0 以前也有第三方的布隆過濾器 lib 使用,只不過在實現上使用 redis 的位圖來實現的,性能上也要差很多。好比一次 exists 查詢會涉及到屢次 getbit 操做,網絡開銷相比而言會高出很多。另外在實現上這些第三方 lib 也不盡完美,好比 pyrebloom 庫就不支持重連和重試,在使用時須要對它作一層封裝後才能在生產環境中使用。
py:https://github.com/robinhoodmarkets/pyreBloom
Java:https://github.com/Baqend/Orestes-Bloomfilter
4.能夠刪除麼?
目前咱們知道布隆過濾器能夠支持 add 和 isExist 操做,那麼 delete 操做能夠麼,答案是不能夠,例如上圖中的 bit 位 4 被兩個值共同覆蓋的話,一旦你刪除其中一個值例如 「user1」 而將其置位 0,那麼下次判斷另外一個值例如 「user2」 是否存在的話,會直接返回 false,而實際上你並無刪除它。
如何解決這個問題,答案是計數刪除。可是計數刪除須要存儲一個數值,而不是原先的 bit 位,會增大佔用的內存大小。這樣的話,增長一個值就是將對應索引槽上存儲的值加一,刪除則是減一,判斷是否存在則是看值是否大於0。
可是可使用 del 刪除key,這樣會把全部的值給刪除掉。
5.總結
1.HyperLogLog(包含在Redis中)來計算集合中的元素。
2 布隆過濾器(在ReBloom中可用),用於跟蹤集合中存在或缺失的元素。