【转】布隆过滤器

转自《程序员代码面试指南 IT名企算法与数据结构题目最优解 ,左程云著》

如果碰到网页黑名单系统、爬虫的网址判重等,如果系统容忍一定程度的失误率,但对空间要求比较严格,往往是要求了解布隆过滤器。

一个布隆过滤器精确代表一个集合,并可以精确判断一个元素是否在集合中。但是有多精确,取决于具体的设计,但不可能完全正确。

哈希函数

输入是任意,输出是固定范围,假设为S,并具有如下性质:

1、典型的哈希函数都有无限的输入值域。

2、传入相同的输入值,返回值一样。

3、传入不同输入值,返回值可能一样,也可能不一样。

4、最重要的性质:很多不同输入值得到的返回值会均匀分布在S上。

第4点是评价一个哈希函数优劣的关键,这种均匀分布与输入值出现的规律无关。比如MD5和SHA1算法。

布隆过滤器

原理

假设有一个长度为m的bit类型的数组,即数组中每个位置只占一个bit,每个bit只有0和1两种状态

bit array.png

再假设一共有k个哈希函数,这些函数的输出域S都大于或等于m,且这些哈希函数彼此独立。那么对同一个输入对象,经过k个哈希函数算出来的结果也是独立的。对算出来的每个结果都对m取余,然后再bit array上把相应位置设置1。

bit array mod.png

把bit类型的数组记为bitMap。处理完所有输入对象后,可能bitMap中已经有很多位置被涂黑。至此,一个布隆过滤器生成完毕,这个代表之前所有输入对象组成的集合。

在检查阶段,如何检查某一个对象是否是之前的某一个输入对象?

一个输入对象,通过k个哈希算出k个值,把k个值对m取余,在bitMap上看这些位置是不是都是黑,如果有一个不为黑,则一定不在集合。如果都是黑,说明在集合,但是这里存在误判。

实现

如果bitMap的大小m相比于输入对象的个数n过小,失误率会变大。

假设黑名单中样本个数为100亿个,记为n;失误率不希望超过0.01%,记为p;每个样本(url)大小64B,当然这个大小不会影响布隆过滤器的大小。

所以n=100亿,p=0.01%,布隆过滤器的大小m由以下公式确定:
m = - \frac{n*\ln p}{(\ln 2)^2}
根据公式计算出m=19.19n,大约20n,即需要2000亿个bit,也即是25G。

哈希函数的个数由以下公式决定:
k = \ln 2 * \frac {m} {n}
计算出约为14个。

然后用25GB的bitMap再单独实现14个哈希函数,根据如上描述生成布隆过滤器。

失误率的计算

失误率就是某个不认识的url,经过k个哈希函数后,在布隆过滤器取余后发现这些位置上都是黑,这样的情况称为一个失误。那么失误率就是看k个位置都是黑的概率

假设k个哈希函数独立,对于某一个bit位,一个输入对象在被k个哈希函数散列后,这个位置依然没有被涂黑的概率为:
(1-\frac 1 m)^k
经过n个输入对象后,这个位置依然没有被涂黑的概率为:
(1-\frac 1 m)^{kn}
被涂黑的概率就是
1-(1-\frac 1 m)^{kn}
则在检查阶段,检查k个位置都为黑的概率为:
(1-(1-\frac 1 m)^{kn})^k = (1-(1-\frac 1 m)^{-m*- \frac {-kn} {m}})^k
x \to 0时,(1+x)^{\frac 1 x} \to e 。这里m是很大的数,所以-\frac 1m \to 0,则简化为
(1-e^{\frac {-nk} {m}})^k

误报的解决

通过建立白名单防止误报。比如已经发现某个样本不在布隆过滤器,但每次计算后结果都显示其在布隆过滤器中,则可以把该样本加入白名单中,以后就知道的确不在过滤器中。

代码

爬虫项目中用到了布隆过滤器

[SimpleBloomFilter.java](../../../../gitlab/data-platform/page-feature/rtbreq-frontier/src/main/java/com/buzzinate/util/SimpleBloomFilter.java)

package com.buzzinate.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.BitSet;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

public class SimpleBloomFilter{
    private static Logger logger = Logger.getLogger(SimpleBloomFilter.class);
    private static SimpleBloomFilter instance = null;
    private static final int DEFAULT_SIZE = Integer.MAX_VALUE;
    private static final int[] seeds = new int[] { 5, 7, 11, 13, 31, 37, 61 };
    private BitSet bits = new BitSet(DEFAULT_SIZE);
    private SimpleHash[] func = new SimpleHash[seeds.length];

    public static String filename = "/opt/tomcat/bloomfilter";
    private SimpleBloomFilter() {
        for (int i = 0; i < seeds.length; i++) {
            func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]);
        }
    }

    
    public BitSet getBits() {
        return bits;
    }


    public void setBits(BitSet bits) {
        this.bits = bits;
    }


    public void add(String value) {
        for (SimpleHash f : func) {
            bits.set(f.hash(value), true);
        }
    }

    public boolean contains(String value) {
        if (value == null) {
            return false;
        }
        boolean ret = true;
        for (SimpleHash f : func) {
            ret = ret && bits.get(f.hash(value));
        }
        return ret;
    }

    public void saveBit() {
        try {
            
            int count = 0;
            for(int i = 0;i < DEFAULT_SIZE;i ++){
                if(bits.get(i)){
                    count ++ ;
                }
            }
            logger.info("bloomfilter true bits:"+count);
            
            File file = new File(filename+".tmp");
            logger.info("save bloomfilter, path "+ file.getAbsolutePath());
            ObjectOutputStream oos = new ObjectOutputStream(
                    new FileOutputStream(file, false));
            oos.writeObject(bits);
            
            oos.flush();
            oos.close();
            file.renameTo(new File(filename));
            logger.info("saved bloomfilter to " + file.getAbsolutePath());
        } catch (Exception e) {
            logger.error(e, e);
        }
    }

    public BitSet readBit(String path) {
        BitSet bits = new BitSet(DEFAULT_SIZE);
        File file = new File(filename);
        if(StringUtils.isNotBlank(path)){
            file = new File(path);
        }
        logger.info("read bloomfilter, path "+ file.getAbsolutePath());
        if (!file.exists()) {
            logger.info(file.getAbsolutePath()+" not exists!");
            return bits;
        }
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
                    file));
            bits = (BitSet) ois.readObject();
            ois.close();
            int count = 0;
            for(int i = 0;i < DEFAULT_SIZE;i ++){
                if(bits.get(i)){
                    count ++ ;
                }
            }
            logger.info("bloomfilter true bits:"+count);
        } catch (Exception e) {
            logger.error("read bloomfilter fail!", e);
        }
        return bits;
    }

    // 内部类,simpleHash
    public static class SimpleHash {
        private int cap;
        private int seed;

        public SimpleHash(int cap, int seed) {
            this.cap = cap;
            this.seed = seed;
        }

        public int hash(String value) {
            int result = 0;
            int len = value.length();
            for (int i = 0; i < len; i++) {
                result = seed * result + value.charAt(i);
            }
            return (cap - 1) & result;
        }
    }
    
    public static SimpleBloomFilter getInstance(){
        if(instance == null){
            instance = new SimpleBloomFilter();
            if(new File(filename).exists()){
                instance.setBits(instance.readBit(""));
            }
        }
        return instance;
    }
    
    /**
     * 改变实例
     * @param path
     */
    public static boolean changeInstance(String path){
        SimpleBloomFilter tmpInstance = new SimpleBloomFilter();
        File file = new File(path);
        if(System.currentTimeMillis() - file.lastModified() < 3600 * 1000){
            tmpInstance.setBits(tmpInstance.readBit(path));
            instance = tmpInstance;
            return true;
        }
        return false;
    }
    
    /**
     * 返回bit为1的数量
     * @return
     */
    public static int trueBits(){
        int count = 0;
        for(int i = 0;i < DEFAULT_SIZE;i ++){
            if(instance.bits.get(i)){
                count ++ ;
            }
        }
        return count;
    }

    public static void main(String[] args) {
        SimpleBloomFilter filter = new SimpleBloomFilter();
        for (int i = 0; i <100000; i++) {
            filter.add(i + "");
        }

        long t1 = System.currentTimeMillis();
        filter.saveBit();
        long t2 = System.currentTimeMillis();
        System.out.println("it takes " + (t2 - t1)+"ms");
        
        
//      System.out.println("add end");
//      int cnt = 0;
//      for (int i = 200000; i > 100000; i--) {
//          if (filter.contains(i + "")) {
//              cnt++;
//          }
//      }
//
//      System.out.println("total:" + cnt / 300000000.0);
    }

}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • ref:https://www.cnblogs.com/cpselvis/p/6265825.html 什么情况下...
    七海的游风阅读 152评论 0 1
  • 写在前面 在大数据与云计算发展的时代,我们经常会碰到这样的问题。我们是否能高效的判断一个用户是否访问过某网站的主页...
    Audience0阅读 1,712评论 0 0
  • 布隆过滤器 (Bloom Filter) 详解 原文链接:http://www.cnblogs.com/allen...
    JackChen1024阅读 11,550评论 0 3
  • 一家人事真多!儿子的老子哄好了,听话了,乖了,不跟我倔强了。我笑了两天。儿子又闹情绪了。读高中不想去读了,说再读都...
    b0ad1e451363阅读 150评论 0 0
  • 1 今天会议的主要内容是关于劳动关系方面的,一上来就是按照地区别分成了几个讨论小组,中国区的人数最多,分成了3个小...
    冬冬Steven阅读 115评论 0 0