分布式BloomFilter实现
布隆算法是一个以牺牲一定的准确率来换取低内存消耗的过滤算法,可以实现大量数据的过滤、去重等操作。
利用Redis的BitMap实现布隆过滤器的底层映射。
BloomFilter
请求到达正式业务之前, 判断该请求是否有效
维护一个大的bit数组, 把有效key的一次或多次的hash索引位置标志已存在. 当有请求进来时, 计算进来的key的hash索引, 判断每一个索引的值是否为true.
常用于处理缓存穿透问题
选用redis实现的好处
部署高可用节点时, 减少每一个节点的开销
redis可以持久化, 避免因服务器宕机导致需要重新灌数据的开销
redis中可实现的数组长度更长,大数据量下,hash索引可散列的范围更大
实现
索引个数,bit数组长度
使用google中根据原有数据长度计算索引个数, bit数组长度的方法
hash算法
使用一个key衍生出多个key, 根据对bit长度求余的hash算法
initBloomFilter方法
根据数据的长度, 计算hash个数,和bit数组长度, 将以上信息和容错率信息保存到redis中(这样可以实现多个过滤器)
使用通道将信息,所有数据压到redis中,使用通道可以提高效率,减少网络开销(测试时初始数据,可以减少75%时间)
首先看下项目需要的jar包
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!--借助guava的布隆过滤器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
代码
package com.until;
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; //hash循环次数
private int bitSize; //bitsize长度
private Funnel<T> funnel;
/**
* @param funnel
* @param expectedInsertions 期望插入长度
* @param fpp 误差率
*/
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);
}
/**
* 计算bit数组长度
*/
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)));
}
}
package com.until;
import com.google.common.base.Preconditions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
/**
* @Author : JCccc
* @CreateTime : 2020/4/23
* @Description :
**/
@Service
public class RedisBloomFilter {
@Autowired
private RedisTemplate redisTemplate;
/**
* 根据给定的布隆过滤器添加值
*/
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) {
System.out.println("key : " + key + " " + "value : " + i);
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) {
System.out.println("key : " + key + " " + "value : " + i);
if (!redisTemplate.opsForValue().getBit(key, i)) {
return false;
}
}
return true;
}
}
调用:
package com.controller;
import com.google.common.base.Charsets;
import com.google.common.hash.Funnel;
import com.until.BloomFilterHelper;
import com.until.RedisBloomFilter;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.UUID;
@RequestMapping("/redisBloomFilter")
@Api(tags = "布朗过滤器", value = "redis相关接口")
@RestController
@Slf4j
public class RedisBloomFilterController {
@Resource
private RedisBloomFilter redisBloomFilter;
BloomFilterHelper<String> myBloomFilterHelper = new BloomFilterHelper<>(
(Funnel<String>) (from, into) -> into.putString(from, Charsets.UTF_8).putString(from, Charsets.UTF_8), 150000, 0.00001);
@GetMapping("/redis/bloomFilter")
@ApiOperation("redis布隆过滤器数据添加")
public void redisBloomFilter(){
//List<String> allResourceId = redisBloomFilter.getAllResourceId();
for (int i=0;i<10;i++) {
String uuid = UUID.randomUUID().toString().replaceAll("-","");
System.out.println("uuid:"+uuid);
//将所有的资源id放入到布隆过滤器中
redisBloomFilter.addByBloomFilter(myBloomFilterHelper,"bloom",uuid);
}
//return new ResponseResult(ResponseEnum.SUCCESS);
}
@GetMapping("/redis/bloomFilter/resourceId")
@ApiOperation("redis布隆过滤器资源测试")
public void redisBloomFilterResourceId(@RequestParam("resourceId")String resourceId){
boolean mightContain = redisBloomFilter.includeByBloomFilter(myBloomFilterHelper,"bloom",resourceId);
if (!mightContain){
System.out.println("错误:"+mightContain);
// return new QueryResult<>(ResCenterEnum.RESOURCE_EXSIT,"");
}
// return new ResponseResult(ResponseEnum.SUCCESS);
}
}