SpringBoot+Redis布隆過濾器防惡意流量擊穿緩存的正確姿勢

什麼是惡意流量穿透

假設我們的Redis裏存有一組用戶的註冊email,以email作爲Key存在,同時它對應着DB裏的User表的部分字段。

一般來說,一個合理的請求過來我們會先在Redis裏判斷這個用戶是否是會員,因爲從緩存裏讀數據返回快。如果這個會員在緩存中不存在那麼我們會去DB中查詢一下。

現在試想,有千萬個不同IP的請求(不要以爲沒有,我們就在2018年和2019年碰到了,因爲攻擊的成本很低)帶着Redis里根本不存在的key來訪問你的網站,這時我們來設想一下:

  1. 請求到達Web服務器;
  2. 請求派發到應用層->微服務層;
  3. 請求去Redis撈數據,Redis內不存在這個Key;
  4. 於是請求到達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

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

上面這條命令就是:創建一個空的布隆過濾器,並設置一個期望的錯誤率和初始大小。{error_rate}過濾器的錯誤率在0-1之間,如果要設置0.1%,則應該是0.001。該數值越接近0,內存消耗越大,對cpu利用率越高

bf.add {key} {item}

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

上面這條命令就是:往過濾器中添加元素。如果key不存在,過濾器會自動創建。

bf.exists {key} {item}

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

上面這條命令就是:判斷指定key的value是否在bloomfilter裏存在。存在:返回1,不存在:返回0。

結合SpringBoot使用

網上很多寫的都是要麼是直接使用jedis來操作的,或者是java裏execute一個外部進程來調用Redis的bloom filter指令的。很多都是調不通或者helloworld一個級別的,是根本無法上生產級別應用的。

筆者給出的代碼保障讀者完全可用!

筆者不是數學家,因此就借用了google的guava包來實現了核心算法,核心代碼如下:

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數組的長度
	 */
	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方法執行次數
	 */
	private int optimalNumOfHashFunctions(long n, long m) {
		return Math.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=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環境的配置。

我們此處依舊使用的是在前一篇springboot+nacos+dubbo實現異常統一管理中的xxx-project->sky-common->nacos-parent的依賴結構。

在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作爲默認緩存工具
	 * 
	 * @param redisTemplate
	 * @return
	 */
	@Bean
	public CacheManager cacheManager(RedisTemplate redisTemplate) {
		RedisCacheManager rcm = new RedisCacheManager(redisTemplate);
		return rcm;
	}

	/**
	 * retemplate相關配置
	 * 
	 * @param factory
	 * @return
	 */
	@Bean
	public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

		RedisTemplate<String, Object> template = new RedisTemplate<>();
		// 配置連接工廠
		template.setConnectionFactory(factory);

		// 使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值(默認使用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());

		// 設置hash key 和value序列化模式
		template.setHashKeySerializer(new StringRedisSerializer());
		template.setHashValueSerializer(jacksonSeial);
		template.afterPropertiesSet();

		return template;
	}

	/**
	 * 對hash類型的數據操作
	 *
	 * @param redisTemplate
	 * @return
	 */
	@Bean
	public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
		return redisTemplate.opsForHash();
	}

	/**
	 * 對redis字符串類型數據操作
	 *
	 * @param redisTemplate
	 * @return
	 */
	@Bean
	public ValueOperations<String, Object> valueOperations(RedisTemplate<String, Object> redisTemplate) {
		return redisTemplate.opsForValue();
	}

	/**
	 * 對鏈表類型的數據操作
	 *
	 * @param redisTemplate
	 * @return
	 */
	@Bean
	public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
		return redisTemplate.opsForList();
	}

	/**
	 * 對無序集合類型的數據操作
	 *
	 * @param redisTemplate
	 * @return
	 */
	@Bean
	public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
		return redisTemplate.opsForSet();
	}

	/**
	 * 對有序集合類型的數據操作
	 *
	 * @param redisTemplate
	 * @return
	 */
	@Bean
	public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
		return redisTemplate.opsForZSet();
	}
}

這個配置除實現了springboot自動發現redis在application.properties中的配置外我們還添加了不少redis基本的數據結構的操作的封裝。

我們爲此還要再封裝一套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;

	/**
	 * 默認過期時長,單位:秒
	 */
	public static final long DEFAULT_EXPIRE = 60 * 60 * 24;

	/**
	 * 不設置過期時長
	 */
	public static final long NOT_EXPIRE = -1;

	public boolean existsKey(String key) {
		return redisTemplate.hasKey(key);
	}

	/**
	 * 重名名key,如果newKey已經存在,則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);
	}

	/**
	 * 設置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設置爲永久有效
	 *
	 * @param key
	 */
	public void persistKey(String key) {
		redisTemplate.persist(key);
	}

	/**
	 * 根據給定的布隆過濾器添加值
	 */
	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);
		}
	}

	/**
	 * 根據給定的布隆過濾器判斷值是否存在
	 */
	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文件夾,源碼如上已經貼了,因此此處就不再作重複。

最後我們在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;
	}

}

下面給出我們所有gitrepo裏依賴的nacos-parent的pom.xml文件內容,此次我們增加了對於“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裏有兩個方法:

  1. public ResponseEntity<String> addUser(@RequestBody String params),該方法用於接受來自外部的api post然後把一條email地址塞入redis的bloomfilter中;
  2. public ResponseEntity<String> findEmailInBloom(@RequestBody String params),該方法用於接受來自外部的api post然後去redis的bloomfilter中驗證是否外部輸入的user信息中的email地址在上百萬的email記錄中存在;

以此來完成驗證塞入redis的bloom filter中上百萬條記錄佔用了多少內存以及使用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萬數據的空間。如果存和的數據大於了你預先申請的空間怎麼辦?那麼它會增加“誤傷率”

下面我們把這個項目運行起來看看效果吧。

運行redis-practice工程

運行起來後

我們可以使用postman先來做個小實驗

我們使用"、addEmailToBloom"往redis bloom filter裏插入了一個“[email protected]”的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 -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執行計劃文件所在路徑;
  • -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如:[email protected]來看一下,redis bloomfilter找到這條記錄的表現如何,76ms,我運行了多次,平均在80ms左右:

通過上面這麼一個實例,大家可以看到把email以hash後並以bit的形式存入bloomfilter後,它佔用的內存是多麼的小,而查詢效率又是多麼的高。

往往在生產上,我們經常會把上千萬或者是上億的記錄"load"進bloomfilter,然後拿它去做“防擊穿”或者是去重的動作。

只要bloomfilter中不存在的key直接返回客戶端false,配合着nginx的動態擴充、cdn、waf、接口層的緩存,整個網站抗6位數乃至7位數的併發其實是件非常簡單的事。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章