Redis使用過程中有哪些注意事項(xiàng)?看看BAT這類的公司是正確使用Redis的??!

Redis使用過程中要注意的事項(xiàng)

Redis使用起來很簡單,但是在實(shí)際應(yīng)用過程中,一定會(huì)碰到一些比較麻煩的問題,常見的問題有

  • redis和數(shù)據(jù)庫數(shù)據(jù)的一致性
  • 緩存雪崩
  • 緩存穿透
  • 熱點(diǎn)數(shù)據(jù)發(fā)現(xiàn)

下面逐一來分析這些問題的原理及解決方案。

數(shù)據(jù)一致性

針對(duì)讀多寫少的高并發(fā)場景,我們可以使用緩存來提升查詢速度。當(dāng)我們使用Redis作為緩存的時(shí)候,一般流程如圖3-4所示。

  • 如果數(shù)據(jù)在Redis存在,應(yīng)用就可以直接從Redis拿到數(shù)據(jù),不用訪問數(shù)據(jù)庫。
  • 如果Redis里面沒有,先到數(shù)據(jù)庫查詢,然后寫入到Redis,再返回給應(yīng)用。
image-20210705200053476

<center>圖3-4</center>

因?yàn)檫@些數(shù)據(jù)是很少修改的,所以在絕大部分的情況下可以命中緩存。但是,一旦被緩存的數(shù)據(jù)發(fā)生變化的時(shí)候,我們既要操作數(shù)據(jù)庫的數(shù)據(jù),也要操作Redis的數(shù)據(jù),所以問題來了?,F(xiàn)在我們有兩種選擇:

  • 先操作Redis的數(shù)據(jù)再操作數(shù)據(jù)庫的數(shù)據(jù)

  • 先操作數(shù)據(jù)庫的數(shù)據(jù)再操作Redis的數(shù)據(jù)

到底選哪一種?

首先需要明確的是,不管選擇哪一種方案, 我們肯定是希望兩個(gè)操作要么都成功,要么都一個(gè)都不成功。不然就會(huì)發(fā)生Redis跟數(shù)據(jù)庫的數(shù)據(jù)不一致的問題。但是,Redis的數(shù)據(jù)和數(shù)據(jù)庫的數(shù)據(jù)是不可能通過事務(wù)達(dá)到統(tǒng)一的,我們只能根據(jù)相應(yīng)的場景和所需要付出的代價(jià)來采取一些措施降低數(shù)據(jù)不一致的問題出現(xiàn)的概率,在數(shù)據(jù)一致性和性能之間取得一個(gè)權(quán)衡。

對(duì)于數(shù)據(jù)庫的實(shí)時(shí)性一致性要求不是特別高的場合,比如T+1的報(bào)表,可以采用定時(shí)任務(wù)查詢數(shù)據(jù)庫數(shù)據(jù)同步到Redis的方案。由于我們是以數(shù)據(jù)庫的數(shù)據(jù)為準(zhǔn)的,所以給緩存設(shè)置一個(gè)過期時(shí)間,是保證最終一致性的解決方案。

Redis:刪除還是更新?

這里我們先要補(bǔ)充一點(diǎn),當(dāng)存儲(chǔ)的數(shù)據(jù)發(fā)生變化,Redis的數(shù)據(jù)也要更新的時(shí)候,我們有兩種方案,一種就是直接更新,調(diào)用set;還有一種是直接刪除緩存,讓應(yīng)用在下次查詢的時(shí)候重新寫入。

這兩種方案怎么選擇呢?這里我們主要考慮更新緩存的代價(jià)。

更新緩存之前,判斷是不是要經(jīng)過其他表的查詢、接口調(diào)用、計(jì)算才能得到最新的數(shù)據(jù),而不是直接從數(shù)據(jù)庫拿到的值,如果是的話,建議直接刪除緩存,這種方案更加簡單,一般情況下也推薦刪除緩存方案。

這一點(diǎn)明確之后,現(xiàn)在我們就剩一個(gè)問題:

  • 到底是先更新數(shù)據(jù)庫,再刪除緩存

  • 還是先刪除緩存,再更新數(shù)據(jù)庫

先更新數(shù)據(jù)庫,再刪除緩存

正常情況:更新數(shù)據(jù)庫,成功。刪除緩存,成功。

異常情況

1、更新數(shù)據(jù)庫失敗,程序捕獲異常,不會(huì)走到下一步,所以數(shù)據(jù)不會(huì)出現(xiàn)不一致。

2、更新數(shù)據(jù)庫成功,刪除緩存失敗。數(shù)據(jù)庫是新數(shù)據(jù),緩存是舊數(shù)據(jù),發(fā)生了不一致的情況。

這種問題怎么解決呢?我們可以提供一個(gè)重試的機(jī)制。

比如:如果刪除緩存失敗,我們捕獲這個(gè)異常,把需要?jiǎng)h除的key發(fā)送到消息隊(duì)列。然后自己創(chuàng)建一個(gè)消費(fèi)者消費(fèi),嘗試再次刪除這個(gè)key,如圖3-5所示。

image-20210705201740430

<center>圖3-5</center>

另外一種方案,異步更新緩存

因?yàn)楦聰?shù)據(jù)庫時(shí)會(huì)往binlog寫入日志,所以我們可以通過一個(gè)服務(wù)來監(jiān)聽binlog的變化(比如阿里的canal),然后在客戶端完成刪除key的操作。如果刪除失敗的話,再發(fā)送到消息隊(duì)列。

總之,對(duì)于后刪除緩存失敗的情況,我們的做法是不斷地重試刪除,直到成功。無論是重試還是異步刪除,都是最終一致性的思想,如圖3-6所示。

基于數(shù)據(jù)庫增量日志解析,提供增量數(shù)據(jù)訂閱&消費(fèi),目前主要支持了mysql。

image-20210705202304171

<center>圖3-6</center>

先刪除緩存,再更新數(shù)據(jù)庫

正常情況:刪除緩存,成功。更新數(shù)據(jù)庫,成功。

異常情況:

  • 刪除緩存,程序捕獲異常,不會(huì)走到下一步,所以數(shù)據(jù)不會(huì)出現(xiàn)不一致。

  • 刪除緩存成功,更新數(shù)據(jù)庫失敗。 因?yàn)橐詳?shù)據(jù)庫的數(shù)據(jù)為準(zhǔn),所以不存在數(shù)據(jù)不一致的情況。

看起來好像沒問題,但是如果有程序并發(fā)操作的情況下:

  • 線程A需要更新數(shù)據(jù),首先刪除了Redis緩存

  • 線程B查詢數(shù)據(jù),發(fā)現(xiàn)緩存不存在,到數(shù)據(jù)庫查詢舊值,寫入Redis,返回

  • 線程A更新了數(shù)據(jù)庫

這個(gè)時(shí)候,Redis是舊的值,數(shù)據(jù)庫是新的值,發(fā)生了數(shù)據(jù)不一致的情況,如圖3-7所示,這種情況就比較難處理了,只有針對(duì)同一條數(shù)據(jù)進(jìn)行串行化訪問,才能解決這個(gè)問題,但是這種實(shí)現(xiàn)起來對(duì)性能影響較大,因此一般情況下不會(huì)采用這種做法。

image-20210705204932980

<center>圖3-7</center>

緩存雪崩

緩存雪崩就是Redis的大量熱點(diǎn)數(shù)據(jù)同時(shí)過期(失效),因?yàn)樵O(shè)置了相同的過期時(shí)間,剛好這個(gè)時(shí)候Redis請(qǐng)求的并發(fā)量又很大,就會(huì)導(dǎo)致所有的請(qǐng)求落到數(shù)據(jù)庫。

關(guān)于緩存過期

在實(shí)際開發(fā)中,我們經(jīng)常會(huì),比如限時(shí)優(yōu)惠、緩存、驗(yàn)證碼有效期等。一旦過了指定的有效時(shí)間就需要自動(dòng)刪除這些數(shù)據(jù),否則這些無效數(shù)據(jù)會(huì)一直占用內(nèi)存但是缺沒有任何價(jià)值,因此在Redis中提供了Expire命令設(shè)置一個(gè)鍵的過期時(shí)間,到期以后Redis會(huì)自動(dòng)刪除它。這個(gè)在我們實(shí)際使用過程中用得非常多。

expire key seconds # 設(shè)置鍵在給定秒后過期
pexpire key milliseconds # 設(shè)置鍵在給定毫秒后過期

expireat key timestamp # 到達(dá)指定秒數(shù)時(shí)間戳之后鍵過期
pexpireat key timestamp # 到達(dá)指定毫秒數(shù)時(shí)間戳之后鍵過期

EXPIRE 返回值為1表示設(shè)置成功,0表示設(shè)置失敗或者鍵不存在,如果向知道一個(gè)鍵還有多久時(shí)間被刪除,可以使用TTL命令

ttl key # 返回鍵多少秒后過期
pttl key # 返回鍵多少毫秒后過期

當(dāng)鍵不存在時(shí),TTL命令會(huì)返回-2,而對(duì)于沒有給指定鍵設(shè)置過期時(shí)間的,通過TTL命令會(huì)返回-1。

除此之外,針對(duì)String類型的key的過期時(shí)間,我們還可以通過下面這個(gè)方法來設(shè)置,其中可選參數(shù)ex表示設(shè)置過期時(shí)間。

set key value [ex seconds]

如果向取消鍵的過期時(shí)間設(shè)置(使該鍵恢復(fù)成為永久的),可以使用PERSIST命令,如果該命令執(zhí)行成功或者成功清除了過期時(shí)間,則返回1 。 否則返回0(鍵不存在或者本身就是永久的)

SET expire.demo 1 ex 20
TTL expire.demo
PERSIST expire.demo
TTL expire

除了PERSIST命令,使用set命令為鍵賦值的操作也會(huì)導(dǎo)致過期時(shí)間失效。

關(guān)于key過期的實(shí)現(xiàn)原理

Redis使用一個(gè)過期字典(Redis字典使用哈希表實(shí)現(xiàn),可以將字典看作哈希表)存儲(chǔ)鍵的過期時(shí)間,字典的鍵是指向數(shù)據(jù)庫鍵的指針(使用指針可以避免浪費(fèi)內(nèi)存空間),字典的值是一個(gè)毫秒時(shí)間戳,所以在當(dāng)前時(shí)間戳大于字典值的時(shí)候這個(gè)鍵就過期了,就可以對(duì)這個(gè)鍵進(jìn)行刪除(刪除一個(gè)鍵不僅要?jiǎng)h除數(shù)據(jù)庫中的鍵,也要?jiǎng)h除過期字典中的鍵)。

設(shè)置過期時(shí)間的命令都是使用pexpireat命令實(shí)現(xiàn)的,其他命令也會(huì)轉(zhuǎn)換成pexpireat。給一個(gè)鍵設(shè)置過期時(shí)間,就是將這個(gè)鍵的指針以及給定的到期時(shí)間戳添加到過期字典中。比如,執(zhí)行命令pexpireat key 1608290696843,那么過期字典結(jié)構(gòu)將如圖3-8所示。

image-20210705221859849

<center>圖3-8</center>

過期鍵的刪除

過期鍵的刪除有兩種方法。

  • 被動(dòng)方式刪除

    被動(dòng)方式的核心原理是,當(dāng)客戶端嘗試訪問某個(gè)key時(shí),發(fā)現(xiàn)當(dāng)前key已經(jīng)過期了,就直接刪除這個(gè)key。

    當(dāng)然,有可能會(huì)存在一些key,一直沒有客戶端訪問,就會(huì)導(dǎo)致這部分key一直占用內(nèi)存,因此加了一個(gè)主動(dòng)刪除方式。

  • 主動(dòng)方式刪除

    主動(dòng)刪除就是Redis定期掃描國期間中的key進(jìn)行刪除,它的刪除策略是:

    • 從過期鍵中隨機(jī)獲取20個(gè)key,刪除這20個(gè)key中已經(jīng)過期的key。
    • 如果在這20個(gè)key中有超過25%的key過期,則重新執(zhí)行當(dāng)前步驟。實(shí)際上這是利用了一種概率算法。

Redis結(jié)合這兩種設(shè)計(jì)很好的解決了過期key的處理問題。

如何解決緩存雪崩

了解了過期key的刪除后,再來分析緩存雪崩問題。緩存雪崩有幾個(gè)方面的原因?qū)е隆?/p>

  • Redis的大量熱點(diǎn)數(shù)據(jù)同時(shí)過期(失效)
  • Redis服務(wù)器出現(xiàn)故障, 這種情況,我們需要考慮到redis的高可用集群,這塊后面再說。

我們來分析第一種情況,這種情況無非就是程序再去查一次數(shù)據(jù)庫,再把數(shù)據(jù)庫中的數(shù)據(jù)保存到緩存中就行,問題也不大??墒且坏┥婕按髷?shù)據(jù)量的需求,比如一些商品搶購的情景,或者是主頁訪問量瞬間較大的時(shí)候,單一使用數(shù)據(jù)庫來保存數(shù)據(jù)的系統(tǒng)會(huì)因?yàn)槊嫦虼疟P,磁盤讀/寫速度比較慢的問題而存在嚴(yán)重的性能弊端,一瞬間成千上萬的請(qǐng)求到來,需要系統(tǒng)在極短的時(shí)間內(nèi)完成成千上萬次的讀/寫操作,這個(gè)時(shí)候往往不是數(shù)據(jù)庫能夠承受的,極其容易造成數(shù)據(jù)庫系統(tǒng)癱瘓,最終導(dǎo)致服務(wù)宕機(jī)的嚴(yán)重生產(chǎn)問題。

解決這類問題的方法有幾個(gè)。

  • 對(duì)過期時(shí)間增加一個(gè)隨機(jī)值,避免同一時(shí)刻大量key失效。
  • 對(duì)于熱點(diǎn)數(shù)據(jù),不設(shè)置過期時(shí)間。
  • 當(dāng)從redis中獲取數(shù)據(jù)為空時(shí),去數(shù)據(jù)庫查詢數(shù)據(jù)的地方互斥鎖,這種方式會(huì)造成性能下降。
  • 增加二級(jí)緩存,以及緩存和二級(jí)緩存的過期時(shí)間不同,當(dāng)一級(jí)緩存失效后,可以再通過二級(jí)緩存獲取。

緩存穿透

緩存穿透,一般是指當(dāng)前訪問的數(shù)據(jù)在redis和mysql中都不存在的情況,有可能是一次錯(cuò)誤的查詢,也可能是惡意攻擊。

在這種情況下,因?yàn)閿?shù)據(jù)庫值不存在,所以肯定不會(huì)寫入Redis,那么下一次查詢相同的key的時(shí)候,肯定還是會(huì)再到數(shù)據(jù)庫查一次。試想一下,如果有人惡意設(shè)置大量請(qǐng)求去訪問一些不存在的key,這些請(qǐng)求同樣最終會(huì)訪問到數(shù)據(jù)庫中,有可能導(dǎo)致數(shù)據(jù)庫的壓力過大而宕機(jī)。

這種情況一般有兩種處理方法。

緩存空值

我們可以在數(shù)據(jù)庫緩存一個(gè)空字符串,或者緩存一個(gè)特殊的字符串,那么在應(yīng)用里面拿到這個(gè)特殊字符串的時(shí)候,就知道數(shù)據(jù)庫沒有值了,也沒有必要再到數(shù)據(jù)庫查詢了。

但是這里需要設(shè)置一個(gè)過期時(shí)間,不然的會(huì)數(shù)據(jù)庫已經(jīng)新增了這一條記錄,應(yīng)用也還是拿不到值。

這個(gè)是應(yīng)用重復(fù)查詢同一個(gè)不存在的值的情況,如果應(yīng)用每一次查詢的不存在的值是不一樣的呢?即使你每次都緩存特殊字符串也沒用,因?yàn)樗闹挡灰粯?,比如我們的用戶系統(tǒng)登錄的場景,如果是惡意的請(qǐng)求,它每次都生成了一個(gè)符合ID規(guī)則的賬號(hào),但是這個(gè)賬號(hào)在我們的數(shù)據(jù)庫是不存在的,那Redis就完全失去了作用,因此我們有另外一種方法,布隆過濾器。

布隆過濾器解決緩存穿透

先來了解一下布隆過濾器的原理,

  • 首先,項(xiàng)目在啟動(dòng)的時(shí)候,把所有的數(shù)據(jù)加載到布隆過濾器中。
  • 然后,當(dāng)客戶端有請(qǐng)求過來時(shí),先到布隆過濾器中查詢一下當(dāng)前訪問的key是否存在,如果布隆過濾器中沒有該key,則不需要去數(shù)據(jù)庫查詢直接反饋即可
image-20210705232359445

<center>圖3-9</center>

下面我們通過一個(gè)案例來演示一下布隆過濾器的工作機(jī)制。

注意,該案例是在[springboot-redis-example]這個(gè)工程中進(jìn)行演示。

  • 添加guava依賴,guava中提供了布隆過濾器的api

    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>21.0</version>
    </dependency>
    
  • 增加一個(gè)ApplicationRunner實(shí)現(xiàn),當(dāng)spring boot啟動(dòng)完成后執(zhí)行初始化

    @Slf4j
    @Component
    public class BloomFilterDataLoadApplicationRunner implements ApplicationRunner {
    
        @Autowired
        ICityService cityService;
    
        @Override
        public void run(ApplicationArguments args) throws Exception {
            List<City> cityList=cityService.list();
            // expectedInsertions: 預(yù)計(jì)添加的元素個(gè)數(shù)
            // fpp: 誤判率(后續(xù)再講)
            BloomFilter<String> bloomFilter=BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),10000000,0.03);
            cityList.parallelStream().forEach(city -> {
                bloomFilter.put(RedisKeyConstants.CITY_KEY+":"+city.getId());
            });
            BooleanFilterCache.bloomFilter=bloomFilter;
        }
    }
    
  • 添加一個(gè)controller用來訪問測試

    @RestController
    public class BloomFilterController {
        @Autowired
        RedisTemplate redisTemplate;
    
        @GetMapping("/bloom/{id}")
        public String filter(@PathVariable("id")Integer id){
            String key=RedisKeyConstants.CITY_KEY+":"+id;
            if(BooleanFilterCache.bloomFilter.mightContain(key)){ //判斷當(dāng)前數(shù)據(jù)在布隆過濾器中是否存在,如果存在則從緩存中加載
                return redisTemplate.opsForValue().get(key).toString();
            }
            return "數(shù)據(jù)不存在";
        }
    }
    

布隆過濾器存儲(chǔ)空間大小計(jì)算: https://hur.st/bloomfilter/?n=1000000&p=0.03&m=&k=

布隆過濾器原理分析

完成上述實(shí)驗(yàn)過程后,很多同學(xué)會(huì)產(chǎn)生疑問,

  • 老師,如果我的數(shù)據(jù)量有上千萬,那不會(huì)很占內(nèi)存???
  • 老師,布隆過濾器的實(shí)現(xiàn)原理是什么呀?

什么是布隆過濾器

布隆過濾器是Burton Howard Bloom在1970年提出來的,一種空間效率極高的概率型算法和數(shù)據(jù)結(jié)構(gòu),主要用來判斷一個(gè)元素是否在集合中存在。因?yàn)樗且粋€(gè)概率型的算法,所以會(huì)存在一定的誤差,如果傳入一個(gè)值去布隆過濾器中檢索,可能會(huì)出現(xiàn)檢測存在的結(jié)果但是實(shí)際上可能是不存在的,但是肯定不會(huì)出現(xiàn)實(shí)際上不存在然后反饋存在的結(jié)果。因此,Bloom Filter不適合那些“零錯(cuò)誤”的應(yīng)用場合。而在能容忍低錯(cuò)誤率的應(yīng)用場合下,Bloom Filter通過極少的錯(cuò)誤換取了存儲(chǔ)空間的極大節(jié)省

BitMap(位圖)

所謂的Bit-map就是用一個(gè)bit位來標(biāo)記某個(gè)元素對(duì)應(yīng)的Value,通過Bit為單位來存儲(chǔ)數(shù)據(jù),可以大大節(jié)省存儲(chǔ)空間.

ps:比特是一個(gè)二進(jìn)制數(shù)的最小單元,就像我們現(xiàn)在金額的最小單位是分。只不過比特是二進(jìn)制數(shù)而已,一個(gè)比特只能擁有一個(gè)值,不是0就是1,所以如果我給你一個(gè)值0,你可以說它就是一個(gè)比特,如果我給你兩個(gè)(00),你就可以說它們是兩個(gè)比特了。如果你將八個(gè)0或者1組合在一起,我們可以說說是8比特或者1個(gè)字節(jié)。在32位的機(jī)器上,一個(gè)int類型的數(shù)據(jù)會(huì)占用4個(gè)字節(jié),也就是32個(gè)比特位。

在java中,一個(gè)int類型占32個(gè)比特,我們用一個(gè)int數(shù)組來表示時(shí)未new int[32],總計(jì)占用內(nèi)存32*32bit,現(xiàn)假如我們用int字節(jié)碼的每一位表示一個(gè)數(shù)字的話,那么32個(gè)數(shù)字只需要一個(gè)int類型所占內(nèi)存空間大小就夠了,這樣在大數(shù)據(jù)量的情況下會(huì)節(jié)省很多內(nèi)存。

如果要存儲(chǔ)n個(gè)數(shù)字,那么具體思路如下。

  • 1個(gè)int占4字節(jié)即4*8=32位,那么我們只需要申請(qǐng)一個(gè)int數(shù)組長度為 int tmp[1+N/32]即可存儲(chǔ)完這些數(shù)據(jù),其中N代表要進(jìn)行查找的總數(shù),tmp中的每個(gè)元素在內(nèi)存在占32位可以對(duì)應(yīng)表示十進(jìn)制數(shù)0~31,所以可得到BitMap表:

    • tmp[0]:可表示0~31
    • tmp[1]:可表示32~63
    • tmp[2]可表示64~95
    • .......
  • 接著,我們只需要把對(duì)應(yīng)的數(shù)字存儲(chǔ)到指定數(shù)組元素的bit中即可,如何判斷int數(shù)字在tmp數(shù)組的哪個(gè)下標(biāo),這個(gè)其實(shí)可以通過直接除以32取整數(shù)部分,例如:整數(shù)8除以32取整等于0,那么8就在tmp[0]上。另外,我們?nèi)绾沃懒?在tmp[0]中的32個(gè)位中的哪個(gè)位,這種情況直接mod上32就ok,又如整數(shù)8,在tmp[0]中的8 mod 32等于8,那么整數(shù)8就在tmp[0]中的第八個(gè)bit位(從右邊數(shù)起)

比如我們要存儲(chǔ)5(101)、9(1001)、3(11)、1(1)四個(gè)數(shù)字,那么我們申請(qǐng)int型的內(nèi)存空間,會(huì)有32個(gè)比特位。這四個(gè)數(shù)字的二進(jìn)制分別對(duì)應(yīng)如下。

從右往左開始數(shù),比如第一個(gè)數(shù)字是5,對(duì)應(yīng)的二進(jìn)制數(shù)據(jù)是101, 那么從有往左數(shù)到第5位,把對(duì)應(yīng)的二進(jìn)制數(shù)據(jù)存儲(chǔ)到32個(gè)比特位上。

第一個(gè)5就是     00000000000000000000000000101000 

而輸入9的時(shí)候   00000000000000000000001001000000 

輸入3時(shí)候      00000000000000000000000000001100 

輸入1的時(shí)候    00000000000000000000000000000010

思想比較簡單,關(guān)鍵是十進(jìn)制和二進(jìn)制bit位需要一個(gè)map映射表,把10進(jìn)制映射到bit位上,這樣的好處是內(nèi)存占用少、效率很高(不需要比較和位移)。

布隆過濾器原理

有了對(duì)位圖的理解以后,我們對(duì)布隆過濾器的原理理解就會(huì)更容易了,基于前面的例子,我們把數(shù)據(jù)庫中的一張表的數(shù)據(jù)全部先保存到布隆過濾器中,用來判斷當(dāng)前訪問的key是否存在于數(shù)據(jù)庫。

假設(shè)我們需要把id=1這個(gè)key保存到布隆過濾器中,并且該布隆過濾器中的hash函數(shù)個(gè)數(shù)為3{x、y、z},它的具體實(shí)現(xiàn)原理如下:

  • 首先將位數(shù)組進(jìn)行初始化,將里面每個(gè)位都設(shè)置位0。
  • 對(duì)于集合里面的每一個(gè)元素,將元素依次通過3個(gè)哈希函數(shù){x、y、z}進(jìn)行映射,每次映射都會(huì)產(chǎn)生一個(gè)哈希值,這個(gè)值對(duì)應(yīng)位數(shù)組上面的一個(gè)點(diǎn),然后將位數(shù)組對(duì)應(yīng)的位置標(biāo)記為1。
  • 查詢id=1元素是否存在集合中的時(shí)候,同樣的方法將W通過哈希映射到位數(shù)組上的3個(gè)點(diǎn)。
    • 如果3個(gè)點(diǎn)的其中有一個(gè)點(diǎn)不為1,則可以判斷該元素一定不存在集合中。
    • 反之,如果3個(gè)點(diǎn)都為1,則該元素可能存在集合中。
image-20210706210232859

<center>圖3-10</center>

接下來按照該方法處理所有的輸入對(duì)象,每個(gè)對(duì)象都可能把bitMap中一些白位置涂黑,也可能會(huì)遇到已經(jīng)涂黑的位置,遇到已經(jīng)為黑的讓他繼續(xù)為黑即可。處理完所有的輸入對(duì)象之后,在bitMap中可能已經(jīng)有相當(dāng)多的位置已經(jīng)被涂黑。至此,一個(gè)布隆過濾器生成完成,這個(gè)布隆過濾器代表之前所有輸入對(duì)象組成的集合。

如何去判斷一個(gè)元素是否存在bit array中呢? 原理是一樣,根據(jù)k個(gè)哈希函數(shù)去得到的結(jié)果,如果所有的結(jié)果都是1,表示這個(gè)元素可能(假設(shè)某個(gè)元素通過映射對(duì)應(yīng)下標(biāo)為4,5,6這3個(gè)點(diǎn)。雖然這3個(gè)點(diǎn)都為1,但是很明顯這3個(gè)點(diǎn)是不同元素經(jīng)過哈希得到的位置,因此這種情況說明元素雖然不在集合中,也可能對(duì)應(yīng)的都是1)存在。 如果一旦發(fā)現(xiàn)其中一個(gè)比特位的元素是0,表示這個(gè)元素一定不存在

至于k個(gè)哈希函數(shù)的取值為多少,能夠最大化的降低錯(cuò)誤率(因?yàn)楣:瘮?shù)越多,映射沖突會(huì)越少),這個(gè)地方就會(huì)涉及到最優(yōu)的哈希函數(shù)個(gè)數(shù)的一個(gè)算法邏輯。

  • fpp表示允許的錯(cuò)誤概率

  • expectedInsertions: 預(yù)期插入的數(shù)量

public static void main(String[] args) {
    BloomFilter<String> bloomFilter=BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),10000000,0.03);
    bloomFilter.put("Mic");
    System.out.println(bloomFilter.mightContain("Mic"));
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容