布隆过滤器的详细解析

104 阅读4分钟

1. 什么是Bloom Filter(布隆过滤器)

一个很长的二进制向量和一系列随机映射函数实现可以用于检索一个元素是否存在一个集合中

1.1 布隆过滤器优点

  • 空间效率高,所占用空间小
  • 查询时间短

1.2 布隆过滤器缺点

  • 元素添加到集合中后,不能被删除
  • 存在一定的误判率,数据越多误判率越高

1.3 布隆过滤器使用场景

  • 字处理软件中,需要检查一个英语单词是否拼写正确

  • 在 FBI,一个嫌疑人的名字是否已经在嫌疑名单上

  • 在网络爬虫里,一个网址是否被访问过

  • yahoo, gmail等邮箱垃圾邮件过滤功能

  • 最常见的也就是面试中提到的缓存穿透是怎么解决的 或多或少都会提到一个布隆过滤器的概念

  • 如图便是一个基本的布隆过滤器 请添加图片描述

  • 下图是将0 存入到布隆过滤器中 请添加图片描述

2. 布隆过滤器的使用

不论是在 Guava 中,还是自己的简单实现,都只是本地的布隆过滤器,仅仅存在单个应用中,同步起来十分复杂,而且一旦应用重启,则之前添加的元素均丢失,对于分布式环境,可以利用 Redis 构建分布式布隆过滤器。
所以这里主要讲解 Redission 的使用, 因为其支持分布式布隆过滤器的实现。

2.1 Redisson 布隆过滤器使用

java
复制代码
RBloomFilter<SomeObject> bloomFilter = redisson.getBloomFilter("sample");
// 初始化布隆过滤器,预计统计元素数量为55000000,期望误差率为0.03
bloomFilter.tryInit(55000000L, 0.03);
bloomFilter.add(new SomeObject("field1Value", "field2Value"));
bloomFilter.add(new SomeObject("field5Value", "field8Value"));
bloomFilter.contains(new SomeObject("field1Value", "field8Value"));

2.1.1 Redission 中源码

2.1.1.1 通用接口

java
复制代码
public interface RBloomFilter<T> extends RExpirable {
    boolean add(T object)
    boolean contains(T object)
    boolean tryInit(long expectedInsertions, double falseProbability);
}

2.1.1.2 接口实现

java
复制代码
public class RedissonBloomFilter<T> extends RedissonExpirable implements RBloomFilter<T> {

    public boolean add(T object) {
        // 构造多个哈希值
        long[] hashes = hash(object);

        while (true) {
            if (size == 0) {
                // 配置以哈希表的形式存在 Redis 中
                // 这里执行 HGETALL
                readConfig();
            }

            int hashIterations = this.hashIterations;
            long size = this.size;

            // 需要设置为 1 的索引
            long[] indexes = hash(hashes[0], hashes[1], hashIterations, size);

            // 省略部分代码 ${新建客户端}

            // 依次执行 set 操作
            for (int i = 0; i < indexes.length; i++) {
                bs.setAsync(indexes[i]);
            }
            try {
                List<Boolean> result = (List<Boolean>) executorService.execute();

                for (Boolean val : result.subList(1, result.size()-1)) {
                    if (!val) {
                        return true;
                    }
                }
                return false;
            } catch (RedisException e) {
                if (!e.getMessage().contains("Bloom filter config has been changed")) {
                    throw e;
                }
            }
        }
    }
}

2.1.1.3 GET 函数

这里就不再深入探讨,只是将 add 函数中的 SET 变成 GET 操作

Hash 函数

java
复制代码
private long[] hash(Object object) {
    ByteBuf state = encode(object);
    try {
        return Hash.hash128(state);
    } finally {
        state.release();
    }
}

这个函数将 Object 编码后,返回一个 Byte 数组,然后调用 Hash.hash128 计算哈希值,这里的哈希算法是 HighwayHash

java
复制代码
public static long[] hash128(ByteBuf objectState) {
    HighwayHash h = calcHash(objectState);
    return h.finalize128();
}

protected static HighwayHash calcHash(ByteBuf objectState) {
    HighwayHash h = new HighwayHash(KEY);
    int i;
    int length = objectState.readableBytes();
    int offset = objectState.readerIndex();
    byte[] data = new byte[32];

    // 分区计算哈希
    for (i = 0; i + 32 <= length; i += 32) {
        objectState.getBytes(offset  + i, data);
        h.updatePacket(data, 0);
    }
    if ((length & 31) != 0) {
        data = new byte[length & 31];
        objectState.getBytes(offset  + i, data);
        h.updateRemainder(data, 0, length & 31);
    }
    return h;
}

// 第二个哈希函数,计算最后的索引值
private long[] hash(long hash1, long hash2, int iterations, long size) {
    long[] indexes = new long[iterations];
    long hash = hash1;

    // 多次迭代
    for (int i = 0; i < iterations; i++) {
        indexes[i] = (hash & Long.MAX_VALUE) % size;

        // 根据迭代次数选择哈希值,累加
        if (i % 2 == 0) {
            hash += hash2;
        } else {
            hash += hash1;
        }
    }
    return indexes;
}

3. 使用布隆过滤器解决Redis缓存穿透

关于缓存穿透问题可以在之前写的博客如何应对缓存问题查看。解决缓存穿透问题可以使用缓存空对象和布隆过滤器两种方法,这里仅讨论布隆过滤器方法。

使用布隆过滤器逻辑如下:

  • 根据 key 查询缓存,如果存在对应的值,直接返回;如果不存在则继续执行
  • 根据 key 查询缓存在布隆过滤器的值,
    如果存在值,则说明该 key 不存在对应的值,直接返回空,
    如果不存在值,继续向下执行查询DB
  • 查询 DB 对应的值,
    如果存在,则更新到缓存,并返回该值,
    如果不存在值,则更新到布隆过滤器中,并返回空

注意点:
1. redis 存储DB已经存在的数据
2. 布隆过滤器存储不存在的数据

具体流程图如下所示:

在这里插入图片描述

java
复制代码
public String getByKey(String key) {
    String value = get(key);
    if (StringUtils.isEmpty(value)) {
        logger.info("Redis 没命中 {}", key);
        if (bloomFilter.mightContain(key)) {
            logger.info("BloomFilter 命中 {}", key);
            return value;
        } else {
            if (mapDB.containsKey(key)) {
                logger.info("更新 Key {} 到 Redis", key);
                String valDB = mapDB.get(key);
                set(key, valDB);
                return valDB;
            } else {
                logger.info("更新 Key {} 到 BloomFilter", key);
                bloomFilter.put(key);
                return value;
            }
        }
    } else {
        logger.info("Redis 命中 {}", key);
        return value;
    }
}

参考链接:juejin.cn/post/707068…