Redis——布隆过滤器

第一次听到布隆是从英雄联盟中,本篇讲的是布隆过滤器,是Redis避免缓存穿透的防御利器; 

1. 布隆过滤器的基本概念

布隆过滤器(英語:Bloom Filter)是1970年由一个叫布隆的小伙子提出的;它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难;

——《布隆过滤器——维基百科》

简单来说就是:当我们通过布隆过滤器判断一个元素在不在集合中时,如果布隆过滤器返回的是在集合中,那么集合中可能有这个元素,但因为误判率也可能没有这个元素;如果布隆过滤器返回的不存在于集合中,那么集合中是一定不存在这个元素的;常见的使用场景就是作为避免Redis缓存穿透的利器;

2. Redis缓存穿透

举个例子,你的数据库新增一条记录一般都会自增主键加1,主键id是从1开始然后自增的,为了保证查询性能,我们每新增/更新一条记录就把它放入Redis中,并设置过期时间;

如果Hacker知道你的接口是通过id查询的,就拿负数去查询,这个时候,会发现缓存里面没这个数据,然后又去数据库查询一次发现也没有;一个请求这样,100个,1000个,10000个的这样的无效请求呢?每次请求都会绕过Redis而直接走到DB查询,这样你的DB基本上就扛不住了,也就是Redis缓存看起来被"穿透"了,大量请求可能把DB打挂掉;

解决思路是,如果在查询缓存之前,先判断当前请求所带的id是否在有效的id集合中,如果判断集合里没这个id就不去查了,直接return一个数据为空不就好了嘛。

所以问题回到了——如何高效判断一个元素是否在集合内

2. 判断一个元素是否在集合内

如果想判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较确定;Java中最常见的就是HashSet(HashMap);

2.1 消耗空间分析

(1)传统方案:将集合M的用散列表保存,然后对数x判断是否在散列表中即可;内存计算:假设使用int类型来保存,那么10亿个数消耗的内存 = 4byte(int)*10亿 ≈ 381M。

(2)位图方案:由于我们只关心数x是否存在于集合中,因此我们通标记0和1来标记,通下标来表示数;即开辟空间使用byte数组来保存,那么10亿个数消耗的内存 = 1byte*10亿 ≈ 95M。

上面看去视乎消耗不大,那么我们现在将数据量扩大10倍,即有100亿个数,此时传统方案大约需要3.7G;使用位图的方案大约需要0.9G;

想想我们服务器的内存才多少个G,而这个还仅仅只是这个Java程序中的一个Java对象,同时其他外在因素都未考虑进去,而我们这仅仅是系统中一个小小的判断是否存在的功能;无论是使用位图方案还是传统方案都不是太符合要求;

其实,不要觉得10亿数据很多;假如公司的一个业务有1000万用户,平均每个用户一天产生10条数据,一个月(30天)就是3亿条数据,那么一年就是36亿条数据;而这只是一个业务功能中的一个表,如果有多个的话...

2.2 HashMap的问题

讲述布隆过滤器的原理之前,我们先思考一下,通常你判断某个元素是否存在用的是什么?应该蛮多人回答HashMap吧,确实可以将值映射到HashMap的Key,然后可以在O(1)的时间复杂度内返回结果,效率奇高;但是HashMap的实现也有缺点,例如存储容量占比高,考虑到负载因子的存在,通常空间是不能被用满的,而一旦你的值很多例如上亿的时候,那HashMap占据的内存大小就变得很可观了;

还比如说你的数据集存储在远程服务器上,本地服务接受输入,而数据集非常大不可能一次性读进内存构建HashMap的时候,也会存在问题;

2.3 BloomFilter基本思想

将所有元素保存起来,然后通过比较确定,链表,树等数据结构都是这种思路;但是随着集合中元素的增加,通过上面的内存消耗分析,我们知道需要的存储空间越来越大,检索速度也越来越慢;不过世界上还有一种叫作散列表(又叫哈希表,Hashtable)的数据结构;它可以通过一个Hash函数将一个元素映射成一个位阵列(BitArray)中的一个点;这样一来,我们只要看看这个点是不是1就知道可以集合中有没有它了;这就是布隆过滤器的基本思想;

Hash面临的问题就是冲突;

传统的散列表设计使用1个hash的话,会出现hash冲突;因此在布隆过滤器中选择使用多种不同的hash算法得到多个hash值,有多个hash值来定位一个数的位置,这样就可以有效的减小hash冲突的概率了;

假设Hash函数是良好的,如果我们的位阵列长度为m个点,那么如果我们想将冲突率降低到例如1%,这个散列表就只能容纳m/100个元素;显然这就不叫空间有效了(Space-efficient);解决方法也简单,就是使用多个Hash;

但是由于其散列表的基础特性依旧没有突破,因此依然会存在误判的可能(从图中可以很容易的发现),这也就就是布隆过滤器的特性总结一句话就是:

"告诉你没有那是真的没有;告诉你有可能是有,也可能是没有。你细品...像不像你问你女票的时候的情况,区别就布隆过滤器说没有那就是真的没有 :)"

因此布隆过滤器适合的场景是对误判有一定容忍度的场景,比如说:垃圾邮箱、钓鱼网址、URL地址判重(比如爬虫)、海量图库判重、推荐算法(比如新闻资讯这些推荐给用户,但是需要将用户看过的去掉,如果通过数据库里面的历史记录来去重,时间久了数据量会很大...)

引入布隆过滤器后,也会带来新的问题,导致将系统业务复杂化。正所谓 ‘鱼与熊掌不可兼得’;因此需要仔细思量。

3. BloomFilter的基本原理

布隆过滤器的原理是:(1)当一个元素加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点把它们置为1;(2)检索时,让当前元素依次通过K个散列函数计算,我们只要看看这K个点是不是都是1就(大约)知道集合中有没有它了,即可以反推:如果这K个点中有有任何一个0,则被检元素一定不在;如果都是1,则被检元素E很可能在。这就是布隆过滤器的基本原理;

可以看下面的图,上面是S集合的元素分别通过函数hi(i=1,2,3)找到落在序列B上的3个点,然后将这些点置为1;下面判断d、e是否在集合中时,分别用d、e计算出点位,判断是否全是1(存在0则可以直接返回false);

总结一下,步骤如下:

1. 首先需要k个hash函数,每个函数可以把key散列成为1个整数;
2. 初始化时,需要一个长度为n比特的数组,每个比特位初始化为0;
3. 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为1;
4. 判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中;

Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率;

4. BloomFilter的优缺点

优点

相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势;布隆过滤器存储空间和插入/查询时间都是常数。另外,Hash 函数相互之间没有关系,方便由硬件并行实现;布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势;

缺点

bloom filter之所以能做到在时间和空间上的效率比较高,是因为牺牲了判断的准确率、删除的便利性;布隆过滤器的缺点和优点一样明显。误算率(False Positive)是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。

另外,删除困难也是缺点之一;一般情况下不能从布隆过滤器中删除元素;一个放入容器的元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断;

我们很容易想到把位列阵变成整数数组(位点命中时不仅仅置为1,而是存放1的数量),每插入一个元素相应的计数器加1,这样删除元素时将计数器减掉就可以了;然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面;这一点单凭这个过滤器是无法保证的;另外计数器回绕也会造成问题。目前可以采用Counting Bloom Filter;

5. 误判率False Positives概率推导

这一部分涉及数学知识,纯个人兴趣,可以略过;

(1)假设Hash函数以等概率条件选择并现在开始设置BitArray中的某一位,令:m是该位数组的大小(长度),k是Hash函数的个数;那么对于位数组BitArray中任意某一位P1,在进行1次元素插入时通过1次Hash操作使得P1置位"1"的概率是:

\frac{1}{m}

那么位数组中P1在进行元素插入时的1次Hash操作中没有被置位"1"的概率是:

1-\frac{1}{m}

那么在所有k次Hash操作后该位都没有被置 "1" 的概率是:

(1-\frac{1}{m})^{k}

以上是进行1次元素插入时的概率计算,如果我们插入了n个元素,那么某一位P1仍然为 "0" 的概率是:

(1-\frac{1}{m})^{kn}

从而,在插入了n个元素的条件下,那么某一位P1"1" 的概率是:

1-(1-\frac{1}{m})^{kn}

现在,检测某一元素是否在该集合中;即计算——"集合中插入了n个元素后,某个元素经过k个Hash函数计算后的k个位置(上一步计算的是针对某一位置P1),这k个位置都被设置为1"的概率,这个概率是针对任意元素的,也就是说该方法可能会使算法错误的认为某一原本不在集合中的元素却被检测为在该集合中False Positives),给予上一步的结果,该概率由以下公式确定:

(1-[1-\frac{1}{m}]^{kn})^{k} \approx (1-e^{-kn/m})^{k}

其实,上述结果是在假定由每个Hash计算出需要设置的位(bit)的位置是相互独立为前提计算出来的,不难看出,随着m(位数组大小)的增加,假正例(False Positives)的概率会下降,同时随着插入元素个数n的增加,FalsePositives的概率又会上升

(1)BitArray大小的选择

对于给定的False Positives概率 p,如何选择最优的位数组大小m由以下公式确定:

m = -\frac{n\cdot lnp}{(ln2)^{2}}

上式表明,位数组的大小m最好与插入元素的个数n成线性关系;

(2)哈希函数个数k的选择

对于给定的m,n,如何选择Hash函数个数k由以下公式确定:

k = \frac{m}{n}\cdot ln2

上式表明,Hash函数个数k与位数组的大小m成正比,与插入元素的个数n成反比;

哈希函数个数k、位数组大小m、加入的字符串数量n的关系可以参考Bloom Filters - the mathBloom_filter-wikipedia

6. BloomFilter的使用

6.1 Java本地内存使用布隆过滤器

引入依赖:

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>29.0-jre</version>
</dependency>

测试类:

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
 
public class BloomFilterTest {
 
    /** 预计插入的数据 */
    private static Integer expectedInsertions = 10000000;
    /** 误判率 */
    private static Double fpp = 0.01;
    /** 布隆过滤器 */
    private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(),expectedInsertions,fpp);
 
    public static void main(String[] args) {
        // 插入 1千万数据
        for (int i = 0; i < expectedInsertions; i++) {
            bloomFilter.put(i);
        }
 
        // 从10000000开始,用1千万数据测试误判率
        int count = 0;
        for (int i = expectedInsertions; i < expectedInsertions *2; i++) {
            if (bloomFilter.mightContain(i)) {
                count++;
            }
        }
        System.out.println("一共误判了:" + count);
 
    }
 
}

测试结果:

一共误判了:100055 //大概是expectedInsertions(1千万)的0.01,这与我们设置的 p = 0.01非常接近。

参数说明

在guava包中的BloomFilter源码中,构造一个BloomFilter对象有四个参数:

  • Funnel funnel:数据类型,由Funnels类指定即可
  • long expectedInsertions:预期插入的值的数量
  • fpp:错误率
  • BloomFilter.Strategy:hash算法

通过断点BloomFilter类中的构造函数,发现我们调整expectedInsertions和误判率p时,位数组BitArray的大小m(numBits)和Hash函数的个数k(numHashFunctions)都会自适应变化;

6.2 Java集成Redis使用布隆过滤器

Redis经常会被问道缓存击穿问题,比较优秀的解决办法是使用布隆过滤器,也有使用空对象解决的,但是最好的办法肯定是布隆过滤器,我们可以通过布隆过滤器来判断元素是否存在,避免缓存和数据库都不存在的数据进行查询访问;在如下的代码中只要通过bloomFilter.contains(xxx)即可,我这里演示的还是误判率;

引入依赖:

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.16.0</version>
</dependency>

测试类:

import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
 
public class RedisBloomFilterTest {
 
    /** 预计插入的数据 */
    private static Integer expectedInsertions = 10000;
    /** 误判率 */
    private static Double fpp = 0.01;
 
    public static void main(String[] args) {
        // Redis连接配置,无密码
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.211.108:6379");
        // config.useSingleServer().setPassword("123456");
 
        // 初始化布隆过滤器
        RedissonClient client = Redisson.create(config);
        RBloomFilter<Object> bloomFilter = client.getBloomFilter("user");
        bloomFilter.tryInit(expectedInsertions,fpp);
 
        // 布隆过滤器增加元素
        for (Integer i = 0; i < expectedInsertions; i++) {
            bloomFilter.add(i);
        }
 
        // 统计元素
        int count = 0;
        for (int i = expectedInsertions; i < expectedInsertions*2; i++) {
            if (bloomFilter.contains(i)) {
                count++;
            }
        }
        System.out.println("误判次数" + count);
 
    }
 
}

7. BloomFilter的应用场景

根据布隆过滤器的特性,它可以告诉我们 “某个元素一定不存在集合中或者可能存在集合中”,也就是说布隆过滤器说这个数不存在则一定不存,布隆过滤器说这个数存在可能不存在(误判);以下是它的常见的应用场景:

  • 解决Redis缓存穿透问题(面试重点)
  • 邮件过滤,使用布隆过滤器来做邮件黑名单过滤
  • 对爬虫网址进行过滤,爬过的不再爬
  • 解决新闻推荐过的不再推荐(类似抖音刷过的往下滑动不再刷到)
  • HBase\RocksDB\LevelDB等数据库内置布隆过滤器,用于判断数据是否存在,可以减少数据库的IO请求

总结

布隆过滤器主要是在Redis防止缓存穿透的时候引出来的,文章里面还是写的比较复杂了,其实在面试中只要回答其基本原理和思想,还有就是知道他的使用场景即可;


参考:

布隆过滤器详解 - 简书

Redis-避免缓存穿透的利器之BloomFilter - 掘金

布隆过滤器 - 简书

布隆(Bloom Filter)过滤器——全面讲解

原文地址:https://blog.csdn.net/minghao0508

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


文章浏览阅读752次。关系型数据库关系型数据库是一个结构化的数据库,创建在关系模型(二维表模型)基础上,一般面向于记录SQL语句(标准数据查询语言)就是一种基于关系型数据库的语言,用于执行对关系型数据库中数据的检索和操作主流的关系数据库包括Oracle、Mysql、SQL Server、Microsoft Access、DB2等非关系型数据库NoSQL(nOSQL=Not Only SQL),意思是“不仅仅是SQL”,是非关系型数据库的总称。除了主流的关系型数据库外的数据库,都认为是非关系型主流的NoSQ.._redis是非关系型数据库吗
文章浏览阅读687次,点赞2次,收藏5次。商城系统中,抢购和秒杀是很常见的营销场景,在一定时间内有大量的用户访问商场下单,主要需要解决的问题有两个:1. 高并发对数据库产生的压力;2. 竞争状态下如何解决商品库存超卖;高并发对数据库产生的压力对于第一个问题,使用缓存来处理,避免直接操作数据库,例如使用 Redis。竞争状态下如何解决商品库存超卖对于第二个问题,需要重点说明。常规写法:查询出对应商品的库存,判断库存数量否大于 0,然后执行生成订单等操作,但是在判断库存是否大于 0 处,如果在高并发下就会有问题,导致库存_php库存结余并发
文章浏览阅读1.4k次。MongoTemplate开发spring-data-mongodb提供了MongoTemplate和MongoRepository两种方式访问MongoDB,MongoRepository的方式访问较为简单,MongoTemplate方式较为灵活,这两种方式在Java对于MongoDB的运用中相辅相成。_springboot插入指定的mongodb数据库
文章浏览阅读887次,点赞10次,收藏19次。1.背景介绍1. 背景介绍NoSQL数据库是一种非关系型数据库,它的特点是可以存储非结构化的数据,并且可以处理大量的数据。HBase是一个分布式、可扩展的列式存储系统,它是基于Google的Bigtable设计的。HBase是一个开源的NoSQL数据库,它的核心功能是提供高性能的随机读写访问。在本文中,我们将对比HBase与其他NoSQL数据库,例如Redis、MongoDB、Cass...
文章浏览阅读819次。MongoDB连接失败记录_edentialmechanisn-scram-sha-1
文章浏览阅读470次。mongodb抽取数据到ES,使用ELK内部插件无法获取数据,只能试试monstache抽取mongodb数据,但是monstache需要mongodb replica set 模式才能采集数据。############monstache-compose文件。#replicas set 启动服务。# 默认备份节点不能读写,可以设置。# mydb指的是需要同步的数据库。#登录主mongodb初始化rs。#primary 创建用户。# ip地址注意要修改。# ip地址注意要修改。_monstache csdn
文章浏览阅读913次,点赞4次,收藏5次。storage:fork: trueadmin登录切换数据库注意: use 代表创建并使用,当库中没有数据时默认不显示这个库删除数据库查看表清单> show tables # 或者 > show collections表创建db.createCollection('集合名称', [options])table1字段类型描述capped布尔(可选)如果为 true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。_mongodb5
文章浏览阅读862次。Centos7.9设置MongoDB开机自启(超全教程,一条龙)_mongodb centos开机启动脚本
文章浏览阅读1.3k次,点赞6次,收藏21次。NoSQL数据库使用场景以及架构介绍
文章浏览阅读856次,点赞21次,收藏20次。1.背景介绍1. 背景介绍NoSQL数据库是一种非关系型数据库,它的设计目标是为了解决传统关系型数据库(如MySQL、Oracle等)在处理大量不结构化数据方面的不足。NoSQL数据库可以处理大量数据,具有高性能、高可扩展性和高可用性。但是,与关系型数据库不同,NoSQL数据库没有固定的模式,数据结构也不一定是表格。在NoSQL数据库中,数据存储和查询都是基于键值对、列族、图形等不同的...
文章浏览阅读416次。NoSQL定义:非关系型、分布式、开放源码和具有横向扩展能力的下一代数据库。由c++编写的开源、高性能、无模式的基于分布式文件存储的文档型数据库特点:高性能、高可用性、高扩展性、丰富的查询支持、可替换已完场文档某个指定的数据字段应用场景:社交场景:使用mongodb存储用户信息游戏场景:用户信息,装备积分物流场景:订单信息,订单状态场景操作特点:数据量大;读写操作频繁;价值较低的数据,对事物性要求不高开源、c语言编写、默认端口号6379、key-value形式存在,存储非结构化数据。_nosql
文章浏览阅读1.5k次,点赞3次,收藏2次。Exception in thread "main" redis.clients.jedis.exceptions.JedisConnectionException: Failed to create socket. at redis.clients.jedis.DefaultJedisSocketFactory.createSocket(DefaultJedisSocketFactory.java:110) at redis.clients.jedis.Connection.connect(Conne_redis.clients.jedis.exceptions.jedisconnectionexception: failed to create so
文章浏览阅读6.5k次,点赞3次,收藏12次。readAnyDatabase(在所有数据库上都有读取数据的权限)、readWriteAnyDatabase(在所有数据库上都有读写数据的权限)、userAdminAnyDatabase(在所有数据库上都有管理user的权限)、dbAdminAnyDatabase(管理所有数据库的权限);:clusterAdmin(管理机器的最高权限)、clusterManager(管理和监控集群的权限)、clusterMonitor(监控集群的权限)、hostManager( 管理Server);_mongodb创建用户密码并授权
文章浏览阅读593次。Redis是一个基于内存的键值型NoSQL数据库,在实际生产中有着非常广泛的用处_搭建本地redis
文章浏览阅读919次。Key 的最佳实践[业务名]:[数据名]:[id]足够简短:不超过 44 字节不包含特殊字符Value 的最佳实践:合理的拆分数据,拒绝 BigKey选择合适数据结构Hash 结构的 entry 数量不要超过 1000(默认是 500,如果达到上限则底层会使用哈希表而不是 ZipList,内存占用较多)设置合理的超时时间批量处理的方案:原生的 M 操作Pipeline 批处理注意事项:批处理时不建议一次携带太多命令。Pipeline 的多个命令之间不具备原子性。_redis高级实战
文章浏览阅读1.2k次。MongoDB 递归查询_mongodb数据库 递归
文章浏览阅读1.2k次。通过实际代码例子介绍:如何通过MongoTemplate和MongoRepository操作数据库数据_springboot操作mongodb
文章浏览阅读687次,点赞7次,收藏2次。首先欢迎大家阅读此文档,本文档主要分为三个模块分别是:Redis的介绍及安装、RedisDesktopManager可视化工具的安装、主从(哨兵)模式的配置。_redis 主从配置工具
文章浏览阅读764次。天下武功,无坚不摧,唯快不破!我的名字叫 Redis,全称是 Remote Dictionary Server。有人说,组 CP,除了要了解她外,还要给机会让她了解你。那么,作为开发工程师的你,是否愿意认真阅读此心法抓住机会来了解我,运用到你的系统中提升性能。我遵守 BSD 协议,由意大利人 Salvatore Sanfilippo 使用 C 语言编写的一个基于内存实现的键值型非关系(NoSQL)..._redis 7.2 源码
文章浏览阅读2k次。MongoDB 的增删改查【1】_mongodb $inc