SpringBoot+Redis布隆过滤器防恶意流量击穿缓存的方法

SpringBoot+Redis布隆过滤器防恶意流量击穿缓存的方法

本篇内容介绍了“SpringBoot+Redis布隆过滤器防恶意流量击穿缓存的方法”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

具体如下:

什么是恶意流量穿透

假设我们的Redis里存有一组用户的注册email,以email作为Key存在,同时它对应着DB里的User表的部分字段。

一般来说,一个合理的请求过来我们会先在Redis里判断这个用户是否是会员,因为从缓存里读数据返回快。如果这个会员在缓存中不存在那么我们会去DB中查询一下。

现在试想,有千万个不同IP的请求(不要以为没有,我们就在2018年和2019年碰到了,因为攻击的成本很低)带着Redis里根本不存在的key来访问你的网站,这时我们来设想一下:

  • 请求到达Web服务器;

  • 请求派发到应用层->微服务层;

  • 请求去Redis捞数据,Redis内不存在这个Key;

  • 于是请求到达DB层,在DB建立connection后进行一次查询

千万乃至上亿的DB连接请求,先不说Redis是否撑的住DB也会被瞬间打爆。这就是“Redis穿透”又被称为“缓存击穿”,它会打爆你的缓存或者是连DB一起打爆进而引起一系列的“雪崩效应”。

怎么防

那就是使用布隆过滤器,可以把所有的user表里的关键查询字段放于Redis的bloom过滤器内。有人会说,这不疯了,我有4000万会员?so what!

你把4000会员放在Redis里是比较夸张,有些网站有8000万、1亿会员呢?因此我没让你直接放在Redis里,而是放在布隆过滤器内!

布隆过滤器内不是直接把key,value这样放进去的,它存放的内容是这么一个样的:

BloomFilter是一种空间效率的概率型数据结构,由Burton Howard Bloom 1970年提出的。通常用来判断一个元素是否在集合中。具有极高的空间效率,但是会带来假阳性(False positive)的错误。

False positive&&False negatives
由于BloomFiter牺牲了一定的准确率换取空间效率。所以带来了False positive的问题。

False positive
BloomFilter在判断一个元素在集合中的时候,会出现一定的错误率,这个错误率称为False positive的。通常缩写为fpp。

False negatives
BloomFilter判断一个元素不在集合中的时候的错误率。 BloomFilter判断该元素不在集合中,则该元素一定不再集合中。故False negatives概率为0。

BloomFilter使用长度为m bit的字节数组,使用k个hash函数,增加一个元素: 通过k次hash将元素映射到字节数组中k个位置中,并设置对应位置的字节为1。
查询元素是否存在: 将元素k次hash得到k个位置,如果对应k个位置的bit是1则认为存在,反之则认为不存在。

由于它里面存的都是bit,因此这个数据量会很小很小,小到什么样的程度呢?在写本博客时我插了100万条email信息进入Redis的bloom filter也只占用了3Mb不到。

Bloom Filter会有几比较关键的值,根据这个值你是大致可以算出放多少条数据然后它的误伤率在多少时会占用多少系统资源的。这个算法有一个网址:https://krisives.github.io/bloom-calculator/,我们放入100万条数据,假设误伤率在0.001%,看,它自动得出Redis需要申请的系统内存资源是多少?

那么怎么解决这个误伤率呢?很简单的,当有误伤时业务或者是运营会来报误伤率,这时你只要添加一个小白名单就是了,相对于100万条数据来说,1000个白名单不是问题。并且bloom filter的返回速度超块,80-100毫秒内即返回调用端该Key存在或者是不存了。

布隆过滤器的另一个用武场景

假设我用python爬虫爬了4亿条url,需要去重?

看,布隆过滤器就是用于这个场景的。

下面就开始我们的Redis BloomFilter之旅。

给Redis安装Bloom Filter

Redis从4.0才开始支持bloom filter,因此本例中我们使用的是Redis5.4。

Redis的bloom filter下载地址在这:https://github.com/RedisLabsModules/redisbloom.git

gitclonehttps://github.com/RedisLabsModules/redisbloom.gitcdredisbloommake#编译

让Redis启动时可以加载bloom filter有两种方式:

手工加载式:

redis-server--loadmodule./redisbloom/rebloom.so

每次启动自加载:

编辑Redis的redis.conf文件,加入:

loadmodule/soft/redisbloom/redisbloom.so

Like this:

在Redis里使用Bloom Filter

基本指令:

bf.reserve {key} {error_rate} {size}

127.0.0.1:6379>bf.reserveuserid0.01100000OK

上面这条命令就是:创建一个空的布隆过滤器,并设置一个期望的错误率和初始大小。{error_rate}过滤器的错误率在0-1之间,如果要设置0.1%,则应该是0.001。该数值越接近0,内存消耗越大,对cpu利用率越高

bf.add {key} {item}

127.0.0.1:6379>bf.adduserid'181920'(integer)1

上面这条命令就是:往过滤器中添加元素。如果key不存在,过滤器会自动创建。

bf.exists {key} {item}

127.0.0.1:6379>bf.existsuserid'101310299'(integer)1

上面这条命令就是:判断指定key的value是否在bloomfilter里存在。存在:返回1,不存在:返回0。

结合SpringBoot使用

网上很多写的都是要么是直接使用jedis来操作的,或者是java里execute一个外部进程来调用Redis的bloom filter指令的。很多都是调不通或者helloworld一个级别的,是根本无法上生产级别应用的。

笔者给出的代码保障读者完全可用!

笔者不是数学家,因此就借用了google的guava包来实现了核心算法,核心代码如下:

BloomFilterHelper.java

packageorg.sky.platform.util;importcom.google.common.base.Preconditions;importcom.google.common.hash.Funnel;importcom.google.common.hash.Hashing;publicclassBloomFilterHelper<T>{privateintnumHashFunctions;privateintbitSize;privateFunnel<T>funnel;publicBloomFilterHelper(Funnel<T>funnel,intexpectedInsertions,doublefpp){Preconditions.checkArgument(funnel!=null,"funnel不能为空");this.funnel=funnel;bitSize=optimalNumOfBits(expectedInsertions,fpp);numHashFunctions=optimalNumOfHashFunctions(expectedInsertions,bitSize);}int[]murmurHashOffset(Tvalue){int[]offset=newint[numHashFunctions];longhash74=Hashing.murmur3_128().hashObject(value,funnel).asLong();inthash2=(int)hash74;inthash3=(int)(hash74>>>32);for(inti=1;i<=numHashFunctions;i++){intnextHash=hash2+i*hash3;if(nextHash<0){nextHash=~nextHash;}offset[i-1]=nextHash%bitSize;}returnoffset;}/***计算bit数组的长度*/privateintoptimalNumOfBits(longn,doublep){if(p==0){p=Double.MIN_VALUE;}return(int)(-n*Math.log(p)/(Math.log(2)*Math.log(2)));}/***计算hash方法执行次数*/privateintoptimalNumOfHashFunctions(longn,longm){returnMath.max(1,(int)Math.round((double)m/n*Math.log(2)));}}

下面放出全工程解说,我已经将源码上传到了我的git上了,确保读者可用,源码地址在这:https://github.com/mkyuangithub/mkyuangithub.git

搭建spring boot工程

项目Redis配置

我们在redis-practice工程里建立一个application.properties文件,内容如下:

spring.redis.database=0spring.redis.host=192.168.56.101spring.redis.port=6379spring.redis.password=111111spring.redis.pool.max-active=10spring.redis.pool.max-wait=-1spring.redis.pool.max-idle=10spring.redis.pool.min-idle=0spring.redis.timeout=1000

以上这个是demo环境的配置。

我们此处依旧使用的是在前一篇springboot+nacos+dubbo实现异常统一管理中的xxx-project->sky-common->nacos-parent的依赖结构。

在redis-practice工程的org.sky.config包中放入redis的springboot配置

RedisConfig.java

packageorg.sky.config;importcom.fasterxml.jackson.annotation.JsonAutoDetect;importcom.fasterxml.jackson.annotation.PropertyAccessor;importcom.fasterxml.jackson.databind.ObjectMapper;importorg.springframework.cache.CacheManager;importorg.springframework.cache.annotation.CachingConfigurerSupport;importorg.springframework.cache.annotation.EnableCaching;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.cache.RedisCacheManager;importorg.springframework.data.redis.connection.RedisConnectionFactory;importorg.springframework.data.redis.core.*;importorg.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;importorg.springframework.data.redis.serializer.StringRedisSerializer;@Configuration@EnableCachingpublicclassRedisConfigextendsCachingConfigurerSupport{/***选择redis作为默认缓存工具**@paramredisTemplate*@return*/@BeanpublicCacheManagercacheManager(RedisTemplateredisTemplate){RedisCacheManagerrcm=newRedisCacheManager(redisTemplate);returnrcm;}/***retemplate相关配置**@paramfactory*@return*/@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactoryfactory){RedisTemplate<String,Object>template=newRedisTemplate<>();//配置连接工厂template.setConnectionFactory(factory);//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)Jackson2JsonRedisSerializerjacksonSeial=newJackson2JsonRedisSerializer(Object.class);ObjectMapperom=newObjectMapper();//指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和publicom.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);//指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jacksonSeial.setObjectMapper(om);//值采用json序列化template.setValueSerializer(jacksonSeial);//使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(newStringRedisSerializer());//设置hashkey和value序列化模式template.setHashKeySerializer(newStringRedisSerializer());template.setHashValueSerializer(jacksonSeial);template.afterPropertiesSet();returntemplate;}/***对hash类型的数据操作**@paramredisTemplate*@return*/@BeanpublicHashOperations<String,String,Object>hashOperations(RedisTemplate<String,Object>redisTemplate){returnredisTemplate.opsForHash();}/***对redis字符串类型数据操作**@paramredisTemplate*@return*/@BeanpublicValueOperations<String,Object>valueOperations(RedisTemplate<String,Object>redisTemplate){returnredisTemplate.opsForValue();}/***对链表类型的数据操作**@paramredisTemplate*@return*/@BeanpublicListOperations<String,Object>listOperations(RedisTemplate<String,Object>redisTemplate){returnredisTemplate.opsForList();}/***对无序集合类型的数据操作**@paramredisTemplate*@return*/@BeanpublicSetOperations<String,Object>setOperations(RedisTemplate<String,Object>redisTemplate){returnredisTemplate.opsForSet();}/***对有序集合类型的数据操作**@paramredisTemplate*@return*/@BeanpublicZSetOperations<String,Object>zSetOperations(RedisTemplate<String,Object>redisTemplate){returnredisTemplate.opsForZSet();}}

这个配置除实现了springboot自动发现redis在application.properties中的配置外我们还添加了不少redis基本的数据结构的操作的封装。

我们为此还要再封装一套Redis Util小组件,它们位于sky-common工程中

RedisUtil.java

packageorg.sky.platform.util;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.stereotype.Component;importjava.util.Collection;importjava.util.Date;importjava.util.Set;importjava.util.concurrent.TimeUnit;importjava.util.stream.Collectors;importjava.util.stream.Stream;importcom.google.common.base.Preconditions;importorg.springframework.data.redis.core.RedisTemplate;@ComponentpublicclassRedisUtil{@AutowiredprivateRedisTemplate<String,String>redisTemplate;/***默认过期时长,单位:秒*/publicstaticfinallongDEFAULT_EXPIRE=60*60*24;/***不设置过期时长*/publicstaticfinallongNOT_EXPIRE=-1;publicbooleanexistsKey(Stringkey){returnredisTemplate.hasKey(key);}/***重名名key,如果newKey已经存在,则newKey的原值被覆盖**@paramoldKey*@paramnewKey*/publicvoidrenameKey(StringoldKey,StringnewKey){redisTemplate.rename(oldKey,newKey);}/***newKey不存在时才重命名**@paramoldKey*@paramnewKey*@return修改成功返回true*/publicbooleanrenameKeyNotExist(StringoldKey,StringnewKey){returnredisTemplate.renameIfAbsent(oldKey,newKey);}/***删除key**@paramkey*/publicvoiddeleteKey(Stringkey){redisTemplate.delete(key);}/***删除多个key**@paramkeys*/publicvoiddeleteKey(String...keys){Set<String>kSet=Stream.of(keys).map(k->k).collect(Collectors.toSet());redisTemplate.delete(kSet);}/***删除Key的集合**@paramkeys*/publicvoiddeleteKey(Collection<String>keys){Set<String>kSet=keys.stream().map(k->k).collect(Collectors.toSet());redisTemplate.delete(kSet);}/***设置key的生命周期**@paramkey*@paramtime*@paramtimeUnit*/publicvoidexpireKey(Stringkey,longtime,TimeUnittimeUnit){redisTemplate.expire(key,time,timeUnit);}/***指定key在指定的日期过期**@paramkey*@paramdate*/publicvoidexpireKeyAt(Stringkey,Datedate){redisTemplate.expireAt(key,date);}/***查询key的生命周期**@paramkey*@paramtimeUnit*@return*/publiclonggetKeyExpire(Stringkey,TimeUnittimeUnit){returnredisTemplate.getExpire(key,timeUnit);}/***将key设置为永久有效**@paramkey*/publicvoidpersistKey(Stringkey){redisTemplate.persist(key);}/***根据给定的布隆过滤器添加值*/public<T>voidaddByBloomFilter(BloomFilterHelper<T>bloomFilterHelper,Stringkey,Tvalue){Preconditions.checkArgument(bloomFilterHelper!=null,"bloomFilterHelper不能为空");int[]offset=bloomFilterHelper.murmurHashOffset(value);for(inti:offset){redisTemplate.opsForValue().setBit(key,i,true);}}/***根据给定的布隆过滤器判断值是否存在*/public<T>booleanincludeByBloomFilter(BloomFilterHelper<T>bloomFilterHelper,Stringkey,Tvalue){Preconditions.checkArgument(bloomFilterHelper!=null,"bloomFilterHelper不能为空");int[]offset=bloomFilterHelper.murmurHashOffset(value);for(inti:offset){if(!redisTemplate.opsForValue().getBit(key,i)){returnfalse;}}returntrue;}}

RedisKeyUtil.java

packageorg.sky.platform.util;publicclassRedisKeyUtil{/***redis的key形式为:表名:主键名:主键值:列名**@paramtableName表名*@parammajorKey主键名*@parammajorKeyValue主键值*@paramcolumn列名*@return*/publicstaticStringgetKeyWithColumn(StringtableName,StringmajorKey,StringmajorKeyValue,Stringcolumn){StringBufferbuffer=newStringBuffer();buffer.append(tableName).append(":");buffer.append(majorKey).append(":");buffer.append(majorKeyValue).append(":");buffer.append(column);returnbuffer.toString();}/***redis的key形式为:表名:主键名:主键值**@paramtableName表名*@parammajorKey主键名*@parammajorKeyValue主键值*@return*/publicstaticStringgetKey(StringtableName,StringmajorKey,StringmajorKeyValue){StringBufferbuffer=newStringBuffer();buffer.append(tableName).append(":");buffer.append(majorKey).append(":");buffer.append(majorKeyValue).append(":");returnbuffer.toString();}}

然后就是制作 redis里如何使用BloomFilter的BloomFilterHelper.java了,它也位于sky-common文件夹,源码如上已经贴了,因此此处就不再作重复。

最后我们在sky-common里放置一个UserVO用于演示

UserVO.java

packageorg.sky.vo;importjava.io.Serializable;publicclassUserVOimplementsSerializable{privateStringname;privateStringaddress;privateIntegerage;privateStringemail="";publicStringgetEmail(){returnemail;}publicvoidsetEmail(Stringemail){this.email=email;}publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}publicStringgetAddress(){returnaddress;}publicvoidsetAddress(Stringaddress){this.address=address;}publicIntegergetAge(){returnage;}publicvoidsetAge(Integerage){this.age=age;}}

下面给出我们所有gitrepo里依赖的nacos-parent的pom.xml文件内容,此次我们增加了对于“spring-boot-starter-data-redis”,它跟着我们的全局springboot版本走:

parent工程的pom.xml

<projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.sky.demo</groupId><artifactId>nacos-parent</artifactId><version>0.0.1-SNAPSHOT</version><packaging>pom</packaging><description>DemoprojectforSpringBootDubboNacos</description><modules></modules><properties><java.version>1.8</java.version><spring-boot.version>1.5.15.RELEASE</spring-boot.version><dubbo.version>2.7.3</dubbo.version><curator-framework.version>4.0.1</curator-framework.version><curator-recipes.version>2.8.0</curator-recipes.version><druid.version>1.1.20</druid.version><guava.version>27.0.1-jre</guava.version><fastjson.version>1.2.59</fastjson.version><dubbo-registry-nacos.version>2.7.3</dubbo-registry-nacos.version><nacos-client.version>1.1.4</nacos-client.version><mysql-connector-java.version>5.1.46</mysql-connector-java.version><disruptor.version>3.4.2</disruptor.version><aspectj.version>1.8.13</aspectj.version><nacos-service.version>0.0.1-SNAPSHOT</nacos-service.version><spring.data.redis>1.8.14-RELEASE</spring.data.redis><skycommon.version>0.0.1-SNAPSHOT</skycommon.version><maven.compiler.source>${java.version}</maven.compiler.source><maven.compiler.target>${java.version}</maven.compiler.target><compiler.plugin.version>3.8.1</compiler.plugin.version><war.plugin.version>3.2.3</war.plugin.version><jar.plugin.version>3.1.2</jar.plugin.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>${spring-boot.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo-spring-boot-starter</artifactId><version>${dubbo.version}</version><exclusions><exclusion><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo</artifactId><version>${dubbo.version}</version></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-framework</artifactId><version>${curator-framework.version}</version></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>${curator-recipes.version}</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql-connector-java.version}</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>${druid.version}</version></dependency><dependency><groupId>com.lmax</groupId><artifactId>disruptor</artifactId><version>${disruptor.version}</version></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>${guava.version}</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>${fastjson.version}</version></dependency><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo-registry-nacos</artifactId><version>${dubbo-registry-nacos.version}</version></dependency><dependency><groupId>com.alibaba.nacos</groupId><artifactId>nacos-client</artifactId><version>${nacos-client.version}</version></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>${aspectj.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>${spring-boot.version}</version></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>${compiler.plugin.version}</version><configuration><source>${java.version}</source><target>${java.version}</target></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-war-plugin</artifactId><version>${war.plugin.version}</version></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>${jar.plugin.version}</version></plugin></plugins></build></project>

sky-common中pom.xml文件

<projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.sky.demo</groupId><artifactId>skycommon</artifactId><version>0.0.1-SNAPSHOT</version><parent><groupId>org.sky.demo</groupId><artifactId>nacos-parent</artifactId><version>0.0.1-SNAPSHOT</version></parent><dependencies><dependency><groupId>org.apache.curator</groupId><artifactId>curator-framework</artifactId></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.spockframework</groupId><artifactId>spock-core</artifactId><scope>test</scope></dependency><dependency><groupId>org.spockframework</groupId><artifactId>spock-spring</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency><dependency><groupId>com.lmax</groupId><artifactId>disruptor</artifactId></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency></dependencies></project>

到此,我们的springboot+redis基本框架、util类、bloomfilter组件搭建完毕,接下来我们重点说我们的demo工程

Demo工程:redis-practice说明

pom.xml文件,它依赖于nacos-parent同时还引用了sky-common

<projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.sky.demo</groupId><artifactId>redis-practice</artifactId><version>0.0.1-SNAPSHOT</version><description>DemoRedisAdvancedFeatures</description><parent><groupId>org.sky.demo</groupId><artifactId>nacos-parent</artifactId><version>0.0.1-SNAPSHOT</version></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo</artifactId></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-framework</artifactId></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.spockframework</groupId><artifactId>spock-core</artifactId><scope>test</scope></dependency><dependency><groupId>org.spockframework</groupId><artifactId>spock-spring</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency><dependency><groupId>com.lmax</groupId><artifactId>disruptor</artifactId></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId></dependency><dependency><groupId>org.sky.demo</groupId><artifactId>skycommon</artifactId><version>${skycommon.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency></dependencies><build><sourceDirectory>src/main/java</sourceDirectory><testSourceDirectory>src/test/java</testSourceDirectory><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins><resources><resource><directory>src/main/resources</directory></resource><resource><directory>src/main/webapp</directory><targetPath>META-INF/resources</targetPath><includes><include>**/**</include></includes></resource><resource><directory>src/main/resources</directory><filtering>true</filtering><includes><include>application.properties</include><include>application-${profileActive}.properties</include></includes></resource></resources></build></project>

用于启动的Application.java

packageorg.sky;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.EnableAutoConfiguration;importorg.springframework.context.annotation.ComponentScan;importorg.springframework.transaction.annotation.EnableTransactionManagement;@EnableTransactionManagement@ComponentScan(basePackages={"org.sky"})@EnableAutoConfigurationpublicclassApplication{publicstaticvoidmain(String[]args){SpringApplication.run(Application.class,args);}}

然后我们制作了一个controller名为UserController,该controller里有两个方法:

  • public ResponseEntity<String> addUser(@RequestBody String params),该方法用于接受来自外部的api post然后把一条email地址塞入redis的bloomfilter中;

  • public ResponseEntity<String> findEmailInBloom(@RequestBody String params),该方法用于接受来自外部的api post然后去redis的bloomfilter中验证是否外部输入的user信息中的email地址在上百万的email记录中存在;

以此来完成验证塞入redis的bloom filter中上百万条记录占用了多少内存以及使用bloom filter查询一条记录有多快。

UserController.java

packageorg.sky.controller;importjava.util.HashMap;importjava.util.Map;importjava.util.concurrent.TimeUnit;importjavax.annotation.Resource;importorg.sky.platform.util.BloomFilterHelper;importorg.sky.platform.util.RedisUtil;importorg.sky.vo.UserVO;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.core.ValueOperations;importorg.springframework.http.HttpHeaders;importorg.springframework.http.HttpStatus;importorg.springframework.http.MediaType;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestBody;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importcom.alibaba.fastjson.JSON;importcom.alibaba.fastjson.JSONObject;importcom.google.common.base.Charsets;importcom.google.common.hash.Funnel;@RestController@RequestMapping("user")publicclassUserControllerextendsBaseController{@ResourceprivateRedisTemplateredisTemplate;@ResourceprivateRedisUtilredisUtil;@PostMapping(value="/addEmailToBloom",produces="application/json")publicResponseEntity<String>addUser(@RequestBodyStringparams){ResponseEntity<String>response=null;StringreturnResultStr;HttpHeadersheaders=newHttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON_UTF8);Map<String,Object>result=newHashMap<>();try{JSONObjectrequestJsonObj=JSON.parseObject(params);UserVOinputUser=getUserFromJson(requestJsonObj);BloomFilterHelper<String>myBloomFilterHelper=newBloomFilterHelper<>((Funnel<String>)(from,into)->into.putString(from,Charsets.UTF_8).putString(from,Charsets.UTF_8),1500000,0.00001);redisUtil.addByBloomFilter(myBloomFilterHelper,"email_existed_bloom",inputUser.getEmail());result.put("code",HttpStatus.OK.value());result.put("message","addintobloomFiltersuccessfully");result.put("email",inputUser.getEmail());returnResultStr=JSON.toJSONString(result);logger.info("returnResultStr======>"+returnResultStr);response=newResponseEntity<>(returnResultStr,headers,HttpStatus.OK);}catch(Exceptione){logger.error("addanewproductwitherror:"+e.getMessage(),e);result.put("message","addanewproductwitherror:"+e.getMessage());returnResultStr=JSON.toJSONString(result);response=newResponseEntity<>(returnResultStr,headers,HttpStatus.INTERNAL_SERVER_ERROR);}returnresponse;}@PostMapping(value="/checkEmailInBloom",produces="application/json")publicResponseEntity<String>findEmailInBloom(@RequestBodyStringparams){ResponseEntity<String>response=null;StringreturnResultStr;HttpHeadersheaders=newHttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON_UTF8);Map<String,Object>result=newHashMap<>();try{JSONObjectrequestJsonObj=JSON.parseObject(params);UserVOinputUser=getUserFromJson(requestJsonObj);BloomFilterHelper<String>myBloomFilterHelper=newBloomFilterHelper<>((Funnel<String>)(from,into)->into.putString(from,Charsets.UTF_8).putString(from,Charsets.UTF_8),1500000,0.00001);booleananswer=redisUtil.includeByBloomFilter(myBloomFilterHelper,"email_existed_bloom",inputUser.getEmail());logger.info("answer====="+answer);result.put("code",HttpStatus.OK.value());result.put("email",inputUser.getEmail());result.put("exist",answer);returnResultStr=JSON.toJSONString(result);logger.info("returnResultStr======>"+returnResultStr);response=newResponseEntity<>(returnResultStr,headers,HttpStatus.OK);}catch(Exceptione){logger.error("addanewproductwitherror:"+e.getMessage(),e);result.put("message","addanewproductwitherror:"+e.getMessage());returnResultStr=JSON.toJSONString(result);response=newResponseEntity<>(returnResultStr,headers,HttpStatus.INTERNAL_SERVER_ERROR);}returnresponse;}privateUserVOgetUserFromJson(JSONObjectrequestObj){StringuserName=requestObj.getString("username");StringuserAddress=requestObj.getString("address");StringuserEmail=requestObj.getString("email");intuserAge=requestObj.getInteger("age");UserVOu=newUserVO();u.setName(userName);u.setAge(userAge);u.setEmail(userEmail);u.setAddress(userAddress);returnu;}}

注意UserController中的BloomFilterHelper的用法,我在Redis的bloomfilter里申明了可以用于存放150万数据的空间。如果存和的数据大于了你预先申请的空间怎么办?那么它会增加“误伤率”

下面我们把这个项目运行起来看看效果吧。

运行redis-practice工程

运行起来后

我们可以使用postman先来做个小实验

我们使用"、addEmailToBloom"往redis bloom filter里插入了一个“yumi@yahoo.com”的email。

接下来我们会使用“/checkEmailInBloom”来验证这个email地址是否存在

我们使用redisclient连接上我们的redis查看,这个值确实也是插入进了bloom filter了。

使用压测工具喂120万条数据进入Redis Bloomfilter看实际效果

接下来,我们用jmeter对着“/addEmailToBloom”喂上个120万左右数据进去,然后我们再来看bloom filter在120万email按照布隆算 法喂进去后我们的系统是如何表现的。

我这边使用的是apache-jmeter5.0,为了偷懒,我用了apache-jmeter里的_RandomString函数来动态创造16位字符长度的email。这边用户名、地址信息都是恒定,就是email是每次不一样,都是一串16位的随机字符+“@163.com”。

jmeter中BeanShell产生16位字符随机组成email的函数

useremail="${__RandomString(16,abcdefghijklmnop,myemail)}"+"@163.com";vars.put("random_email",useremail);

jmeter测试计划设置成了75个线程,连续运行30分钟(实践上笔者运行了3个30分钟,因为是demo环境,30分钟每次插大概40万条数据进去吧)

jmeter post请求

然后我们使用jmeter命令行来运行这个测试计划:

jmeter-n-tadd_randomemail_to_bloom.jmx-ladd_email_to_bloom\report\03-result.csv-jadd_email_to_bloom\logs\03-log.log-e-oadd_email_to_bloom\html_report_3

它代表:

  • -t 指定jmeter执行计划文件所在路径;

  • -l 生成report的目录,这个目录如果不存在则创建 ,必须是一个空目录;

  • -j 生成log的目录,这个目录如果不存在则创建 ,必须是一个空目录;

  • -e 生成html报告,它配合着-o参数一起使用;

  • -o 生成html报告所在的路径,这个目录如果不存在则创建 ,必须是一个空目录;

回车后它就开始运行了

一直执行到这个过程全部结束,跳出command命令符为止。

我们查看我们用-e -o生成的jmeter html报告,前面说过了,我一共运行了3次,第一次是10分钟70059条数据 ,第二次是30分钟40多万条数据 ,第三次是45他钟70多万条数据。我共计插入了1,200,790条email。

而这120万数据总计在redis中占用内存不超过8mb,见下面demo环境的zabbix录制的记录

120万条数据插进去后,我们接着从我们的log4j的输出中随便找一条logger.info住的email如:egpoghnfjekjajdo@163.com来看一下,redis bloomfilter找到这条记录的表现如何,76ms,我运行了多次,平均在80ms左右:

通过上面这么一个实例,大家可以看到把email以hash后并以bit的形式存入bloomfilter后,它占用的内存是多么的小,而查询效率又是多么的高。

往往在生产上,我们经常会把上千万或者是上亿的记录"load"进bloomfilter,然后拿它去做“防击穿”或者是去重的动作。

只要bloomfilter中不存在的key直接返回客户端false,配合着nginx的动态扩充、cdn、waf、接口层的缓存,整个网站抗6位数乃至7位数的并发其实是件非常简单的事。

“SpringBoot+Redis布隆过滤器防恶意流量击穿缓存的方法”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注恰卡编程网网站,小编将为大家输出更多高质量的实用文章!

发布于 2022-03-17 21:15:40
收藏
分享
海报
0 条评论
24
上一篇:Spring中Spring Boot与Spring MVC的核心概念是什么 下一篇:C++如何实现职工工资管理系统
目录

    0 条评论

    本站已关闭游客评论,请登录或者注册后再评论吧~

    忘记密码?

    图形验证码