将任意大小的输入数据转换成特定大小的输出数据的函数,转换后的数据称为哈希值或哈希编码,也叫散列值。
特点:
- 单向散列函数:如果两个散列值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的。这个特性是散列函数具有确定性的结果。
- 散列碰撞:散列函数的输入和输出不是唯一对应关系的,如果两个散列值相同,两个输入值很可能是相同的,但也可能不同
用 hash表存储大数据量时,空间效率还是很低,当只有一个 hash 函数时,还很容易发生哈希碰撞。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除元素困难
- 判断元素如果不在集合内,那么一定不在
- 判断元素如果在集合内,那么不一定在
- 从上面两点可以看出删除元素的问题,因为有可能会误删除
布隆过滤器(BloomFilter )是由一个固定大小的二进制向量或者位图(bitmap)和一系列映射函数组成的。
在初始状态时,它的所有位都被置为0:
000000000000000000000000000000000000
往布隆过滤器中添加一个元素时,我们将这个值传入 k 个hash函数,然后将结果位置bit置为1,例如k=3
000000000100000010000000100000000000
此时可以看出,当我们插入元素时,有可能存在bit位置是其他元素的结果,此时就会存在误判率。
- 如果这些点有任何一个 0,则被查询变量一定不在;
- 如果都是 1,则被查询变量很可能存在
通过数据结构我们可以看出,有可能多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,因此误判的根源在于相同的 bit 位被多次映射为 1。
这种情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位。如果我们直接删除这一位的话,会影响其他的元素。
package com.redis.advanced.pierce;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
/**
* Guava实现布隆过滤器
* TODO 基本原理
* @author lizhifu
* @date 2021/1/14
*/
public class GuavaBloomFilter {
/**
* 预计要插入多少数据
*/
private static int size = 1000000;
/**
* 期望的误判率
*/
private static double fpp = 0.01;
private static BloomFilter<Integer> bloomFilter =
BloomFilter.create(Funnels.integerFunnel(), size, fpp);
public static void main(String[] args) {
//插入数据
for (int i = 0; i < 10 ; i++) {
bloomFilter.put(i);
}
for (int i = 10; i < 20 ; i++) {
System.out.println("========"+bloomFilter.mightContain(i));
}
}
}
当误判率大小为0.1时,k为3。当误判率大小为0.01时,k为7(k为hash函数次数)
package com.redis.advanced.service;
import com.google.common.base.Preconditions;
import com.google.common.hash.Funnel;
import com.google.common.hash.Hashing;
/**
* 布隆过滤器
* 0代表不存在某个数据,1代表存在某个数据,一般不能删除布隆过滤器里的数据
* 代码来源于{@link com.google.common.hash.BloomFilter}
* @author lizhifu
* @date 2020/12/9
*/
public class BloomFilterHelper<T> {
private int numHashFunctions;
private int bitSize;
/**
* Guava中定义的一个接口,它和PrimitiveSink配套使用,
* 主要是把任意类型的数据转化成Java基本数据类型(primitive value,如char,byte,int……)
* ,默认用java.nio.ByteBuffer实现,最终均转化为byte数组
*/
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);
}
/**
* 散列时,此策略使用Hashing.murmur3_128所有128位。
* 它看起来与MURMUR128_MITZ_32中的实现不同,
* 因为我们避免了循环中的乘法,并且执行了(更简单)+ = hash2。
* 我们还通过与Long.MAX_VALUE进行“与”操作而不是翻转位来将索引更改为正数
* @param value
* @return
*/
public 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;
}
/**
* 拿来用的
* 计算m(Bloom过滤器的总位),对于指定的预期插入,该m将达到所需的误报概率。
* n –预期插入(必须为正)
* p –误报率(必须为0 <p <1)
*/
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)));
}
/**
* 给定预期的插入次数和布隆过滤器中的位数,计算最佳k(布隆过滤器中插入的每个元素的哈希数)
* 参数:
* n –预期插入(必须为正)
* m –布隆过滤器中的总位数(必须为正)
*/
private int optimalNumOfHashFunctions(long n, long m) {
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
}
初始化:
@Bean
public BloomFilterHelper<String> initBloomFilterHelper() {
return new BloomFilterHelper<>((Funnel<String>) (from, into) ->
into.putString(from, Charsets.UTF_8).putString(from, Charsets.UTF_8),
1000000, 0.01);
}
如何使用:
/**
* 根据给定的布隆过滤器判断值是否存在
*
* @param bloomFilterHelper 布隆过滤器对象
* @param key KEY
* @param value 值
* @param <T> 泛型,可以传入任何类型的value
* @return 是否存在
*/
public <T> boolean includeByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
if (!redisTemplate.opsForValue().getBit(key, i)) {
return false;
}
}
return true;
}
代码位置:https://github.com/lizhifuabc/spring-learn/tree/main/spring-boot-redis-advanced
代码位置:https://github.com/lizhifuabc/spring-learn/tree/main/spring-boot-redis-redisson
package com.redis.redisson;
import org.junit.jupiter.api.Test;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;
import jakarta.annotation.Resource;
/**
* RedisBloomFilter
*
* @author lizhifu
* @date 2021/1/14
*/
@SpringBootTest
public class RedisBloomFilterTest {
@Resource
private RedissonClient redissonClient;
@Test
public void test(){
// 获取布隆过滤器
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("userList");
//初始化布隆过滤器:预计元素为1wL,误差率为0.01
bloomFilter.tryInit(10000,0.01);
for (int i = 0; i < 10000; i++) {
bloomFilter.add("testtest" + i);
}
int count = 0;
for (int i = 0; i < 100; i++) {
if (bloomFilter.contains("testtest" + i)) {
count++;
}
}
// 匹配数量 100
System.out.println("匹配数量 " + count);
}
}