Redis에는 이메일을 키로 하는 사용자 등록 이메일 세트가 포함되어 있고 이는 User 테이블의 일부 필드에 해당한다고 가정합니다. DB.
일반적으로 캐시에서 데이터를 읽는 것이 더 빠르기 때문에 먼저 사용자가 Redis의 멤버인지 확인합니다. 이 멤버가 캐시에 없으면 DB에서 쿼리합니다.
이제 다양한 IP에서 수천만 건의 요청이 있다고 상상해 보세요(아무것도 없다고 생각하지 마세요. 공격 비용이 매우 낮기 때문에 2018년과 2019년에 발생했습니다.) 키를 사용하여 웹 사이트에 액세스합니다.
요청이 웹 서버에 도착합니다.
요청은 애플리케이션 계층->마이크로서비스 계층으로 전달됩니다.
요청은 검색을 위해 Redis로 이동합니다. 이 Key는 Redis에 존재하지 않습니다.
그러면 요청이 DB 계층에 도달하고 DB 연결이 설정된 후 쿼리가 수행됩니다.
수천만 개의 DB인지 연결 요청은 Redis가 감당할 수 있는지 여부는 문제가 되지 않습니다. DB도 즉시 중단되기 때문입니다. 이는 "캐시 고장"이라고도 알려진 "Redis 침투"입니다. 이는 캐시나 DB를 폭파시켜 일련의 "눈사태 효과"를 유발합니다.
Bloom 필터를 사용하여 사용자 테이블의 모든 주요 쿼리 필드를 Redis Bloom 필터에 넣을 수 있습니다. 어떤 사람들은 '이건 미친 게 아니야, 내 회원이 4천만 명이나 된다고?'라고 말할 것입니다. 그래서 뭐!
Redis에 4,000명의 회원을 넣는 것은 과장입니다. 일부 웹사이트에는 8천만 명 또는 1억 명의 회원이 있습니다. 그래서 Redis에 직접 넣어달라고 하지 않고 Bloom 필터에 넣어달라고 했는데요!
키와 값은 Bloom 필터에 직접 입력되지 않습니다. 저장되는 내용은 다음과 같습니다.
BloomFilter는 1970년 Burton Howard Bloom이 제안한 공간 효율적인 확률 데이터 구조입니다. 일반적으로 요소가 집합에 있는지 여부를 확인하는 데 사용됩니다. 뛰어난 공간 효율성에도 불구하고 잘못된 긍정 오류가 발생할 수 있습니다.
False positive&&False negatives
BloomFiter는 공간 효율성을 위해 특정 정확도를 희생하기 때문입니다. 그래서 False positive 문제가 발생합니다.
거짓양성
BloomFilter는 요소가 컬렉션에 있다고 판단할 때 특정 오류율을 가지게 됩니다. 이 오류율을 거짓양성이라고 합니다. 일반적으로 fpp로 축약됩니다.
False negatives
BloomFilter는 요소가 세트에 없을 때 오류율을 결정합니다. BloomFilter는 요소가 컬렉션에 없다고 판단한 경우 해당 요소가 컬렉션에 없어야 합니다. 따라서 거짓음성 확률은 0이다.
BloomFilter는 길이가 m 비트인 바이트 배열을 사용하고 k 해시 함수를 사용하고 요소를 추가합니다. k 해시를 통해 요소를 바이트 배열의 k 위치에 매핑하고 해당 위치의 바이트를 1로 설정합니다.
요소 존재 여부 쿼리: 요소를 k번 해시하여 k 위치에 해당하는 비트가 1이면 존재하는 것으로 간주되고, 그렇지 않으면 존재하지 않는 것으로 간주됩니다.
비트를 저장하기 때문에 데이터의 양은 매우 작습니다. 이 블로그를 작성할 때 Redis 블룸 필터에 100만 개의 이메일 메시지를 삽입했는데 그 크기는 3Mb 미만이었습니다.
Bloom Filter에는 몇 가지 핵심 값이 있습니다. 이 값을 기반으로 얼마나 많은 데이터를 넣을지, 잘못된 피해율이 있을 때 얼마나 많은 시스템 리소스를 차지할지 대략적으로 계산할 수 있습니다. 이 알고리즘의 URL은 https://krisives.github.io/bloom-calculator/입니다. 100만 개의 데이터를 입력하고 사고 피해율을 0.001%라고 가정합니다. 보세요. 시스템 메모리 리소스가 자동으로 결정됩니다. Redis를 신청해야 하는 사람은 몇 명입니까?
이런 재해율을 어떻게 해결할까요? 매우 간단합니다. 사고가 발생하면 기업이나 운영업체에서 사고 사고율을 보고합니다. 이때 100만 개의 데이터에 비해 작은 화이트리스트만 추가하면 됩니다. Bloom 필터의 쿼리 속도는 일반적으로 80~100밀리초 내에 쿼리 결과를 반환하여 호출 측에 키가 있는지 알려줄 수 있습니다.
Python 크롤러를 사용하여 4억 개의 URL을 크롤링했다고 가정해 보겠습니다. 중복된 항목을 제거해야 합니까?
보세요, 이 시나리오에서는 Bloom 필터가 사용되었습니다.
Redis BloomFilter 여정을 시작하겠습니다.
Redis는 4.0의 Bloom Filter만 지원하므로 이 예에서는 Redis5.4를 사용합니다.
Redis의 블룸 필터 다운로드 주소는 다음과 같습니다: https://github.com/RedisLabsModules/redisbloom.git
git clone https://github.com/RedisLabsModules/redisbloom.git cd redisbloom make # 编译
Redis가 시작될 때 블룸 필터를 로드하는 방법에는 두 가지가 있습니다:
수동 로딩:
redis-server --loadmodule ./redisbloom/rebloom.so
시작할 때마다 자동 로딩:
Redis의 redis.conf 파일을 편집하고 다음을 추가합니다:
loadmodule /soft/redisbloom/redisbloom.so
이렇게:
基本指令:
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
这个命令的作用是检查 Bloom 过滤器中是否包含指定 key 的值。存在:返回1,不存在:返回0。
网上很多写的都是要么是直接使用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 hash74 = Hashing.murmur3_128().hashObject(value, funnel).asLong(); int hash2 = (int) hash74; int hash3 = (int) (hash74 >>> 32); for (int i = 1; i <= numHashFunctions; i++) { int nextHash = hash2 + i * hash3; 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
项目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里有两个方法:
public ResponseEntity
public ResponseEntity
以此来完成验证塞入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里插入了一个“yumi@yahoo.com”的email。
接下来我们会使用“/checkEmailInBloom”来验证这个email地址是否存在
我们使用redisclient连接上我们的redis查看,这个值确实也是插入进了bloom filter了。
接下来,我们用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如:egpoghnfjekjajdo@163.com来看一下,redis bloomfilter找到这条记录的表现如何,76ms,我运行了多次,平均在80ms左右:
通过上面这么一个实例,大家可以看到把email以hash后并以bit的形式存入bloomfilter后,它占用的内存是多么的小,而查询效率又是多么的高。
往往在生产上,我们经常会把上千万或者是上亿的记录"load"进bloomfilter,然后拿它去做“防击穿”或者是去重的动作。
bloomfilter에 존재하지 않는 키가 클라이언트에 직접 false를 반환하는 한, nginx, cdn, waf 및 인터페이스 레이어 캐싱의 동적 확장을 통해 실제로 전체 웹사이트에서 6자리 또는 심지어 7자리 동시성도 가능합니다.
위 내용은 SpringBoot+Redis Bloom 필터는 악성 트래픽이 캐시에 침투하는 것을 방지합니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!