1.再看Redis的布隆过滤器之前,先看看其他几位大佬的博客。
《转载—张维鹏》什么是散列码,什么是hashCode()
简单的总结一下:
hashCode就是对象的散列码。
散列的作用在于速度。
线性查询是最慢的查询方法。所以散列将键保存在某处,以便能够很快找到。存储一组元素最快的数据结构是数组,所以使用它来表示键的信息。
数组并不保存键本身,而是通过键对象生成一个数字,将其作为数组的下标,这个数字就是散列码。
听起来就像HashMap一样,其实就是HashMap实现原理。
String字符串的hashCode()相同而StringBuilder 对象的hashCode()不同。取决于是否重写了hashCode()方法,因为如果没有重写hashCode(),那么会使用Obkect()类的hashCode(),而Obhect.hashCode()永远计算的是对象的存储地址,而非内容。
2.使用场景,布隆过滤器判可以准确断该数据不存在,或者存在但有可能不存在的场景。
例如: 某个网站有1000万的用户,防止有人恶意刷登陆接口,导致数据库无法承受大量的并发,而被击穿.。
解决: 使用Redis做缓存将1000王的用户信息放入Redis的set中。。。好像有点大
布隆过滤器: Bloom filter是由Howard Bloom在1970年提出的二进制向量数据结构,它具有空间和时间效率,被用来检测一个元素是不是集合中的一个成员。
3.以精度换空间
优势
1.全量存储但是不存储元素本身
2.空间高效率
3.插入/查询时间都是常数O(k),效率高
用户a的唯一id存储入布隆过滤器。经过计算a存储到下标0,5,9998的存储位置
用户b的唯一id存储入布隆过滤器。经过计算b存储到下标是1,3,9999的存储位置
劣势:
用户c的唯一id存储入布隆过滤器。经过计算c存储到下标是0,5,9999的存储位置.。
1.存在误算率
但是0,5,9999的位置已经被a用户和b用户存储过了,不管c用户是合法用户还是非法用户都会通过我们布隆过滤器的判断,而且随着存入的元素数量增加,误算率随之增加。
我们能做的,就是尽可能将容器扩大和增加散列码的散列程度,减少散列码的碰撞几率。
2.无法从中删除莫一个用户。因为我们无法判断当前存储位置是否有多个用户使用,只有全部删除重新写入。
4.如何计算可以减少存储位置的碰撞率
《demo转载— 负债程序猿》
该博主使用的是Hash 算法(扰动函数)减少碰撞的概率。
5.我们以hashMap的put操作为例子
先上源码:
//创建一个hashMap
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(1,1);
//put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//get方法
public V get(Object key) {
HashMap.Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//获取hash方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//向桶中设置一个值
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
//put 和 get的时候 获取节点的方法
final HashMap.Node<K,V> getNode(int hash, Object key) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof HashMap.TreeNode)
return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
1.我们 hashMap.put(1,1);时如何保证这个key可以最理想化的分布在我们的桶中,(如何最大散列程度)
看下put()方法中的hash(key)方法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2.h = key.hashCode()) ^ (h >>> 16) 这是hash()方法拿到key的hashCode然后^ (h >>> 16);
但是在putVal()中,在hash()方法上有了 p = tab[i = (n - 1) & hash]
最终就是p为该key的最终位置:
为什么p需要这么计算 :p = tab[i = (n - 1) & (key.hashCode()) ^ (h >>> 16)])
n = tab.length 就是hashMap桶的大小 默认16 为什么需要减一
log.info("桶数为2^n: hashde的值为{} ", 16 & "hash".hashCode());
log.info("桶数为2^n: PUBG的值为{} ", 16 & "PUBG".hashCode());
log.info("桶数为2^n: 零度之下的值为{} ", 16 & "零度之下".hashCode());
log.info("桶数为2^n-1: hashde的值为{} ", 15 & "hash".hashCode());
log.info("桶数为2^n-1: PUBG的值为{} ", 15 & "PUBG".hashCode());
log.info("桶数为2^n-1: 零度之下的值为{} ", 15 & "零度之下".hashCode());
Console
14:30:51.053 [main] INFO com.hdh.test.test - 桶数为2^n: hashde的值为0
14:30:51.057 [main] INFO com.hdh.test.test - 桶数为2^n: PUBG的值为0
14:30:51.057 [main] INFO com.hdh.test.test - 桶数为2^n: 零度之下的值为16
14:30:51.057 [main] INFO com.hdh.test.test - 桶数为2^n-1: hashde的值为14
14:30:51.057 [main] INFO com.hdh.test.test - 桶数为2^n-1: PUBG的值为10
14:30:51.057 [main] INFO com.hdh.test.test - 桶数为2^n-1: 零度之下的值为0
不管是 (n - 1) & hash 还是 (key.hashCode()) ^ (h >>> 16)]) 都是为了散列,让我们的key可以更加均匀的散列在桶中
减少碰撞因为
2^ 4=16=10000B 因为 2^n 10000& 任意一个数要么0要么是2^n 而且大小16的 ,而 2^ 4-1=15=1111B 就有更多种可能。
数组没有下标16的位置。
6.Linux安装redis和Bloom
//或者自己去官网下tar
wget http://download.redis.io/releases/redis-4.0.9.tar.gz
tar xzvf redis-4.0.8.tar.gz
make
make install PREFIX=/usr/local/redis
手动cp redis.config到你自己的安装目录下
修改配置文件自后台启动,注释掉#bind 127.0.0.1
protected-mode no
daemonize yes
启动服务端./redis.server redis.config
启动客户端 ./redis.cli -p 6379 [-a (pwd) 可不写]
详细springBoot整合redis看这
RedisBloom安装
//或者自己去官网下tar
wget https://github.com/RedisBloom/RedisBloom/archive/v2.0.3.tar.gz
make
修改redis.conf
loadmodule /opt/apps/RedisBloom-2.0.3/redisbloom.so
自己测试一下
127.0.0.1:6379> bf.add key1 val1
安装完成
7.springBoot整合redie和RedisBloom(自己整理的将就着看)
Redis.config 序列化RedisTemplate和注册布隆过滤器
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> myRedisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//string的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//key采用string的序列化
redisTemplate.setKeySerializer(stringRedisSerializer);
//hash的key也采用string的序列化
redisTemplate.setHashKeySerializer(stringRedisSerializer);
//value序列化的方式也采用jackson
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
//hash的value序列化方式采用jackson
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
//初始化布隆过滤器,放入到spring容器里面
@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);
}
}
/**
* @description: 在redis中设置 判断是否存在
* @author:hdh
* @date:2021-07-05 15:09
**/
@Component
public class RedisBloomFilterUtils {
@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 + " " + "value : " + i);
if (!redisTemplate.opsForValue().getBit(key, i)) {
return false;
}
}
return true;
}
}
Bloom过滤器设置
public class BloomFilterHelper<T> {
private int numHashFunctions;
private int bitSize;
private Funnel<T> funnel;
public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) {
// Preconditions里面的方法:
//
// 1 .checkArgument(boolean) :
// 功能描述:检查boolean是否为真。 用作方法中检查参数
// 失败时抛出的异常类型: IllegalArgumentException
//
// 2.checkNotNull(T):
// 功能描述:检查value不为null, 直接返回value;
// 失败时抛出的异常类型:NullPointerException
//
// 3.checkState(boolean):
// 功能描述:检查对象的一些状态,不依赖方法参数。 例如, Iterator可以用来next是否在remove之前被调用。
// 失败时抛出的异常类型:IllegalStateException
//
// 4.checkElementIndex(int index, int size):
// 功能描述:检查index是否为在一个长度为size的list, string或array合法的范围。 index的范围区间是[0, size)(包含0不包含size)。无需直接传入list, string或array, 只需传入大小。返回index。
// 失败时抛出的异常类型:IndexOutOfBoundsException
//
//
// 5.checkPositionIndex(int index, int size):
// 功能描述:检查位置index是否为在一个长度为size的list, string或array合法的范围。 index的范围区间是[0, size)(包含0不包含size)。无需直接传入list, string或array, 只需传入大小。返回index。
// 失败时抛出的异常类型:IndexOutOfBoundsException
//
// 6.checkPositionIndexes(int start, int end, int size):
// 功能描述:检查[start, end)是一个长度为size的list, string或array合法的范围子集。伴随着错误信息。
// 失败时抛出的异常类型:IndexOutOfBoundsException
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)));
}
}