干货 | 使用布隆过滤器实现高效缓存

简介: 本文主要描述,使用布隆过滤实现高效缓存。文中采用数组做为缓存,如果需要高并发命中,则需将文中的数组换成Redis数据库。

1

 前言  

本文主要描述,使用布隆过滤实现高效缓存。文中采用数组做为缓存,如果需要高并发命中,则需将文中的数组换成Redis数据库。


2

 布隆过滤  

布隆缓存的创建过程如下:

1,先定义缓存bit数组(BitArray),数组的长度就是缓存数据的最大数量。

2,然后将字符串通过哈希运算,求出它的HashCode。

3,然后将HashCode作为伪随机数生成器(Random)的种子,生成一个小于最大数量的正数x。

4,然后将这x作为缓存数组的索引,将数组[x]的值设置为true。

布隆过滤

将获取到的字符串,通过上述前三步运算,计算出数组索引,然后在布隆缓存里取出指定索引的值,如果为True,则缓存存在,可以使用这个字符串去真正的数据缓存中取数据,如果未Fasle,则缓存不存在则去数据库中取数据。


3

代码实现  

首先建立WinForm项目BloomTest。

然后编写布隆过滤器,代码如下:


public class BloomFilter
{
    //布隆缓存数组
    public BitArray BloomCache;
    //布隆缓存数组的长度
    public Int64 BloomCacheLength { get; } 
    public Int64 HashCount { get; }
    /// 
    /// 
    /// 
    /// 布隆缓存数组的长度,默认20000 
    /// hash运算次数,默认3
    public BloomFilter(int bloomCacheLength = 20000,  int hashCount = 3)
    {
        BloomCache = new BitArray(bloomCacheLength);
        BloomCacheLength = bloomCacheLength; 
        HashCount = hashCount;
    }
    public void Add(string str)
    {
        var hashCode =str.GetHashCode();
        Random random = new Random(hashCode);
        for (int i = 0; i < HashCount; i++)
        {
            var x = random.Next((int)(BloomCacheLength - 1));
            BloomCache[x] = true;
        }
    }
    public bool IsExist(string str)
    {
        var hashCode = str.GetHashCode();
        Random random = new Random(hashCode);
        for (int i = 0; i < HashCount; i++)
        {
            if (!BloomCache[random.Next((int)(BloomCacheLength - 1))])
            {
                return false;
            }
        }
        return true;
    }
    //错误率查看
    public double GetFalsePositiveProbability(double setSize)
    {
        // (1 - e^(-k * n / m)) ^ k
        return Math.Pow((1 - Math.Exp(-HashCount * setSize / BloomCacheLength)), HashCount);
    }
    //计算基于布隆过滤器散列的最佳数量,即hashCount的最佳值
    public int OptimalNumberOfHashes(int setSize)
    {
        return (int)Math.Ceiling((BloomCacheLength / setSize) * Math.Log(2.0));
    }
}

然后编写布隆过滤器的使用代码,如下:


    public partial class Form1 : Form
    {
        BloomFilter bloom = new BloomFilter(20000, 3);
        int setSize = 2000;
        public Form1()
        {
            InitializeComponent();
            //生成布隆缓存数组
            for (int i = 0; i < setSize; i++)
            {
                bloom.Add("kiba" + i);
            }
        } 
        private void btnSearch_Click(object sender, EventArgs e)
        { 
            Stopwatch sw = new Stopwatch();
            sw.Start();
            string con = tbCon.Text.Trim();
            var ret = bloom.IsExist(con);
            sw.Stop();
            lblRet.Text = $@"结果:{ret}{Environment.NewLine}
耗时:{sw.ElapsedTicks}{Environment.NewLine}
错误概率:{bloom.GetFalsePositiveProbability(setSize)}{Environment.NewLine} 
最佳数量:{bloom.OptimalNumberOfHashes(setSize)}"; 
        } 
    }


测试结果

运行项目,点击查询数据。


54.png


如上图所示,我们成功命中了,如果在项目中,命中了就可以查询真实缓存了。

错误概率

布隆缓存存在命中错误,即如果两个数据的哈希运算后值相同,那么久会存在命中失败的问题。

错误概率可以通过哈希运算次数和布隆缓存数组长度和插入数据数量计算出来。

最佳数量

布隆缓存建议我们多做几次哈希运算,即多存几个缓存索引,文中默认创建3个。

我们代码中,向布隆缓存数组中插入了2000个数据,通过计算得出,最佳的哈希运算次数为7,即当插入数量为2000,布隆缓存数组长度为20000时,HashCount的最优值为7。

应用场景

布隆缓存有很多场景可以应用,比如重复URL检测,黑名单验证,垃圾邮件过滤等等。

举个例子,爬虫在爬取网站之前,会先通过布隆过滤计算出该Url是否已经爬取过,再确定是否发起Http请求。


4

关于缓存穿透、缓存击穿、缓存雪崩

缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,这时用户不断的发起这样的请求,会对数据库和缓存造成比较大的压力。

解决方案:增加更多,更有效的数据校验,让这些请求在进入查询前被拦截。将缓存和数据库中都没有的数据写入缓存,并设置一个较短的有效期,用来防止该请求多次进入到数据库。

缓存击穿

缓存击穿是指缓存中的数据正好到期,然后又突然出现大量该数据的访问。导致大量请求直接发送到数据库。

解决方案:对数据进行热点标记,然后对热点数据进行特殊有效期设置。对普通数据进行有效期延长处理,比如被请求过一次,加长固定时间的有效期。

缓存雪崩

缓存雪崩与缓存击穿的意思类似,区别在于,缓存击穿指的是只有一条数据直接请求数据库,而雪崩指的是很多这样的数据直接请求数据库。

解决方案:缓存数据库分布式部署。


5

 结语  

布隆缓存因为存在误判,所以不能用于百分之百定位数据的场景,但如果该场景可以容错,那布隆缓存将大大提升性能。


代码已经传到Github上了,欢迎大家下载。

Github地址:https://github.com/kiba518/BloomFilter_Kiba

本文作者:kiba518,全栈.Net软件工程师

声明:本文为 脚本之家专栏作者 投稿,未经允许请勿转载。

相关文章
|
6天前
|
存储 缓存 NoSQL
Redis 布隆过滤器实战「缓存击穿、雪崩效应」
Redis 布隆过滤器实战「缓存击穿、雪崩效应」
98 1
Redis 布隆过滤器实战「缓存击穿、雪崩效应」
|
缓存 数据格式
实现LRU缓存的三种方式(建议收藏)
LRU全称为Least Recently Used,即最近使用的。针对的是在有限的内存空间内,只缓存最近使用的数据(即get和set的数据),超过有限内存空间的数据将会被删除。这个在面试题中也是常会被问到的内容,接下来就看看怎么来实现。
1010 0
实现LRU缓存的三种方式(建议收藏)
|
6天前
|
缓存 NoSQL Java
【二十六】springboot整合jedis和redisson布隆过滤器处理缓存穿透
【二十六】springboot整合jedis和redisson布隆过滤器处理缓存穿透
93 0
|
9月前
|
存储 缓存 NoSQL
用实战演练如何通过布隆过滤器防止缓存击穿
为什么引入 我们的业务中经常会遇到穿库的问题,通常可以通过缓存解决。如果数据维度比较多,结果数据集合比较大时,缓存的效果就不明显了。 因此为了解决穿库的问题,我们引入Bloom Filter。 适合的场景 数据库防止穿库 Google Bigtable,Apache HBase和Apache Cassandra以及Postgresql 使用BloomFilter来减少不存在的行或列的磁盘查找。避免代价高昂的磁盘查找会大大提高数据库查询操作的性能。如同一开始的业务场景。如果数据量较大,不方便放在缓存中。需要对请求做拦截防止穿库。 缓存宕机 缓存宕机的场景,使用布隆过滤器会造成一定程度的误判。
85 0
|
8月前
|
存储 缓存 NoSQL
从原理到实战:如何通过布隆过滤器防止缓存击穿
我们的业务中经常会遇到穿库的问题,通常可以通过缓存解决。如果数据维度比较多,结果数据集合比较大时,缓存的效果就不明显了。 因此为了解决穿库的问题,我们引入Bloom Filter。
|
10月前
|
存储 缓存 NoSQL
【Java项目】布隆过滤器解决缓存穿透问题以及布隆过滤器删除困难问题
【Java项目】布隆过滤器解决缓存穿透问题以及布隆过滤器删除困难问题
198 0
|
机器学习/深度学习 缓存 Oracle
【数据库设计与实现】第7章:缓存与检查点
缓存与检查点设计原则数据缓冲区与检查点是相辅相成的,所以放在同一个章节介绍。由于CPU与持久化设备之间存在巨大的速度差距,所以在内存中引入缓冲区缩小这个差距。从读的角度来看,将热点数据或预判用户可能读取的数据提前加载到内存中,从而将持久化设备的读时延和带宽提升至内存的时延和带宽。从写的角度来看,直接修改缓冲区中的数据而不是磁盘中的数据,可以带来两方面的优势。其一,将持久化设备的写时延和带宽提升至内
【数据库设计与实现】第7章:缓存与检查点
|
缓存 JSON Java
java 实现读取txt文件,反射创建对象,android 手机缓存文件目录
java 实现读取txt文件,反射创建对象,android 手机缓存文件目录
345 1
java 实现读取txt文件,反射创建对象,android 手机缓存文件目录
|
存储 缓存 算法
基于LinkedHashMap实现LRU缓存
基于LinkedHashMap实现LRU缓存
171 0
基于LinkedHashMap实现LRU缓存
|
缓存 算法 安全
如何使用 LinkedHashMap 实现 LRU 缓存?
在上一篇文章里,我们聊到了 HashMap 的实现原理和源码分析,在源码分析的过程中,我们发现一些 LinkedHashMap 相关的源码,当时没有展开,现在它来了。 那么,LinkedHashMap 与 HashMap 有什么区别呢?其实,LinkedHashMap 的使用场景非常明确 —— LRU 缓存。今天,我们就来讨论 LinkedHashMap 是如何实现 LRU 缓存的。
136 0