SpringBoot+Redis布隆過濾器防惡意流量擊穿緩存
本文主要介紹了SpringBoot+Redis布隆過濾器防惡意流量擊穿緩存,具體如下:

什么是惡意流量穿透
假設(shè)我們的Redis里存有一組用戶的注冊(cè)email,以email作為Key存在,同時(shí)它對(duì)應(yīng)著DB里的User表的部分字段。
一般來說,一個(gè)合理的請(qǐng)求過來我們會(huì)先在Redis里判斷這個(gè)用戶是否是會(huì)員,因?yàn)閺木彺胬镒x數(shù)據(jù)返回快。如果這個(gè)會(huì)員在緩存中不存在那么我們會(huì)去DB中查詢一下。
現(xiàn)在試想,有千萬個(gè)不同IP的請(qǐng)求(不要以為沒有,我們就在2018年和2019年碰到了,因?yàn)楣舻某杀竞艿停е鳵edis里根本不存在的key來訪問你的網(wǎng)站,這時(shí)我們來設(shè)想一下:
- 請(qǐng)求到達(dá)Web服務(wù)器;
- 請(qǐng)求派發(fā)到應(yīng)用層->微服務(wù)層;
- 請(qǐng)求去Redis撈數(shù)據(jù),Redis內(nèi)不存在這個(gè)Key;
- 于是請(qǐng)求到達(dá)DB層,在DB建立connection后進(jìn)行一次查詢
千萬乃至上億的DB連接請(qǐng)求,先不說Redis是否撐的住DB也會(huì)被瞬間打爆。這就是“Redis穿透”又被稱為“緩存擊穿”,它會(huì)打爆你的緩存或者是連DB一起打爆進(jìn)而引起一系列的“雪崩效應(yīng)”。
怎么防
那就是使用布隆過濾器,可以把所有的user表里的關(guān)鍵查詢字段放于Redis的bloom過濾器內(nèi)。有人會(huì)說,這不瘋了,我有4000萬會(huì)員?so what!
你把4000會(huì)員放在Redis里是比較夸張,有些網(wǎng)站有8000萬、1億會(huì)員呢?因此我沒讓你直接放在Redis里,而是放在布隆過濾器內(nèi)!
布隆過濾器內(nèi)不是直接把key,value這樣放進(jìn)去的,它存放的內(nèi)容是這么一個(gè)樣的:
BloomFilter是一種空間效率的概率型數(shù)據(jù)結(jié)構(gòu),由Burton Howard Bloom 1970年提出的。通常用來判斷一個(gè)元素是否在集合中。具有極高的空間效率,但是會(huì)帶來假陽性(False positive)的錯(cuò)誤。
False positive&&False negatives
由于BloomFiter犧牲了一定的準(zhǔn)確率換取空間效率。所以帶來了False positive的問題。
False positive
BloomFilter在判斷一個(gè)元素在集合中的時(shí)候,會(huì)出現(xiàn)一定的錯(cuò)誤率,這個(gè)錯(cuò)誤率稱為False positive的。通??s寫為fpp。
False negatives
BloomFilter判斷一個(gè)元素不在集合中的時(shí)候的錯(cuò)誤率。 BloomFilter判斷該元素不在集合中,則該元素一定不再集合中。故False negatives概率為0。
BloomFilter使用長度為m bit的字節(jié)數(shù)組,使用k個(gè)hash函數(shù),增加一個(gè)元素: 通過k次hash將元素映射到字節(jié)數(shù)組中k個(gè)位置中,并設(shè)置對(duì)應(yīng)位置的字節(jié)為1。
查詢?cè)厥欠翊嬖? 將元素k次hash得到k個(gè)位置,如果對(duì)應(yīng)k個(gè)位置的bit是1則認(rèn)為存在,反之則認(rèn)為不存在。
由于它里面存的都是bit,因此這個(gè)數(shù)據(jù)量會(huì)很小很小,小到什么樣的程度呢?在寫本博客時(shí)我插了100萬條email信息進(jìn)入Redis的bloom filter也只占用了3Mb不到。
Bloom Filter會(huì)有幾比較關(guān)鍵的值,根據(jù)這個(gè)值你是大致可以算出放多少條數(shù)據(jù)然后它的誤傷率在多少時(shí)會(huì)占用多少系統(tǒng)資源的。這個(gè)算法有一個(gè)網(wǎng)址:https://krisives.github.io/bloom-calculator/,我們放入100萬條數(shù)據(jù),假設(shè)誤傷率在0.001%,看,它自動(dòng)得出Redis需要申請(qǐng)的系統(tǒng)內(nèi)存資源是多少?
那么怎么解決這個(gè)誤傷率呢?很簡單的,當(dāng)有誤傷時(shí)業(yè)務(wù)或者是運(yùn)營會(huì)來報(bào)誤傷率,這時(shí)你只要添加一個(gè)小白名單就是了,相對(duì)于100萬條數(shù)據(jù)來說,1000個(gè)白名單不是問題。并且bloom filter的返回速度超塊,80-100毫秒內(nèi)即返回調(diào)用端該Key存在或者是不存了。

布隆過濾器的另一個(gè)用武場景
假設(shè)我用python爬蟲爬了4億條url,需要去重?
看,布隆過濾器就是用于這個(gè)場景的。
下面就開始我們的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啟動(dòng)時(shí)可以加載bloom filter有兩種方式:
手工加載式:
redis-server --loadmodule ./redisbloom/rebloom.so
每次啟動(dòng)自加載:
編輯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)建一個(gè)空的布隆過濾器,并設(shè)置一個(gè)期望的錯(cuò)誤率和初始大小。{error_rate}過濾器的錯(cuò)誤率在0-1之間,如果要設(shè)置0.1%,則應(yīng)該是0.001。該數(shù)值越接近0,內(nèi)存消耗越大,對(duì)cpu利用率越高。
bf.add {key} {item}
127.0.0.1:6379> bf.add userid '181920' (integer) 1
上面這條命令就是:往過濾器中添加元素。如果key不存在,過濾器會(huì)自動(dòng)創(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一個(gè)外部進(jìn)程來調(diào)用Redis的bloom filter指令的。很多都是調(diào)不通或者h(yuǎn)elloworld一個(gè)級(jí)別的,是根本無法上生產(chǎn)級(jí)別應(yīng)用的。
筆者給出的代碼保障讀者完全可用!
筆者不是數(shù)學(xué)家,因此就借用了google的guava包來實(shí)現(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;
}
/**
* 計(jì)算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)));
}
/**
* 計(jì)算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工程
項(xiàng)目Redis配置
我們?cè)趓edis-practice工程里建立一個(gè)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
以上這個(gè)是demo環(huán)境的配置。
我們此處依舊使用的是在前一篇springboot+nacos+dubbo實(shí)現(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等會(huì)跑出異常
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;
}
/**
* 對(duì)hash類型的數(shù)據(jù)操作
*
* @param redisTemplate
* @return
*/
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
/**
* 對(duì)redis字符串類型數(shù)據(jù)操作
*
* @param redisTemplate
* @return
*/
@Bean
public ValueOperations<String, Object> valueOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForValue();
}
/**
* 對(duì)鏈表類型的數(shù)據(jù)操作
*
* @param redisTemplate
* @return
*/
@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
/**
* 對(duì)無序集合類型的數(shù)據(jù)操作
*
* @param redisTemplate
* @return
*/
@Bean
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}
/**
* 對(duì)有序集合類型的數(shù)據(jù)操作
*
* @param redisTemplate
* @return
*/
@Bean
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForZSet();
}
}這個(gè)配置除實(shí)現(xiàn)了springboot自動(dòng)發(fā)現(xiàn)redis在application.properties中的配置外我們還添加了不少redis基本的數(shù)據(jù)結(jié)構(gòu)的操作的封裝。
我們?yōu)榇诉€要再封裝一套R(shí)edis 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)過期時(shí)長,單位:秒
*/
public static final long DEFAULT_EXPIRE = 60 * 60 * 24;
/**
* 不設(shè)置過期時(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不存在時(shí)才重命名
*
* @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);
}
/**
* 刪除多個(gè)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ù)。
最后我們?cè)趕ky-common里放置一個(gè)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)容,此次我們?cè)黾恿藢?duì)于“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組件搭建完畢,接下來我們重點(diǎn)說我們的demo工程
Demo工程:redis-practice說明

pom.xml文件,它依賴于nacos-parent同時(shí)還引用了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>用于啟動(dòng)的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);
}
}然后我們制作了一個(gè)controller名為UserController,該controller里有兩個(gè)方法:
- public ResponseEntity<String> addUser(@RequestBody String params),該方法用于接受來自外部的api post然后把一條email地址塞入redis的bloomfilter中;
- public ResponseEntity<String> findEmailInBloom(@RequestBody String params),該方法用于接受來自外部的api post然后去redis的bloomfilter中驗(yàn)證是否外部輸入的user信息中的email地址在上百萬的email記錄中存在;
以此來完成驗(yàn)證塞入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ù)先申請(qǐng)的空間怎么辦?那么它會(huì)增加“誤傷率”。
下面我們把這個(gè)項(xiàng)目運(yùn)行起來看看效果吧。
運(yùn)行redis-practice工程
運(yùn)行起來后

我們可以使用postman先來做個(gè)小實(shí)驗(yàn)

我們使用"、addEmailToBloom"往redis bloom filter里插入了一個(gè)“yumi@yahoo.com”的email。
接下來我們會(huì)使用“/checkEmailInBloom”來驗(yàn)證這個(gè)email地址是否存在

我們使用redisclient連接上我們的redis查看,這個(gè)值確實(shí)也是插入進(jìn)了bloom filter了。

使用壓測工具喂120萬條數(shù)據(jù)進(jìn)入Redis Bloomfilter看實(shí)際效果
接下來,我們用jmeter對(duì)著“/addEmailToBloom”喂上個(gè)120萬左右數(shù)據(jù)進(jìn)去,然后我們?cè)賮砜碽loom filter在120萬email按照布隆算 法喂進(jìn)去后我們的系統(tǒng)是如何表現(xiàn)的。
我這邊使用的是apache-jmeter5.0,為了偷懶,我用了apache-jmeter里的_RandomString函數(shù)來動(dòng)態(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測試計(jì)劃設(shè)置成了75個(gè)線程,連續(xù)運(yùn)行30分鐘(實(shí)踐上筆者運(yùn)行了3個(gè)30分鐘,因?yàn)槭莇emo環(huán)境,30分鐘每次插大概40萬條數(shù)據(jù)進(jìn)去吧)

jmeter post請(qǐng)求

然后我們使用jmeter命令行來運(yùn)行這個(gè)測試計(jì)劃:
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í)行計(jì)劃文件所在路徑;
- -l 生成report的目錄,這個(gè)目錄如果不存在則創(chuàng)建 ,必須是一個(gè)空目錄;
- -j 生成log的目錄,這個(gè)目錄如果不存在則創(chuàng)建 ,必須是一個(gè)空目錄;
- -e 生成html報(bào)告,它配合著-o參數(shù)一起使用;
- -o 生成html報(bào)告所在的路徑,這個(gè)目錄如果不存在則創(chuàng)建 ,必須是一個(gè)空目錄;
回車后它就開始運(yùn)行了

一直執(zhí)行到這個(gè)過程全部結(jié)束,跳出command命令符為止。
我們查看我們用-e -o生成的jmeter html報(bào)告,前面說過了,我一共運(yùn)行了3次,第一次是10分鐘70059條數(shù)據(jù) ,第二次是30分鐘40多萬條數(shù)據(jù) ,第三次是45他鐘70多萬條數(shù)據(jù)。我共計(jì)插入了1,200,790條email。



而這120萬數(shù)據(jù)總計(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左右:

通過上面這么一個(gè)實(shí)例,大家可以看到把email以hash后并以bit的形式存入bloomfilter后,它占用的內(nèi)存是多么的小,而查詢效率又是多么的高。
往往在生產(chǎn)上,我們經(jīng)常會(huì)把上千萬或者是上億的記錄"load"進(jìn)bloomfilter,然后拿它去做“防擊穿”或者是去重的動(dòng)作。
只要bloomfilter中不存在的key直接返回客戶端false,配合著nginx的動(dòng)態(tài)擴(kuò)充、cdn、waf、接口層的緩存,整個(gè)網(wǎng)站抗6位數(shù)乃至7位數(shù)的并發(fā)其實(shí)是件非常簡單的事。
到此這篇關(guān)于SpringBoot+Redis布隆過濾器防惡意流量擊穿緩存的文章就介紹到這了,更多相關(guān)SpringBoot防惡意流量擊穿緩存內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中null相關(guān)注解的實(shí)現(xiàn)
本文主要介紹了Java中null相關(guān)注解的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
spring?cloud?配置阿里數(shù)據(jù)庫連接池?druid的示例代碼
這篇文章主要介紹了spring?cloud?配置阿里數(shù)據(jù)庫連接池?druid,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03
利用Spring IOC技術(shù)實(shí)現(xiàn)用戶登錄驗(yàn)證機(jī)制
這篇文章主要為大家詳細(xì)介紹了Spring IOC技術(shù)實(shí)現(xiàn)用戶登錄驗(yàn)證機(jī)制的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10
Seata集成Mybatis-Plus解決多數(shù)據(jù)源事務(wù)問題
當(dāng)進(jìn)行業(yè)務(wù)操作時(shí),訂單發(fā)生異常 ,進(jìn)行了回滾操作,因?yàn)樵诓煌臄?shù)據(jù)庫實(shí)例中,余額卻扣除成功,此時(shí)發(fā)現(xiàn)數(shù)據(jù)不一致問題,本文給大家介紹Seata集成Mybatis-Plus解決多數(shù)據(jù)源事務(wù)問題,感興趣的朋友一起看看吧2023-11-11
Java Web使用Html5 FormData實(shí)現(xiàn)多文件上傳功能
這篇文章主要介紹了Java Web使用Html5 FormData實(shí)現(xiàn)多文件上傳功能,需要的朋友可以參考下2017-07-07
Mybatis控制臺(tái)打印SQL語句的兩種方式實(shí)現(xiàn)
這篇文章主要介紹了Mybatis控制臺(tái)打印SQL語句的兩種方式實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03

