SpringBoot+Redis布隆過濾器防惡意流量擊穿緩存
本文主要介紹了SpringBoot+Redis布隆過濾器防惡意流量擊穿緩存,具體如下:
什么是惡意流量穿透
假設(shè)我們的Redis里存有一組用戶的注冊email,以email作為Key存在,同時它對應(yīng)著DB里的User表的部分字段。
一般來說,一個合理的請求過來我們會先在Redis里判斷這個用戶是否是會員,因為從緩存里讀數(shù)據(jù)返回快。如果這個會員在緩存中不存在那么我們會去DB中查詢一下。
現(xiàn)在試想,有千萬個不同IP的請求(不要以為沒有,我們就在2018年和2019年碰到了,因為攻擊的成本很低)帶著Redis里根本不存在的key來訪問你的網(wǎng)站,這時我們來設(shè)想一下:
- 請求到達(dá)Web服務(wù)器;
- 請求派發(fā)到應(yīng)用層->微服務(wù)層;
- 請求去Redis撈數(shù)據(jù),Redis內(nèi)不存在這個Key;
- 于是請求到達(dá)DB層,在DB建立connection后進(jìn)行一次查詢
千萬乃至上億的DB連接請求,先不說Redis是否撐的住DB也會被瞬間打爆。這就是“Redis穿透”又被稱為“緩存擊穿”,它會打爆你的緩存或者是連DB一起打爆進(jìn)而引起一系列的“雪崩效應(yīng)”。
怎么防
那就是使用布隆過濾器,可以把所有的user表里的關(guān)鍵查詢字段放于Redis的bloom過濾器內(nèi)。有人會說,這不瘋了,我有4000萬會員?so what!
你把4000會員放在Redis里是比較夸張,有些網(wǎng)站有8000萬、1億會員呢?因此我沒讓你直接放在Redis里,而是放在布隆過濾器內(nèi)!
布隆過濾器內(nèi)不是直接把key,value這樣放進(jìn)去的,它存放的內(nèi)容是這么一個樣的:
BloomFilter是一種空間效率的概率型數(shù)據(jù)結(jié)構(gòu),由Burton Howard Bloom 1970年提出的。通常用來判斷一個元素是否在集合中。具有極高的空間效率,但是會帶來假陽性(False positive)的錯誤。
False positive&&False negatives
由于BloomFiter犧牲了一定的準(zhǔn)確率換取空間效率。所以帶來了False positive的問題。
False positive
BloomFilter在判斷一個元素在集合中的時候,會出現(xiàn)一定的錯誤率,這個錯誤率稱為False positive的。通常縮寫為fpp。
False negatives
BloomFilter判斷一個元素不在集合中的時候的錯誤率。 BloomFilter判斷該元素不在集合中,則該元素一定不再集合中。故False negatives概率為0。
BloomFilter使用長度為m bit的字節(jié)數(shù)組,使用k個hash函數(shù),增加一個元素: 通過k次hash將元素映射到字節(jié)數(shù)組中k個位置中,并設(shè)置對應(yīng)位置的字節(jié)為1。
查詢元素是否存在: 將元素k次hash得到k個位置,如果對應(yīng)k個位置的bit是1則認(rèn)為存在,反之則認(rèn)為不存在。
由于它里面存的都是bit,因此這個數(shù)據(jù)量會很小很小,小到什么樣的程度呢?在寫本博客時我插了100萬條email信息進(jìn)入Redis的bloom filter也只占用了3Mb不到。
Bloom Filter會有幾比較關(guān)鍵的值,根據(jù)這個值你是大致可以算出放多少條數(shù)據(jù)然后它的誤傷率在多少時會占用多少系統(tǒng)資源的。這個算法有一個網(wǎng)址:https://krisives.github.io/bloom-calculator/,我們放入100萬條數(shù)據(jù),假設(shè)誤傷率在0.001%,看,它自動得出Redis需要申請的系統(tǒng)內(nèi)存資源是多少?
那么怎么解決這個誤傷率呢?很簡單的,當(dāng)有誤傷時業(yè)務(wù)或者是運(yùn)營會來報誤傷率,這時你只要添加一個小白名單就是了,相對于100萬條數(shù)據(jù)來說,1000個白名單不是問題。并且bloom filter的返回速度超塊,80-100毫秒內(nèi)即返回調(diào)用端該Key存在或者是不存了。
布隆過濾器的另一個用武場景
假設(shè)我用python爬蟲爬了4億條url,需要去重?
看,布隆過濾器就是用于這個場景的。
下面就開始我們的Redis BloomFilter之旅。
給Redis安裝Bloom Filter
Redis從4.0才開始支持bloom filter,因此本例中我們使用的是Redis5.4。
Redis的bloom filter下載地址在這:https://github.com/RedisLabsModules/redisbloom.git
git clone https://github.com/RedisLabsModules/redisbloom.git cd redisbloom make # 編譯
讓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.reserve userid 0.01 100000 OK
上面這條命令就是:創(chuàng)建一個空的布隆過濾器,并設(shè)置一個期望的錯誤率和初始大小。{error_rate}過濾器的錯誤率在0-1之間,如果要設(shè)置0.1%,則應(yīng)該是0.001。該數(shù)值越接近0,內(nèi)存消耗越大,對cpu利用率越高。
bf.add {key} {item}
127.0.0.1:6379> bf.add userid '181920' (integer) 1
上面這條命令就是:往過濾器中添加元素。如果key不存在,過濾器會自動創(chuàng)建。
bf.exists {key} {item}
127.0.0.1:6379> bf.exists userid '101310299' (integer) 1
上面這條命令就是:判斷指定key的value是否在bloomfilter里存在。存在:返回1,不存在:返回0。
結(jié)合SpringBoot使用
網(wǎng)上很多寫的都是要么是直接使用jedis來操作的,或者是java里execute一個外部進(jìn)程來調(diào)用Redis的bloom filter指令的。很多都是調(diào)不通或者h(yuǎn)elloworld一個級別的,是根本無法上生產(chǎn)級別應(yīng)用的。
筆者給出的代碼保障讀者完全可用!
筆者不是數(shù)學(xué)家,因此就借用了google的guava包來實現(xiàn)了核心算法,核心代碼如下:
BloomFilterHelper.java
package org.sky.platform.util; import com.google.common.base.Preconditions; import com.google.common.hash.Funnel; import com.google.common.hash.Hashing; public class BloomFilterHelper<T> { private int numHashFunctions; private int bitSize; private Funnel<T> funnel; public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) { Preconditions.checkArgument(funnel != null, "funnel不能為空"); this.funnel = funnel; bitSize = optimalNumOfBits(expectedInsertions, fpp); numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize); } int[] murmurHashOffset(T value) { int[] offset = new int[numHashFunctions]; long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong(); int hash1 = (int) hash64; int hash2 = (int) (hash64 >>> 32); for (int i = 1; i <= numHashFunctions; i++) { int nextHash = hash1 + i * hash2; if (nextHash < 0) { nextHash = ~nextHash; } offset[i - 1] = nextHash % bitSize; } return offset; } /** * 計算bit數(shù)組的長度 */ private int optimalNumOfBits(long n, double p) { if (p == 0) { p = Double.MIN_VALUE; } return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2))); } /** * 計算hash方法執(zhí)行次數(shù) */ private int optimalNumOfHashFunctions(long n, long m) { return Math.max(1, (int) Math.round((double) m / n * Math.log(2))); } }
下面放出全工程解說,我已經(jīng)將源碼上傳到了我的git上了,確保讀者可用,源碼地址在這:https://github.com/mkyuangithub/mkyuangithub.git
搭建spring boot工程
項目Redis配置
我們在redis-practice工程里建立一個application.properties文件,內(nèi)容如下:
spring.redis.database=0 spring.redis.host=192.168.56.101 spring.redis.port=6379 spring.redis.password=111111 spring.redis.pool.max-active=10 spring.redis.pool.max-wait=-1 spring.redis.pool.max-idle=10 spring.redis.pool.min-idle=0 spring.redis.timeout=1000
以上這個是demo環(huán)境的配置。
我們此處依舊使用的是在前一篇springboot+nacos+dubbo實現(xiàn)異常統(tǒng)一管理中的xxx-project->sky-common->nacos-parent的依賴結(jié)構(gòu)。
在redis-practice工程的org.sky.config包中放入redis的springboot配置
RedisConfig.java
package org.sky.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.*; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { /** * 選擇redis作為默認(rèn)緩存工具 * * @param redisTemplate * @return */ @Bean public CacheManager cacheManager(RedisTemplate redisTemplate) { RedisCacheManager rcm = new RedisCacheManager(redisTemplate); return rcm; } /** * retemplate相關(guān)配置 * * @param factory * @return */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); // 配置連接工廠 template.setConnectionFactory(factory); // 使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值(默認(rèn)使用JDK的序列化方式) Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); // 指定要序列化的域,field,get和set,以及修飾符范圍,ANY是都有包括private和public om.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(new StringRedisSerializer()); // 設(shè)置hash key 和value序列化模式 template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(jacksonSeial); template.afterPropertiesSet(); return template; } /** * 對hash類型的數(shù)據(jù)操作 * * @param redisTemplate * @return */ @Bean public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) { return redisTemplate.opsForHash(); } /** * 對redis字符串類型數(shù)據(jù)操作 * * @param redisTemplate * @return */ @Bean public ValueOperations<String, Object> valueOperations(RedisTemplate<String, Object> redisTemplate) { return redisTemplate.opsForValue(); } /** * 對鏈表類型的數(shù)據(jù)操作 * * @param redisTemplate * @return */ @Bean public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) { return redisTemplate.opsForList(); } /** * 對無序集合類型的數(shù)據(jù)操作 * * @param redisTemplate * @return */ @Bean public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) { return redisTemplate.opsForSet(); } /** * 對有序集合類型的數(shù)據(jù)操作 * * @param redisTemplate * @return */ @Bean public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) { return redisTemplate.opsForZSet(); } }
這個配置除實現(xiàn)了springboot自動發(fā)現(xiàn)redis在application.properties中的配置外我們還添加了不少redis基本的數(shù)據(jù)結(jié)構(gòu)的操作的封裝。
我們?yōu)榇诉€要再封裝一套Redis Util小組件,它們位于sky-common工程中
RedisUtil.java
package org.sky.platform.util; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.Collection; import java.util.Date; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; import com.google.common.base.Preconditions; import org.springframework.data.redis.core.RedisTemplate; @Component public class RedisUtil { @Autowired private RedisTemplate<String, String> redisTemplate; /** * 默認(rèn)過期時長,單位:秒 */ public static final long DEFAULT_EXPIRE = 60 * 60 * 24; /** * 不設(shè)置過期時長 */ public static final long NOT_EXPIRE = -1; public boolean existsKey(String key) { return redisTemplate.hasKey(key); } /** * 重名名key,如果newKey已經(jīng)存在,則newKey的原值被覆蓋 * * @param oldKey * @param newKey */ public void renameKey(String oldKey, String newKey) { redisTemplate.rename(oldKey, newKey); } /** * newKey不存在時才重命名 * * @param oldKey * @param newKey * @return 修改成功返回true */ public boolean renameKeyNotExist(String oldKey, String newKey) { return redisTemplate.renameIfAbsent(oldKey, newKey); } /** * 刪除key * * @param key */ public void deleteKey(String key) { redisTemplate.delete(key); } /** * 刪除多個key * * @param keys */ public void deleteKey(String... keys) { Set<String> kSet = Stream.of(keys).map(k -> k).collect(Collectors.toSet()); redisTemplate.delete(kSet); } /** * 刪除Key的集合 * * @param keys */ public void deleteKey(Collection<String> keys) { Set<String> kSet = keys.stream().map(k -> k).collect(Collectors.toSet()); redisTemplate.delete(kSet); } /** * 設(shè)置key的生命周期 * * @param key * @param time * @param timeUnit */ public void expireKey(String key, long time, TimeUnit timeUnit) { redisTemplate.expire(key, time, timeUnit); } /** * 指定key在指定的日期過期 * * @param key * @param date */ public void expireKeyAt(String key, Date date) { redisTemplate.expireAt(key, date); } /** * 查詢key的生命周期 * * @param key * @param timeUnit * @return */ public long getKeyExpire(String key, TimeUnit timeUnit) { return redisTemplate.getExpire(key, timeUnit); } /** * 將key設(shè)置為永久有效 * * @param key */ public void persistKey(String key) { redisTemplate.persist(key); } /** * 根據(jù)給定的布隆過濾器添加值 */ public <T> void addByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) { Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能為空"); int[] offset = bloomFilterHelper.murmurHashOffset(value); for (int i : offset) { redisTemplate.opsForValue().setBit(key, i, true); } } /** * 根據(jù)給定的布隆過濾器判斷值是否存在 */ public <T> boolean includeByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) { Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能為空"); int[] offset = bloomFilterHelper.murmurHashOffset(value); for (int i : offset) { if (!redisTemplate.opsForValue().getBit(key, i)) { return false; } } return true; } }
RedisKeyUtil.java
package org.sky.platform.util; public class RedisKeyUtil { /** * redis的key 形式為: 表名:主鍵名:主鍵值:列名 * * @param tableName 表名 * @param majorKey 主鍵名 * @param majorKeyValue 主鍵值 * @param column 列名 * @return */ public static String getKeyWithColumn(String tableName, String majorKey, String majorKeyValue, String column) { StringBuffer buffer = new StringBuffer(); buffer.append(tableName).append(":"); buffer.append(majorKey).append(":"); buffer.append(majorKeyValue).append(":"); buffer.append(column); return buffer.toString(); } /** * redis的key 形式為: 表名:主鍵名:主鍵值 * * @param tableName 表名 * @param majorKey 主鍵名 * @param majorKeyValue 主鍵值 * @return */ public static String getKey(String tableName, String majorKey, String majorKeyValue) { StringBuffer buffer = new StringBuffer(); buffer.append(tableName).append(":"); buffer.append(majorKey).append(":"); buffer.append(majorKeyValue).append(":"); return buffer.toString(); } }
然后就是制作 redis里如何使用BloomFilter的BloomFilterHelper.java了,它也位于sky-common文件夾,源碼如上已經(jīng)貼了,因此此處就不再作重復(fù)。
最后我們在sky-common里放置一個UserVO用于演示
UserVO.java
package org.sky.vo; import java.io.Serializable; public class UserVO implements Serializable { private String name; private String address; private Integer age; private String email = ""; public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } }
下面給出我們所有g(shù)itrepo里依賴的nacos-parent的pom.xml文件內(nèi)容,此次我們增加了對于“spring-boot-starter-data-redis”,它跟著我們的全局springboot版本走:
parent工程的pom.xml
<project xmlns="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.0 https://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>Demo project for Spring Boot Dubbo Nacos</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文件
<project xmlns="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.0 https://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
<project xmlns="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.0 https://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>Demo Redis Advanced Features</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
package org.sky; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.transaction.annotation.EnableTransactionManagement; @EnableTransactionManagement @ComponentScan(basePackages = { "org.sky" }) @EnableAutoConfiguration public class Application { public static void main(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中上百萬條記錄占用了多少內(nèi)存以及使用bloom filter查詢一條記錄有多快。
UserController.java
package org.sky.controller; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.annotation.Resource; import org.sky.platform.util.BloomFilterHelper; import org.sky.platform.util.RedisUtil; import org.sky.vo.UserVO; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.google.common.base.Charsets; import com.google.common.hash.Funnel; @RestController @RequestMapping("user") public class UserController extends BaseController { @Resource private RedisTemplate redisTemplate; @Resource private RedisUtil redisUtil; @PostMapping(value = "/addEmailToBloom", produces = "application/json") public ResponseEntity<String> addUser(@RequestBody String params) { ResponseEntity<String> response = null; String returnResultStr; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON_UTF8); Map<String, Object> result = new HashMap<>(); try { JSONObject requestJsonObj = JSON.parseObject(params); UserVO inputUser = getUserFromJson(requestJsonObj); BloomFilterHelper<String> myBloomFilterHelper = new BloomFilterHelper<>((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", "add into bloomFilter successfully"); result.put("email", inputUser.getEmail()); returnResultStr = JSON.toJSONString(result); logger.info("returnResultStr======>" + returnResultStr); response = new ResponseEntity<>(returnResultStr, headers, HttpStatus.OK); } catch (Exception e) { logger.error("add a new product with error: " + e.getMessage(), e); result.put("message", "add a new product with error: " + e.getMessage()); returnResultStr = JSON.toJSONString(result); response = new ResponseEntity<>(returnResultStr, headers, HttpStatus.INTERNAL_SERVER_ERROR); } return response; } @PostMapping(value = "/checkEmailInBloom", produces = "application/json") public ResponseEntity<String> findEmailInBloom(@RequestBody String params) { ResponseEntity<String> response = null; String returnResultStr; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON_UTF8); Map<String, Object> result = new HashMap<>(); try { JSONObject requestJsonObj = JSON.parseObject(params); UserVO inputUser = getUserFromJson(requestJsonObj); BloomFilterHelper<String> myBloomFilterHelper = new BloomFilterHelper<>((Funnel<String>) (from, into) -> into.putString(from, Charsets.UTF_8).putString(from, Charsets.UTF_8), 1500000, 0.00001); boolean answer = 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 = new ResponseEntity<>(returnResultStr, headers, HttpStatus.OK); } catch (Exception e) { logger.error("add a new product with error: " + e.getMessage(), e); result.put("message", "add a new product with error: " + e.getMessage()); returnResultStr = JSON.toJSONString(result); response = new ResponseEntity<>(returnResultStr, headers, HttpStatus.INTERNAL_SERVER_ERROR); } return response; } private UserVO getUserFromJson(JSONObject requestObj) { String userName = requestObj.getString("username"); String userAddress = requestObj.getString("address"); String userEmail = requestObj.getString("email"); int userAge = requestObj.getInteger("age"); UserVO u = new UserVO(); u.setName(userName); u.setAge(userAge); u.setEmail(userEmail); u.setAddress(userAddress); return u; } }
注意UserController中的BloomFilterHelper的用法,我在Redis的bloomfilter里申明了可以用于存放150萬數(shù)據(jù)的空間。如果存和的數(shù)據(jù)大于了你預(yù)先申請的空間怎么辦?那么它會增加“誤傷率”。
下面我們把這個項目運(yùn)行起來看看效果吧。
運(yùn)行redis-practice工程
運(yùn)行起來后
我們可以使用postman先來做個小實驗
我們使用"、addEmailToBloom"往redis bloom filter里插入了一個“yumi@yahoo.com”的email。
接下來我們會使用“/checkEmailInBloom”來驗證這個email地址是否存在
我們使用redisclient連接上我們的redis查看,這個值確實也是插入進(jìn)了bloom filter了。
使用壓測工具喂120萬條數(shù)據(jù)進(jìn)入Redis Bloomfilter看實際效果
接下來,我們用jmeter對著“/addEmailToBloom”喂上個120萬左右數(shù)據(jù)進(jìn)去,然后我們再來看bloom filter在120萬email按照布隆算 法喂進(jìn)去后我們的系統(tǒng)是如何表現(xiàn)的。
我這邊使用的是apache-jmeter5.0,為了偷懶,我用了apache-jmeter里的_RandomString函數(shù)來動態(tài)創(chuàng)造16位字符長度的email。這邊用戶名、地址信息都是恒定,就是email是每次不一樣,都是一串16位的隨機(jī)字符+“@163.com”。
jmeter中BeanShell產(chǎn)生16位字符隨機(jī)組成email的函數(shù)
useremail="${__RandomString(16,abcdefghijklmnop,myemail)}"+"@163.com"; vars.put("random_email",useremail);
jmeter測試計劃設(shè)置成了75個線程,連續(xù)運(yùn)行30分鐘(實踐上筆者運(yùn)行了3個30分鐘,因為是demo環(huán)境,30分鐘每次插大概40萬條數(shù)據(jù)進(jìn)去吧)
jmeter post請求
然后我們使用jmeter命令行來運(yùn)行這個測試計劃:
jmeter -n -t add_randomemail_to_bloom.jmx -l add_email_to_bloom\report\03-result.csv -j add_email_to_bloom\logs\03-log.log -e -o add_email_to_bloom\html_report_3
它代表:
- -t 指定jmeter執(zhí)行計劃文件所在路徑;
- -l 生成report的目錄,這個目錄如果不存在則創(chuàng)建 ,必須是一個空目錄;
- -j 生成log的目錄,這個目錄如果不存在則創(chuàng)建 ,必須是一個空目錄;
- -e 生成html報告,它配合著-o參數(shù)一起使用;
- -o 生成html報告所在的路徑,這個目錄如果不存在則創(chuàng)建 ,必須是一個空目錄;
回車后它就開始運(yùn)行了
一直執(zhí)行到這個過程全部結(jié)束,跳出command命令符為止。
我們查看我們用-e -o生成的jmeter html報告,前面說過了,我一共運(yùn)行了3次,第一次是10分鐘70059條數(shù)據(jù) ,第二次是30分鐘40多萬條數(shù)據(jù) ,第三次是45他鐘70多萬條數(shù)據(jù)。我共計插入了1,200,790條email。
而這120萬數(shù)據(jù)總計在redis中占用內(nèi)存不超過8mb,見下面demo環(huán)境的zabbix錄制的記錄
120萬條數(shù)據(jù)插進(jìn)去后,我們接著從我們的log4j的輸出中隨便找一條logger.info住的email如:egpoghnfjekjajdo@163.com來看一下,redis bloomfilter找到這條記錄的表現(xiàn)如何,76ms,我運(yùn)行了多次,平均在80ms左右:
通過上面這么一個實例,大家可以看到把email以hash后并以bit的形式存入bloomfilter后,它占用的內(nèi)存是多么的小,而查詢效率又是多么的高。
往往在生產(chǎn)上,我們經(jīng)常會把上千萬或者是上億的記錄"load"進(jìn)bloomfilter,然后拿它去做“防擊穿”或者是去重的動作。
只要bloomfilter中不存在的key直接返回客戶端false,配合著nginx的動態(tài)擴(kuò)充、cdn、waf、接口層的緩存,整個網(wǎng)站抗6位數(shù)乃至7位數(shù)的并發(fā)其實是件非常簡單的事。
到此這篇關(guān)于SpringBoot+Redis布隆過濾器防惡意流量擊穿緩存的文章就介紹到這了,更多相關(guān)SpringBoot防惡意流量擊穿緩存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談SpringBoot之開啟數(shù)據(jù)庫遷移的FlyWay使用
這篇文章主要介紹了淺談SpringBoot之開啟數(shù)據(jù)庫遷移的FlyWay使用,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01Java 隊列實現(xiàn)原理及簡單實現(xiàn)代碼
這篇文章主要介紹了Java 隊列實現(xiàn)原理及簡單實現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2016-10-10淺談HttpClient、okhttp和RestTemplate的區(qū)別
這篇文章主要介紹了HttpClient、okhttp和RestTemplate的區(qū)別,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06