【IT老齐008】布隆过滤器在亿级流量系统中的应用

180 阅读13分钟

配合视频效果更佳:www.itlaoqi.com/chapter.htm…

本节咱们来学习布隆过滤器在亿级流量系统中的应用,什么是布隆过滤器?请听我道来。为了便于大家理解,我这里使用一个真实的案例来进行讲解。

来咱们看一下,大家都应该上过淘宝京东这样的购物网站,大家注意过没有?在我们日常开发中,其实每一个页面的URL网址、是和具体的内容所对应的,

比如说当前我标红的857,这就是我们商品的sku编号,作为 Sku编号,你可以理解为是这个商品型号的唯一编码,这里商品编号为857,那显示的页面自然也是对应的内容,作为这样的商品信息我们该如何处理,这就涉及到我们系统的架构了。

来看一下。首先作为商城的用户,他发起一个请求,比如这里还是要查看857号的商品,这时作为商城应用程序,它会向后台的redis缓存服务器进行查询,如果缓存数据库中没有857号的商品数据,我们程序就需要在后台的数据库服务器中进行查询,并且填充至redis服务器,这是一个正常的操作流程。那么在长时间的积累后,我们的缓存服务器里边的数据可能就是这个样子的。      

 为了好理解这里,我假设我们商城有1000件商品编号,从11000,随着时间的不断积累,那么在redis服务器中应该会存储编号从11000的所有的商品数据缓存。此时此刻作为商城用户,如果查询857号商品时,商城应用就不再需要从MySQL数据库中进行数据提取,直接从redis服务器中将数据提取并返回就可以了。因为redis它是基于内存的,无论是从吞吐量还是处理速度来说,都要比传统的mysql数据库要快很多倍。所以无论是电商还是其他行业,都会在自己的系统架构中增加redis缓存服务器来优化系统的执行速度。

但事无完美。在当前的设计下有一个致命问题,大家请注意,当前缓存中只有1~1000号数据,假设如果是同行恶意竞争,或者由第三方公司研发了爬虫机器人,在短时间内批量的进行数据查询,而这些查询的编号则是之前数据库中不存在的,

比如现在看到的8888、8889、8890这些都是不存在的,此时我们系统就会遇到一个重大的安全隐患,因为商城应用在向后台redis查询时,因为缓存中没有这条数据,它就进而到数据库服务器中来进行查询。要知道数据库服务器对于瞬时超高并发的访问承载能力是并不强的,所以在短时间内由爬虫机器人或者流量攻击机器人发来的这些无效的请求都会瞬间的灌入到数据库服务器中,对我们的系统的性能造成极大的影响,甚至会产生系统崩溃。这种绕过redis服务器,直接进入后台数据库查询的攻击方式,我们称之为缓存穿透,对于小规模的缓存穿透是不会对我们系统产生大的影响,但是如果是缓存穿透攻击又是另外一码事了。

缓存穿透攻击是指恶意用户在短时内大量查询不存在的数据,导致大量请求被送达数据库进行查询。当请求数量超过数据库负载上线时,使系统响应出现高延迟甚至瘫痪的攻击行为,就是缓存穿透攻击。大家不要小看这种攻击方式,在多年前的618前夕,我们就遭到了其他平台的恶意缓存穿透攻击,导致商城应用当机两个小时造成的损失非常巨大,所以后来我们也增加了对于缓存穿透攻击的预防。那么如何预防缓存穿透在架构设计时有一种最常见的设计被称为布隆过滤器,通过布隆过滤器可以有效的减少缓存穿透的情况。我们来看一下到底什么是布隆过滤器呢?

所谓布隆过滤器是巴顿布隆于1970年提出的,其主旨是采用一个很长的二进制数组,通过一系列的Hash函数来确定该数据是否存在。这么说可能有些晦涩。我们通过一系列的图表演示你就明白了,作为布隆过滤器,它本质上是一个n位的二进制数组,都知道二进制它只有0和1来进行表示。

针对于当前我们的场景,这里我模拟了一个二进制数组,其每一位它的初始值都是0,这个二进制数组会被存储在redis服务器中,这个数组该怎么用?刚才我们提到作为当前的商城,假设有1000个商品编号,从1~1000作为布隆过滤器在初始化的时候,实际上就是对每一个商品编号进行若干次的希来确定他们的位置。

比如说针对于当前的1编号,我们对其执行了三次hash,所谓hash函数就是将数据带入以后确定一个具体的位置,比如hash函数当对编号1进行第一次hash时,它会定位到二进制数组的第二位上,并将其数值从0改为1。hash2函数它定位到索引值为5的位置,并将0改为1,hash3函数定位到索引99的位置上,将其从0改为1。

那1号商品计算完以后,该轮到2号商品,2号商品经过3次hash以后,分别定位到索引为1、3以及98号位置上。原始数据中1号因为刚才已经变成了1,现在它不变,而3号位和98号位原始数据从0变为1,这里又衍生出hash一个新的规则,如果在hash后原始位它是0的话,将其从0变为1,如果本身这一位就是1的话,则保持不变,此时二号商品也处理完了。

我们继续向后3、4、5、6、7、8直到编号达到了最后一个1000,当商品编号1000处理完后,他将索引位3、6、98设置为1。

作为布隆过滤器,它存储在redis服务器中该怎么去使用呢?这就涉及到我们日常开发中对于商品编号的比对了。

咱们先看一个已存在的,比如此时某一个用户要查询858号商品数据,都知道858是存在的,那么按照原始的三个分别定位到了1、5和98号位,当每一个hash位的数值都是1的时候,则代表对应的编号它是存在的。

下面我们再看一个不存在的情况,例如这里要查询8888,对于88这个数值经过3次hash以后定位到了3、6和100这三个位置,此时索引为100的数值是0,在多次希时有任何一个为0则代表这个数据是不存在的。

简单总结一下,如果布隆过滤器所有hash位的值都是1的话,则代表这个数据可能存在,注意我的表达它是可能存在,但是如果某一位的数值是0的话,它是一定不存在的。

在布隆过滤器设计之初,它就不是一个精确的判断,因为布隆过滤器存在误判的情况,

来看一下当前的演示,比如现在我要查询8889的情况,经过三次hash正好每一位上都是1,尽管在数据库中8889这个商品是不存在的,但是在布隆过滤器中它会被判定为存在,这是在布隆过滤器中会出现的小概率的误判情况。

如何减少误判的产生,其实方法有两个,

第一个是增加二进制位数,在原始情况下我们设置索引位到达了100,但是如果我们把它放大1万倍,到达了100万,是不是hash以后的数据会变得更加的分散,出现重复的情况就会更小,这是第一种方式。

而第二种方式是增加hash的次数,其实每一次希处理都是在增加数据的特征越多,出现误判的概率就越小。现在我们是做了3次hash,如果你做10次,是不是它出现的概率就会小非常多,但是在这个过程中代价便是CPU需要进行更多的运算,会让布隆过滤器的性能有所降低。

讲到这里想必你对布隆过滤器应该有所了解了,但是在我们开发过程中,我们如何去使用布隆过滤器?来咱们看一下。其实作为Java积累了这么多年,像布隆过滤器这种经典的算法早就为我们进行了封装和集成,在Java中提供了一个redisson的组件,它内置了布隆过滤器可以让程序员非常简单直接的去设置布隆过滤器.

下面我们来介绍redisson的使用办法:

第一步:引入依赖

<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson-all</artifactId>
	<version>3.16.0</version>
</dependency>

在Maven中增加redisson-all的依赖可以直接引入相关Jar文件,为后续开发做好准备。

第二步:运用布隆过滤器

我们先来看一下完整代码

Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
//构造Redisson
RedissonClient redisson = Redisson.create(config);
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("bloom");
//初始化布隆过滤器:预计元素为1000000L,误判率为1%
bloomFilter.tryInit(1000000L,0.01);
bloomFilter.add("1"); //增加数据
//判断指定编号是否在布隆过滤器中
System.out.println(bloomFilter.contains("1")); //输出true
System.out.println(bloomFilter.contains("8888"));//输出false

作为当前的redis,

config.useSingleServer().setAddress("redis://127.0.0.1:6379");

是用来设置red服务器的服务地址以及端口号。

RBloomFilter bloomFilter = redisson.getBloomFilter("bloom");

紧接着我们实例化一个布隆过滤器对象,后面的参数说明redis使用哪个key来保存布隆过滤器数据。

bloomFilter.tryInit(1000000L,0.01);

这句话非常关键,作为当前的布隆过滤器,这里需要调用tryInit的方法。它有两个参数,第一个参数是代表初始化的布隆过滤器长度,长度越大,出现误判的可能性就越低。而第二个参数0.01则代表误判率最大允许为1%,在我们以前的项目中通常也是设置为1%,如果把这个数值设置的太小,虽然会降低误判率,但是会产生更多次的希操作,会降低系统的性能,因此1%也是我所建议的数值。

bloomFilter.add("1");

布隆布隆过滤器初始化以后,我们便可以通过add方法往里边去添加数据,所谓添加数据就是将数据进行多次hash将对应位从0变为1的过程。

System.out.println(bloomFilter.contains("1"));

例如现在我们把编号1增加进去之后,可以通过布隆过滤器的contains方法来判断当前这个数据是否存在。我们输入1,它输出true,而输入了不存在的8则输出false。请注意这两个结果它的含义是不同的,如果输出false则代表这个数据它是肯定不存在的,但是如果输出true的时候,它有1%的概率可能不存在,因为布隆过滤器它存在误判的情况。

以上便是布隆过滤器在Java中的应用,但是布隆过滤器如果要运用在项目中又该变成什么样子?它的处理流程是什么?

咱们看一下布隆过滤器在项目中的使用流程,其实可以归结成两个阶段,第一个阶段是在应用启动时我们去初始化布隆过滤器,例如将1000个、1万个、10万个商品进行初始化,完成从hash从0到1的转化工作。

第二个阶段便是当用户发来请求时会附加商品编号,如果编号存在的话,则直接去读取存储在redis缓存中的数据。如果此时redis缓存没有存在对应的商品数据,则直接去读取数据库,并将读取到的信息重新载入到redis缓存中。这样下一次用户在查询相同编号数据时,就可以直接读取缓存了。另外一种情况是,如果布隆过滤器判断它没有包含指定编号,则直接返回数据不存在的消息提示,这样便可以在redis层面将请求进行拦截。

这时你可能会有疑惑,既然布隆过滤器存在误判率,那出现了误判该怎么办呢?其实在大多数情况下,即使出现误判,也不会对系统产生额外的影响。因为像刚才我们设置1%的误判率,1万次请求才可能会出现100次误判的情况,我们已经将99%的无效请求进行了拦截,至于这些漏网之鱼也不会对我们系统产生任何实质的影响。

在这一节最后还有一个延伸的小问题,假如我们布隆过滤器在初始化以后,对应的商品被删除了,该怎么办?

这是一个布隆过滤器的小难点。因为布隆过滤器某一位的二进制数据可能被多个编号的hash来进行引用,比如说布隆过滤器中2号位是1,但是它可能还被3、5、100、1000这4个商品编号同时引用,这里是不允许直接对布隆过滤器某一位进行删除的,否则数据就乱了,怎么办呢?

这里业内有两种常见的解决方案:

第一种是定时异步重建布隆过滤器,比如说我们每过4个小时在额外的一台服务器上,异步的去执行1个任务调度,来重新生成布隆过滤器,替换掉已有的布隆过滤器,这是第一种做法。

而另外一种做法叫做计数布隆过滤器,在标准的布隆过滤器下,是无法得知当前某一位它是被哪些具体数据进行了引用,但是计数布隆过滤器它是在这一位上额外的附加的技术信息,表达出该位被几个数据进行了引用。如果你对计数布隆过滤器有兴趣的话,可以翻阅一下相关的资料。

好,本节课我们从项目案例切入一点一点的为你解释了什么是布隆过滤器,以及布隆过滤器在项目中的应用场景以及实战的用法,希望能对你有帮助。