详解布隆过滤器

原创不易,转载请注名出处://www.greatytc.com/p/c98427267fb4

为什么用

去重是数据采集中很重要的一个点,如果一个任务被反复放到任务列表中,这无疑是对资源最大的浪费。同样一个结果反复的入库也是对数据库资源的浪费,且先不说内存占用会很大,数据变很乱,你存表的时候唯一键也会让在写存表的脚本时异常考虑占用到你过多的时间。
但是别慌我们有办法,conner下面介绍一个很有用的工具,布隆过滤器,会让你的代码结构变得更加的清晰,也可以应对大批量的去重任务。

原理

当我判断一张脸的时候,如果眼睛,鼻子,嘴巴,眉毛,耳朵都和我们脑海中的某个人的这些特征是一模一样的话,我们就有理由相信,这张脸我们认识。
同样这种方式我们放到算法中去,其实也是蛮好理解的。如果有个数据它通过不同方式来进行hash(hash可以理解为找这一张脸的五官),我们将每次hash后的值都存放到某个空间中。如果每次hash之后的值,都能在我们的空间中能找到的话,我们就有足够的理由相信,这个数据是在我们库中存在的。
有人会问,既然这样我们为什么不把整张脸存到数据库中,然后来比较?你需要先思考清楚,存一张脸和存五官哪个占用的内存大哪个方便?当然只通过几个特征就想来识别一张脸,是会存在一定程度的误判。这种误判可能会将不存在的某个数据错判断为存在的数据,但是它一定不会将存在的值判断为不存在,而且这个误判率其实是一个概率,但其实我们可以控制这个概率。
所以可以理解为布隆过滤器的数据结构就是在你的程序里有一堆在记录有和没有的东西,如下图:
假装有个图(好吧其实我懒得画)

实现

关键其实需要抓住以下几个点:
1,hash(生成我们需要的特征,包括hash的次数,以及我们需要hash的方法)。
2,exits判断这个值的hash结果是否都在我们的空间中。
3,add将hash后到结果加入到我们的空间中。

hash算法确定

下面我分别使用python和java实现一次
hash方法我们选择使用google的mmh3方法,这个方法的好处是能将一个值hash成多个值,而且这个值我们可以指定为64位还是32位,比较方便。这个方法是用c++来实现的,python没有办法看到源码,java版能看到他多位hash的部分逻辑,有兴趣的可以看看。

hash次数以及内存大小的确定,具体计算方式可以参考这篇博客:

https://my.oschina.net/LucasZhu/blog/1813110
感兴趣的可以看看这里我们只关注结果:
假如我们需要去重的量capacity=100000000, 可以接受的错误率error_rate=0.00000001
那么我们所需要的二进制位是
m = math.ceil(capacity * math.log2(math.e) * math.log2(1 / error_rate))等于3834023351
需要的内存数是:
mem = math.ceil(m / 8 / 1024 / 1024) 等于458mb
至少需要的hash次数是:
k = math.ceil(math.log1p(2) * m / capacity) 等于43次
主要有三个个方法,hash,add和exits
所以下面开始展示我的代码(部分)
python:

    def get_hashs(self, value):
        hashs = list()
        for seed in self.seeds:
            hash = mmh3.hash(value, seed)
            if hash >= 0:
                hashs.append(hash)
            else:
                hashs.append(self.N - hash)
        return hashs

    def is_exist(self, value):
        name = self.key + "_" + str(ord(value[0]) % self.blocknum)
        hashs = self.get_hashs(value)
        exist = True
        for hash in hashs:
            exist = exist & self.redis.getbit(name, hash)
        return exist

    def add(self, value):
        name = self.key + "_" + str(ord(value[0]) % self.blocknum)
        hashs = self.get_hashs(value)
        for hash in hashs:
            self.redis.setbit(name, hash, 1)

java:

public boolean add(byte[] bytes) {
        if (contains(bytes)) return true;
        long hash64 = Hashing.murmur3_128().hashObject(bytes, funnel).asLong();
        int hash1 = (int)hash64;
        int hash2 = (int)(hash64 >>> 32);
        boolean success = true;
        for(int i = 1; i <= numHashFunctions; ++i) {
            int nextHash = hash1 + i * hash2;
            long hashValue;
            if(nextHash < 0) {
                hashValue = Integer.MAX_VALUE + Math.abs((long) nextHash);
            } else {
                hashValue = nextHash;
            }
            success &= mmapData.setBit(hashValue);
        }
        if (success) mmapData.increment();
        return success;
    }

    public boolean contains(byte[] bytes) {
        hashMemNo = additiveHash(bytes,need_memory_time);
        long hash64 = Hashing.murmur3_128().hashObject(bytes, funnel).asLong();
        int hash1 = (int)hash64;
        int hash2 = (int)(hash64 >>> 32);
        for(int i = 1; i <= numHashFunctions; ++i) {
            int nextHash = hash1 + i * hash2;
            long hashValue;
            if(nextHash < 0) {
                hashValue = ((long) Integer.MAX_VALUE) + Math.abs(nextHash);
            } else {
                hashValue = nextHash;
            }
            if(mmapData.getBit(hashValue,hashMemNo) == 0) {
                return false;
            }
        }
        return true;
    }

优势和潜在问题

优势

其实布隆过滤器本身的结构就是最省内存的,因为只用到二进制,它的每一个byte上只用0和1,0代表这个位置上没有,1代表代表这个位置有,当数据量少的时候这个问题其实很好解决,但当去重数据量破亿的时候,就会出现问题。因为你需要的空间可能够,但是计数的方式可能会不够,在下面我会再讨论。

寻址问题

众所周知,int类型的长度2的32次方,因为第一位表示的是正负号,所以int能表示的最大值是2的31次方减去1也就是2147483647,当我们位置的值大于这个值时就得换到long类型,long类型的最大值是2的63次方减去1,看起来还是ok的。但是当我们将这个值转换为过滤器中需要的内存里空间的时就有点吓人了,2的63次方个byte位需要的空间是2的35次方个gb的空间,这个空间太大了怎么处理?以后有机会我会讲到。

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

推荐阅读更多精彩内容