如何在海量數(shù)據(jù)中判斷某條記錄是否存在-布隆過濾器的使用(JDK版和Redis版)

場景

  • 爬蟲時判斷某個URL是否已經(jīng)被爬取過
  • 黑名單過濾
  • 防止緩存穿透
  • ...

實現(xiàn)原理

定義一個長度為m的bit型數(shù)組flag[] (用來添加元素以及判斷元素是否存在,因為Integer的最大值為2147483647,所以m取該值即可)
定義n個不同的hash函數(shù)(在添加元素時,需要設(shè)置flag[]哪些位為1; 在判斷元素是否存在時,需要取flag[]哪些位來判斷)
添加某個元素時,通過n個hash函數(shù)算出該元素的n個hash值(整型值),把flag[]對應(yīng)的位置1
判斷某個元素是否存在時,通過n個hash函數(shù)算出該元素的n個hash值(整型值),在flag[]取出對應(yīng)的值,只要有一個不為1 ,即可判斷為不存在.否則就任務(wù)元素存在

布隆過濾器優(yōu)缺點

優(yōu)點: 大大節(jié)省空間

場景: 在10億數(shù)據(jù)中判斷某個數(shù)據(jù)是否存在
如果使用HashSet/HashMap來實現(xiàn)的話
查找的時間復(fù)雜度是O(1),但是我們來算一下存儲空間,Hash值為Integer類型,占四個字節(jié),那10億條數(shù)據(jù)占用的空間就是:10億*4/1024/1024/1024約等于3.7G......這個實現(xiàn)方案很明顯不現(xiàn)實
如果使用布隆過濾器實現(xiàn)
占用的空間大約為2147483647/8/1024/1024=256M

缺點: 誤差

由上面的分析可知, hash函數(shù)是存在hash沖突的, 所以布隆過濾器是會有誤判的情況.
表現(xiàn)為:

如果某條記錄被判斷為不存在,則該記錄必然不存在
如果某條記錄被判斷存在,則該記錄可能會不存在

代碼實現(xiàn)

JDK版

public class BloomFilterDemo {

    private static final int insertions = 1000000;

    @Test
    public void bfTest1(){
        //初始化一個存儲string數(shù)據(jù)的布隆過濾器息楔,初始化大小100w,不能設(shè)置為0
        BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), insertions,0.001);
        //初始化一個存儲string數(shù)據(jù)的set涮拗,初始化大小100w
        Set<String> sets = new HashSet<>(insertions);
        //初始化一個存儲string數(shù)據(jù)的set奏司,初始化大小100w
        List<String> lists = new ArrayList<>(insertions);

        //向三個容器初始化100萬個隨機并且唯一的字符串---初始化操作
        for (int i = 0; i < insertions; i++) {
            String uuid = UUID.randomUUID().toString();
            bf.put(uuid);
            sets.add(uuid);
            lists.add(uuid);
        }
        //布隆過濾器錯誤判斷的次數(shù)
        int wrong = 0;
        //布隆過濾器正確判斷的次數(shù)
        int right = 0;
        for (int i = 0; i < 10000; i++) {
            //按照一定比例選擇bf中肯定存在的字符串
            String test = i%100==0?lists.get(i/100):UUID.randomUUID().toString();
            if(bf.mightContain(test)){
                if(sets.contains(test)){
                    right ++;
                }else{
                    wrong ++;
                }
            }
        }

        //100
        System.out.println("=================right====================="+right);
        System.out.println("=================wrong====================="+wrong);
    }

    @Test
    public void bfTest2() {
        //預(yù)計要插入多少數(shù)據(jù)
        int size = 1000000;
        //期望的誤判率
        double fpp = 0.01;
        BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);
        //插入數(shù)據(jù)
        for (int i = 0; i < 1000000; i++) {
            bloomFilter.put(i);
        }
        int count = 0;
        for (int i = 0; i < 1000000; i++) {
            if (!bloomFilter.mightContain(i)) {
                count++;
                System.out.println(i + "誤判了");
            }
        }
        for (int i = 1000000; i < 2000000; i++) {
            if (bloomFilter.mightContain(i)) {
                count++;
                System.out.println(i + "誤判了");
            }
        }
        System.out.println("總共的誤判數(shù):" + count);

    }

}

redis版

/**
 *
 * 原理是和 JDK 自帶的BloomFilter類似的,我們看add方法,它先 Redis 緩存中是否有指定 key(如:orderBloomFilter) 的值理澎,如果沒有狞洋,則在 offset = 0 處,添加一個值為false颜武,即為 0;
 * 然后調(diào)用createHashes(byte[] data, int hashes)方法拖吼,根據(jù)字節(jié)數(shù)組的內(nèi)容生成digest鳞上,并將結(jié)果分割成 4 字節(jié)的整數(shù)并存儲在數(shù)組中,數(shù)組中的整數(shù)可以理解為每次hash所得的hashcode的值吊档。
 * 最后篙议,遍歷hashcode數(shù)組,將hashcode%sizeOfBloomFilter取模所得下標(biāo)所對應(yīng)的值設(shè)為true怠硼,即為 1鬼贱。
 *
 * contains方法,同樣調(diào)用createHashes(byte[] data, int hashes)得到字節(jié)數(shù)組內(nèi)容所對應(yīng)的hashcode數(shù)組香璃。
 * 遍歷hashcode數(shù)組这难,如果有一個hashcode所對應(yīng)的下標(biāo)的值不為1,則該數(shù)據(jù)不存在增显。反之雁佳,只有所有的hashcode所對應(yīng)的下標(biāo)的值都為1脐帝,才能說明該數(shù)據(jù)已經(jīng)存在。
 *
 * @author: ZENGQINGXUN178
 * @Date: 2019-8-27 10:30
 * @Description:
 */
@Component
public class BloomFilterService<E> {

    @Autowired
    private JedisCluster jedisCluster;

    /**
     * total length of the Bloom filter
     */
    private static int sizeOfBloomFilter;
    /**
     * number of hash functions
     */
    private static int numberOfHashFunctions;
    /**
     * 誤差率
     */
    private static final double falsePositiveProbability = 0.01;
    /**
     * 預(yù)計容量
     */
    private static final int expectedNumberOfElements = 10000;
    private static String bloom_name = "bloom_name_1";
    /**
     * encoding used for storing hash values as strings
     */
    private final Charset charset = Charset.forName("UTF-8");
    /**
     * MD5 gives good enough accuracy in most circumstances. Change to SHA1 if it's needed
     */
    private static final String hashName = "MD5";
    private static final MessageDigest digestFunction;

    /**
     * The digest method is reused between instances
     */
    static {
        MessageDigest messageDigest;
        try {
            messageDigest = java.security.MessageDigest.getInstance(hashName);
        } catch (NoSuchAlgorithmException e) {
            messageDigest = null;
        }
        digestFunction = messageDigest;
        // numberOfHashFunctions = ceil(-ln(falsePositiveProbability)/ln2)
        numberOfHashFunctions = (int) Math.ceil(-(Math.log(falsePositiveProbability) / Math.log(2)));
        // sizeOfBloomFilter = ceil(numberOfHashFunctions*expectedNumberOfElements/ln2)
        sizeOfBloomFilter = (int) Math.ceil(numberOfHashFunctions * expectedNumberOfElements / Math.log(2));
    }

    @PostConstruct
    public void init(){
        // 1. 獲取數(shù)據(jù)
        List<String> datas = new ArrayList<>();
        for (int i = 0; i < expectedNumberOfElements; i++) {
            datas.add(i + "");
        }
        if (jedisCluster.get(bloom_name) == null) {
            jedisCluster.setbit(bloom_name, 0, false);
        }
        // 2. 把數(shù)據(jù)放進bloomFilter
        JedisClusterPipeline pipelined = JedisClusterPipeline.pipelined(jedisCluster);
        try {
            datas.forEach(e -> add(e.getBytes(charset),pipelined));
        }finally {
            pipelined.syncAndReturnAll();
        }

    }

    /**
     * Adds an object to the Bloom filter. The output from the object's
     * toString() method is used as input to the hash functions.
     *
     * @param element is an element to register in the Bloom filter.
     */
    public void add(E element) {
        add(element.toString().getBytes(charset));
    }
    private void add(byte[] bytes) {

        int[] hashes = createHashes(bytes, numberOfHashFunctions);
        for (int hash : hashes) {
            jedisCluster.setbit(bloom_name, Math.abs(hash % sizeOfBloomFilter), true);
        }
    }
    /**
     * Adds an array of bytes to the Bloom filter.
     *
     * @param bytes array of bytes to add to the Bloom filter.
     */
    private void add(byte[] bytes,JedisClusterPipeline pipelined) {
        int[] hashes = createHashes(bytes, numberOfHashFunctions);
        for (int hash : hashes) {
            pipelined.setbit(bloom_name, Math.abs(hash % sizeOfBloomFilter), true);
        }
    }

    /**
     * Adds all elements from a Collection to the Bloom filter.
     *
     * @param c Collection of elements.
     */
    public void addAll(Collection<? extends E> c) {
        for (E element : c) {
            add(element);
        }
    }

    /**
     * Returns true if the element could have been inserted into the Bloom filter.
     * Use getFalsePositiveProbability() to calculate the probability of this
     * being correct.
     *
     * @param element element to check.
     * @return true if the element could have been inserted into the Bloom filter.
     */
    public boolean contains(E element) {
        return contains(element.toString().getBytes(charset));
    }

    public int failCount(List<E> elements) {
        JedisClusterPipeline pipelined = JedisClusterPipeline.pipelined(jedisCluster);
        int count = 0;
        try {
            for (E e : elements) {
                if (!contains(e.toString().getBytes(charset), pipelined)) {
                    count++;
                }
            }
        }finally {
            pipelined.close();
        }

        return count;
    }

    /**
     * Returns true if the array of bytes could have been inserted into the Bloom filter.
     * Use getFalsePositiveProbability() to calculate the probability of this
     * being correct.
     *
     * @param bytes array of bytes to check.
     * @return true if the array could have been inserted into the Bloom filter.
     */
    private boolean contains(byte[] bytes) {
        int[] hashes = createHashes(bytes, numberOfHashFunctions);
        for (int hash : hashes) {
            if (!jedisCluster.getbit(bloom_name, Math.abs(hash % sizeOfBloomFilter))) {
                return false;
            }
        }
        return true;
    }

    private boolean contains(byte[] bytes,JedisClusterPipeline pipelined) {
        int[] hashes = createHashes(bytes, numberOfHashFunctions);
        for (int hash : hashes) {
            pipelined.getbit(bloom_name, Math.abs(hash % sizeOfBloomFilter));
        }
        List<Object> objects = pipelined.syncAndReturnAll();
        Optional<Object> any = objects.stream().filter(Boolean.FALSE::equals).findAny();
        return any.isPresent();
    }

    /**
     * Returns true if all the elements of a Collection could have been inserted
     * into the Bloom filter. Use getFalsePositiveProbability() to calculate the
     * probability of this being correct.
     *
     * @param c elements to check.
     * @return true if all the elements in c could have been inserted into the Bloom filter.
     */
    public boolean containsAll(Collection<? extends E> c) {
        for (E element : c) {
            if (!contains(element)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 根據(jù)字節(jié)數(shù)組的內(nèi)容生成摘要糖权,并將結(jié)果分割成 4 字節(jié)的整數(shù)并存儲在數(shù)組中堵腹。
     * 調(diào)用 digest 函數(shù),直到生成所需的 int 數(shù)星澳。每次生成摘要之前疚顷,都需要加鹽,并且 salt++禁偎。
     *
     * @param data   specifies input data.
     * @param hashes number of hashes/int's to produce.
     * @return array of int-sized hashes
     */
    private static int[] createHashes(byte[] data, int hashes) {
        int[] result = new int[hashes];

        int k = 0;
        byte salt = 0;
        while (k < hashes) {
            byte[] digest;
            synchronized (digestFunction) {
                digestFunction.update(salt);
                salt++;
                digest = digestFunction.digest(data);
            }

            for (int i = 0; i < digest.length / 4 && k < hashes; i++) {
                int h = 0;
                for (int j = (i * 4); j < (i * 4) + 4; j++) {
                    h <<= 8;
                    h |= ((int) digest[j]) & 0xFF;
                }
                result[k] = h;
                k++;
            }
        }
        return result;
    }

    /**
     * Calculates a hash code for this class.
     *
     * @return hash code representing the contents of an instance of this class.
     */
    @Override
    public int hashCode() {
        int hash = 7;
        hash = 61 * hash + sizeOfBloomFilter;
        hash = 61 * hash + expectedNumberOfElements;
        hash = 61 * hash + numberOfHashFunctions;
        return hash;
    }

}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末腿堤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子如暖,更是在濱河造成了極大的恐慌笆檀,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盒至,死亡現(xiàn)場離奇詭異酗洒,居然都是意外死亡,警方通過查閱死者的電腦和手機枷遂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門樱衷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人酒唉,你說我怎么就攤上這事矩桂。” “怎么了痪伦?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵侄榴,是天一觀的道長。 經(jīng)常有香客問我流妻,道長牲蜀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任绅这,我火速辦了婚禮,結(jié)果婚禮上在辆,老公的妹妹穿的比我還像新娘证薇。我一直安慰自己,他們只是感情好匆篓,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布浑度。 她就那樣靜靜地躺著,像睡著了一般鸦概。 火紅的嫁衣襯著肌膚如雪箩张。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音先慷,去河邊找鬼饮笛。 笑死,一個胖子當(dāng)著我的面吹牛论熙,可吹牛的內(nèi)容都是我干的福青。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼脓诡,長吁一口氣:“原來是場噩夢啊……” “哼无午!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起祝谚,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤宪迟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后交惯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體踩验,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年商玫,在試婚紗的時候發(fā)現(xiàn)自己被綠了箕憾。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡拳昌,死狀恐怖袭异,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情炬藤,我是刑警寧澤御铃,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站沈矿,受9級特大地震影響上真,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜羹膳,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一睡互、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧陵像,春花似錦就珠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至泞歉,卻和暖如春逼侦,著一層夾襖步出監(jiān)牢的瞬間匿辩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工榛丢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留铲球,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓涕滋,卻偏偏與公主長得像睬辐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子宾肺,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容