Redis—BloomFilter 预防缓存击穿的一种数据结构(囊括活跃的面试场景)你有玩过么

Redis—BloomFilter 预防缓存击穿的一种数据结构

写在前面

几天前,小付在复习redis的时候,再一次看到了BloomFilter,此时脑中突然出现一撮迷人的小胡子,手拿一面盾牌的大叔朝着我喊了句:站在布隆后面的画面~

此时我跳出画面,正儿八经的直视眼前的这个东西,就有了下面的故事…

BloomFilter 是什么

BloomFilter 的概念——百度百科

Bloom filter是由Howard Bloom在1970年提出的二进制向量数据结构,它具有空间和时间效率,被用来检测一个元素是不是集合中的一个成员

简介

如果检测结果为,该元素不一定在集合中;但如果检测结果为,该元素一定不在集合中。因此Bloom filter具有100%的召回率。这样每个检测请求返回有“在集合内(可能错误)”和“不在集合内(绝对不在集合内)”两种情况,可见 Bloom filter 是牺牲了正确率和时间节省空间

BloomFilter的原理

BloomFilter 的数据结构

在这里插入图片描述
在小付的眼里来说,BloomFilter就是一个位图与hash函数巧妙结合用来应对于redis中预防缓存被击穿的情况而设计出来的一种数据结构。它利用时间以及正确率(也就是后面会提到与之对应的误判率)来节省空间的神奇东东。

那它是如何做到如此精妙的呢?它的原理是什么呢?

当一个元素要被加入到集合当中时,通过n次hash函数计算出n个映像位点,并把当前所处位图此点的值置为1。然后依次进行搜索这几个映像位点是否都是1,如果都是1的话,说明该元素可能存在于当前集合之中,如果但凡出现一个0,都说明此元素不处于当前元素中,直接返回false。

说到了BloomFilter是预防缓存穿透的一种数据结构,那么缓存穿透是什么呢?

emmm,说道缓存穿透就又不得不提到导致缓存宕机的三兄弟了——缓存击穿,缓存穿透,缓存雪崩了。

那咱们先来回答问题:

缓存穿透:透过字面理解、穿透、贯穿之意,也就说明了缓存与数据库中都没有数据,此时用户不断向数据库发送请求,一个完全没有的数据,却要花数据库不少的时间进行处理,如果这个请求达到高峰,导致数据库查询压力过大,那么此时数据库就可能会直接被打掉线。

缓存击穿:击穿、说白了就是击穿掉了缓存,让缓存形同虚设,也就好比其实开发中,数据库中存在这个数据,但是缓存中没有这个数据的时候,或者是最为一个热点数据,当热点数据的key在缓存中即将过期时,高并发进行访问请求,这无疑就是穿破缓存、直接打在你肉上的意思,严重情况下一样可以打死数据库。

缓存雪崩:说道雪崩就不得不提到一句话,“雪崩来临时,没有一片雪花是无辜的”,这里咱们就来举例说明缓存雪崩:雪花就是用户请求数据,而大部分热点数据请求就比作狂风暴雪来临时,这就好理解了塞,当这些热点数据忍不住了要进行请求时(热点数据即将同一时期过期),突然一下落下(一下全部过期),欧后~发生了雪崩(缓存大面积失效,所有请求直接打在数据库脸上,问你疼不疼一样),要是我是数据库的话我直接si给你看,而你还不信邪重启就能解决问题,这时候大量数据又来了,你再给我si一次。

咱们言归正传,小伙子刚才讲的不错诶,来说说BloomFilter实现

啊?BloomFilter的实现 ,看来还是逃不过手撕BloomFilter了~

实现BloomFilter就不得不提及hash函数的选取执行次数和bitmap的数组大小。

当然我们在真实使用的时候,肯定是知道当前要存取的数据为n,期望其误判率(就是bitmap中均为1时,该元素可能存在于此集合出现的差错),然后计算其最佳BITMAP数组大小m[位图最佳大小] = (-n[插入数据量] * ln(p[误判率]))/(ln2)^2,与hash函数的选取k = m[位图的最佳大小] /n[插入的数据链] * (ln2)

通过BloomFilter类查看源码,知其然并且知其所以然:

public static <T> BloomFilter<T> create(Funnel<? super T> funnel, int expectedInsertions, double fpp) {
        return create(funnel, (long)expectedInsertions, fpp);
    }

    public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions, double fpp) {
        return create(funnel, expectedInsertions, fpp, BloomFilterStrategies.MURMUR128_MITZ_64);
    }

    public static <T> BloomFilter<T> create(Funnel<? super T> funnel, int expectedInsertions) {
        return create(funnel, (long)expectedInsertions);
    }

    public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
        return create(funnel, expectedInsertions, 0.03D);
    }

看到如上的四个BloomFilter中的create方法,总结归纳一下,你会发现形参中有共同之处:

  • funnel: 和泛型挂钩 数据类型

  • expectedInsertions 期望插入的数据量

  • fpp 误判率

  • strategy:采取哪种hash算法来计算

然后源码中:又给出了如图的计算代码

 @VisibleForTesting
    static int optimalNumOfHashFunctions(long n, long m) {
        return Math.max(1, (int)Math.round((double)m / (double)n * Math.log(2.0D)));
    }

    @VisibleForTesting
    static long optimalNumOfBits(long n, double p) {
        if (p == 0.0D) {
            p = 4.9E-324D;
        }

        return (long)((double)(-n) * Math.log(p) / (Math.log(2.0D) * Math.log(2.0D)));
    }

计算最佳的bitmap数组大小以及计算最佳的hash函数的使用次数。

以上都弄得差不多了,咱们就差一个 使用何种hash算法来进行bitmap数组映射了

所以我又跑源码中找了找:果不其然 ,全都有

在一个叫做BloomFilterStrategies的枚举类型中他给我们提供了很多的hash算法,博主这里采取的时最简单的hash算法。
在这里插入图片描述
具体如何进行计算的就需要小伙伴们自行去研究啦~

代码实现

/**
 * 布隆过滤器的核心类
 *
 * @author Alascanfu
 */
public class BloomFilterUtil<T> {
    private int numHashFunction;
    private int bitSize;
    @SuppressWarnings("all")
    private Funnel funnel;
    
    @SuppressWarnings("all")
    public BloomFilterUtil(long expectInsertions) {
        funnel = Funnels.stringFunnel(Charset.defaultCharset());
        bitSize = optimalNumOfBitSize(expectInsertions, 0.03);
        numHashFunction = optimalNumOfHashFunction(bitSize, expectInsertions);
    }
    
    @SuppressWarnings("all")
    public BloomFilterUtil(long expectInsertions, double falsePositive, Funnel funnel) {
        this.funnel = funnel;
        bitSize = optimalNumOfBitSize(expectInsertions, falsePositive);
        numHashFunction = optimalNumOfHashFunction(bitSize, expectInsertions);
    }
    
    /**
     * 源码分析中可以看到这个哈希算法 又
     * strategy 哈希算法
     * @param value
     * @return
     */
    @SuppressWarnings("all")
    public int[] murmurHashOffset(T value) {
        int[] offset = new int[numHashFunction];
        
        long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
        int hash1 = (int) hash64;
        int hash2 = (int) (hash64 >>> 32);
        for (int i = 1; i <= numHashFunction; i++) {
            int nextHash = hash1 + i * hash2;
            if (nextHash < 0) {
                nextHash = ~nextHash;
                
            }
            offset[i - 1] = nextHash % bitSize;
        }
        return offset;
    }
    
    /**
     * 获取最佳的位图数组大小
     * @param expectInsertions 期望插入多少数据
     * @param falsePositive 误判率
     * @return 位图数组大小
     * 经过计算 公式 m[位图最佳大小] = (-n[插入数据量] * ln(p[误判率]))/(ln2)^2
     */
    
    public int optimalNumOfBitSize(long expectInsertions, double falsePositive) {
        return (int) (((-expectInsertions) * Math.log(falsePositive)) / (Math.log(2) * Math.log(2)));
    }
    /**
     * 获取最佳执行哈希函数的次数
     * @param expectInsertions 期望插入多少数据
     * @param bitSize 位图数组大小
     * @return 哈希函数执行的最佳次数
     */
    public int optimalNumOfHashFunction(long bitSize, long expectInsertions) {
        return (int) Math.max(1, (double)  bitSize/expectInsertions  * Math.log(2));
    }
}

这里就完成好了一个简易的BloomFilter了,实现都实现完了,今天我觉得我也差不多了。

最后再给大家写了一个比较简单的封装工具类作为BloomFilter的缓存盾牌,确实印证了那句话,站在布隆后面~

因为就是比较简单的增删改查操作,以及值的判定是否存在于内存当中~

读者们可以放心食用,进行测试与学习~

/**
 * redis操作布隆过滤器
 *
 * @param <T>
 * @author Alascanfu
 */
public class RedisBloomFilter<T> {
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * 删除缓存的KEY
     *
     * @param key KEY
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }
    
    /**
     * 根据给定的布隆过滤器添加值,在添加一个元素的时候使用,批量添加的性能差
     *
     * @param bloomFilterHelper 布隆过滤器对象
     * @param key               KEY
     * @param value             值
     * @param <T>               泛型,可以传入任何类型的value
     */
    public <T> void add(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
            redisTemplate.opsForValue().setBit(key, i, true);
        }
    }
    
    /**
     * 根据给定的布隆过滤器添加值,在添加一批元素的时候使用,批量添加的性能好,使用pipeline方式(如果是集群下,请使用优化后RedisPipeline的操作)
     *
     * @param bloomFilterHelper 布隆过滤器对象
     * @param key               KEY
     * @param valueList         值,列表
     * @param <T>               泛型,可以传入任何类型的value
     */
    public <T> void addList(BloomFilterHelper<T> bloomFilterHelper, String key, List<T> valueList) {
        redisTemplate.executePipelined(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                connection.openPipeline();
                for (T value : valueList) {
                    int[] offset = bloomFilterHelper.murmurHashOffset(value);
                    for (int i : offset) {
                        connection.setBit(key.getBytes(), i, true);
                    }
                }
                return null;
            }
        });
    }
    
    /**
     * 根据给定的布隆过滤器判断值是否存在
     *
     * @param bloomFilterHelper 布隆过滤器对象
     * @param key               KEY
     * @param value             值
     * @param <T>               泛型,可以传入任何类型的value
     * @return 是否存在
     */
    public <T> boolean contains(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
            if (!redisTemplate.opsForValue().getBit(key, i)) {
                return false;
            }
        }
        return true;
    }
}

大功告成! 博主后续会更新redis的相关部分哦 ~ 因为最近在复习这一板块

感谢

非常感谢

熬丙三太子

博文《Redis-避免缓存穿透的利器之BloomFilter》:链接地址。

给了作者很多的启发与帮助 以及 他对redis不菲的见解

同时 也感谢 正在阅读此篇博文的你

学无止境 每天学一点 每天收获一点

博主在这里 祝愿 诸君能 事业有成 学有所得

写在最后

最后本文共计3k字

希望读者们慢慢食用

加油!致敬成长道路上的你和我~

如果可以 请记得 动动小手 一键三连

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alascanfu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值