布隆过滤器

一、缓存穿透

项目中的热点数据我们一般会放在 redis 中,在数据库前面加了一层缓存,减少数据库的访问,提升性能。但如果,请求的 key 在 redis 中并不存在,那请求还是会抵达数据库,这就叫缓存穿透。

我们无法避免缓存穿透,因为数据库中的数据要全部放到 redis 中不太现实,也不可能保证数据库数据和 redis 中的数据做到实时同步。但我们可以避免高频的缓存穿透。

避免高频缓存穿透的办法:

  • 做好参数检验,对于一些非法参数直接挡掉,比如 id 为负数的请求直接挡掉;

  • 缓存无效的 key,比如某次请求的 key 在数据库中不存在,那就将其缓存到 redis 并设置过期时间,但是这种办法不好,假如黑客每次请求都用不同的 key,那 redis 中的无用数据就会很多;

  • 使用布隆过滤器;

二、布隆过滤器

1. 过滤器的作用:

上面说了,如果大量不存在的 key 请求过来,还是会直接请求到数据库,如果我们能在请求数据库之前判断这个 key 在数据库到底存不存在,不存在就直接返回相关错误信息,那就可以解决缓存穿透的问题。

如何在不请求数据库的前提下判断这个 key 在数据库中存不存在呢?这就需要用到过滤器。难不成又要将数据库的所有数据缓存到过滤器中吗?当然不是,如果这样,那和将所有 key 缓存到 redis 就没啥区别了。接下来看看布隆过滤器是怎么做的。

2. 布隆过滤器原理:

布隆过滤器使用了布隆算法来存储数据,明确一点,布隆算法存储的数据不是 100% 准确的,即布隆过滤器认为这个 key 存在,实际上它也有可能不存在,如果它认为这个key 不存在,那么它一定不存在。布隆算法是通过一定的错误率来换取空间的。

布隆算法通过 bit 数组 来标识 key 是否存在。怎么做的呢?key 经过 hash 函数的运算,得到一个数组的下标,然后将对应下标的值改成1,1就表示该 key 存在。这个 hash 函数要满足的条件有:

  • 对 key 计算的结果必须在 [0, bitArray.length - 1] 之间;

  • 计算出来的结果分布要足够散列;

因为要进行 hash 计算,所有布隆算法的错误率是由于 hash 碰撞导致的。所以降低 hash 碰撞的概率就可以降低错误率。怎么降低 hash 碰撞的概率呢?两种办法:

  • 加大数组的长度:数组长度更长,hash 碰撞的概率自然更小;

  • 增加 hash 函数的个数:假如 key 为 10 的数据,第一个 hash 函数计算出来的下标是 1,第二个 hash 函数计算出来的是 4,第三个 hash 函数计算出来的是 10,那么就要 1,4,10 这三个下标所对应的值都得是 1,才会认为 key 存在,故而也可以减少误判的情况。

3. 为什么要用 bit 数组:

因为节省空间。1k = 1024byte = 1024 * 8 bit = 8192bit,即长度为8192的bit数组只需要1kb的空间。

4. 怎么用?

业界大佬和民间大神已经造了很多轮子了,这里主要说三种,具体用法大家看一下相关 api 即会了。

  • 可以使用 guava 中的布隆过滤器;

  • 使用 hutools 工具包中的布隆过滤器;

  • redis 有 bitMap,也可以用作布隆过滤器,推荐使用 redisson 构造布隆过滤器;

三、hutools 中的布隆过滤器源码分析

这里带大家分析一下 hutools 中的布隆过滤器源码,看看人家怎么实现的。用法如下:

public static void main(String[] args) {
    BitMapBloomFilter bloomFilter = new BitMapBloomFilter(5);
    bloomFilter.add("aa");
    bloomFilter.add("bb");
    bloomFilter.add("cc");
    bloomFilter.add("dd");
    System.out.println(bloomFilter.contains("aa"));
}

1. 构造方法:

首先来看 new BitMapBloomFilter 的时候做了什么。

public BitMapBloomFilter(int m) {
    long mNum = NumberUtil.div(String.valueOf(m), String.valueOf(5)).longValue();
    long size = mNum * 1024 * 1024 * 8;

    filters = new BloomFilter[]{
            new DefaultFilter(size),
            new ELFFilter(size),
            new JSFilter(size),
            new PJWFilter(size),
            new SDBMFilter(size)
    };
}

用传进来的 m 计算得到一个 size,然后创建了一个 BloomFilter 数组。这个数组有五个不同实现的对象,可以简单地理解为 hutools 中的布隆过滤器用了五个 hash 函数去计算 key 对应的索引。注意:如果传进来的 m 小于 5,那么 size 就是 0,调用 hash 的时候就会报错,因为 hash 函数中用这个 size 做除数了,如下:

@Override
public long hash(String str) {
    return HashUtil.javaDefaultHash(str) % size;
}

2. add 方法:

@Override
public boolean add(String str) {
    boolean flag = false;
    for (BloomFilter filter : filters) {
        flag |= filter.add(str);
    }
    return flag;
}

这里就是遍历上面构造的五个对象,也即分别调用那五个对象的 add 方法。再看看这里调用的那个 add 方法:

@Override
public boolean add(String str) {
    final long hash = Math.abs(hash(str));
    if (bm.contains(hash)) {
        return false;
    }

    bm.add(hash);
    return true;
}

这里首先计算 hash 值,这里的 hash 就是那五个对象的 hash函数,计算出了 hash 值后,判断是否已经存在了,存在了就直接返回 false,否则就进行 add 操作。这个 contains 等会儿再看,先看看这个 add 方法。它的实现有两个,如图:

add的实现

默认用的是 IntMap 中的 add 方法,再去看看它的实现:

@Override
public void add(long i) {
    int r = (int) (i / BitMap.MACHINE32);
    int c = (int) (i % BitMap.MACHINE32);
    ints[r] = (int) (ints[r] | (1 << c));
}

这里是不是有点儿懵逼呢?首先看看这个 ints 数组是啥:

private final int[] ints;

它竟然是个 int 数组,说好的 bit 数组呢?

先来回顾一下,一个 int 占 4 个 byte,而一个 byte 是 32 bit。所以,一个长度为 10 的 int 数组,其实就可以存放 320 bit数据。这里正是用 int 数组来表示 bit。明白了这个之后,再来看上面的代码。首先让 i 除以 32,然后再让 i 对 32 求余,最后再做了一堆计算就完事了。不懂没关系,举个例子就秒懂了。

假如有一个 int 数组:int[] ints = new int[10],那么它可以存放 32 * 10 = 320 bit 数据。

现在我想将第 66 位的 bit 值改成 1,第 66 位索引其实是 65,那么做法如下:

int r = 65 / 32 = 2; int c = 65 % 32 = 1;

1<<1 = 2,二进制就是0000……0010,10 前面是 30 个 0,ints[2] 是0,二进制就是 32 个 0;

它们做与运算,结果就是还是 2,二进制就是 0000……0010。

然后让把 0000……0010 赋值给 ints[2]。为什么这样就表示把第 66 个 bit 值改成 1 了呢?

ints[0] 和 ints[1] 都是 0 对不对,也即 ints[0] 和 ints[1] 中都有 32 个 0,加起来就是 64 个 0。

也就是前 64 bit 都是 0。ints[2] 存的是 2,二进制是 0000……0010,这个二进制第一位是 0,第二位是 1……

所以 ints[2] 中的第一位是 0, 第二位是 1,后面的 30 位都是0。32 + 32 + 2 = 66,所以第 66 位就变成了 1。

3. contains 方法:

@Override
public boolean contains(String str) {
    return bm.contains(Math.abs(hash(str)));
}

再点进去看这个 contains 方法:

@Override
public boolean contains(long i) {
    int r = (int) (i / BitMap.MACHINE32);
    int c = (int) (i % BitMap.MACHINE32);
    return ((int) ((ints[r] >>> c)) & 1) == 1;
}

还是上面的例子,r 还是 2,c 还是 1,ints[2] = 2,2>>>1 结果是 1,

1 & 1 结果是 1,所以返回true,也就是说,如果传进来的 i 还是 65 的话,那就返回 true,因为刚才已经 add 过了。

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

推荐阅读更多精彩内容