如果想要判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较确定。链表
,树等等数据结构都是这种思路. 但是随着集合中元素的增加,我们需要的存储空间越来越大,检索速度也越来越慢(O(n),O(logn))。不过世界上还有一种叫作散列表(又叫哈希表
,Hash table)的数据结构。它可以通过一个Hash函数
将一个元素映射成一个位阵列(Bit array)中的一个点。这样一来,我们只要看看这个点是不是1就可以知道集合中有没有它了。这就是布隆过滤器的基本思想。
Hash 面临的问题就是冲突。假设Hash函数是良好的,如果我们的位阵列长度为m个点,那么如果我们想将冲突率降低到例如 1%, 这个散列表就只能容纳m / 100个元素。显然这就不叫空间效率了(Space-efficient)了。解决方法也简单,就是使用多个Hash,如果它们有一个说元素不在集合中,那肯定就不在。如果它们都说在,虽然也有一定可能性它们在说谎,不过直觉上判断这种事情的概率
是比较低的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 import java.util.ArrayList;import java.util.BitSet;import java.util.List; public class BloomFilter { private static final int DEFAULT_SIZE = 2 << 24 ; private static final int [] seeds = new int [] { 5 , 7 , 11 , 13 , 31 , 37 , 61 }; private BitSet bits = new BitSet (DEFAULT_SIZE); private SimpleHash[] func = new SimpleHash [seeds.length]; public BloomFilter () { for (int i = 0 ; i < seeds.length; i++) { func[i] = new SimpleHash (DEFAULT_SIZE, seeds[i]); } } public void add (String value) { for (SimpleHash f : func) { bits.set(f.hash(value), true ); } } public boolean contains (String value) { if (value == null ) { return false ; } boolean ret = true ; for (SimpleHash f : func) { ret = ret && bits.get(f.hash(value)); } return ret; } public static class SimpleHash { private int cap; private int seed; public SimpleHash (int cap, int seed) { this .cap = cap; this .seed = seed; } public int hash (String value) { int result = 0 ; int len = value.length(); for (int i = 0 ; i < len; i++) { result = seed * result + value.charAt(i); } return (cap - 1 ) & result; } } public static void main (String[] args) { BloomFilter bf = new BloomFilter (); List<String> strs = new ArrayList <String>(); strs.add("123456" ); strs.add("hello word" ); strs.add("transDocId" ); strs.add("123456" ); strs.add("transDocId" ); strs.add("hello word" ); strs.add("test" ); for (int i=0 ;i<strs.size();i++) { String s = strs.get(i); boolean bl = bf.contains(s); if (bl){ System.out.println(i+"," +s); }else { bf.add(s); } } } }
布隆过滤器的应用 黑名单校验 发现存在黑名单中的,就执行特定操作。比如:识别垃圾邮件,只要是邮箱在黑名单中的邮件,就识别为垃圾邮件。假设黑名单的数量是数以亿计的,存放起来就是非常耗费存储空间的,布隆过滤器则是一个较好的解决方案。把所有黑名单都放在布隆过滤器中,再收到邮件时,判断邮件地址是否在布隆过滤器中即可。
解决缓存穿透的问题 一般情况下,先查询缓存是否有该条数据,缓存中没有时,再查询数据库。当数据库也不存在该条数据时,每次查询都要访问数据库,这就是缓存穿透。缓存穿透带来的问题是,当有大量请求查询数据库不存在的数据时,就会给数据库带来压力,甚至会拖垮数据库。
可以使用布隆过滤器解决缓存穿透的问题,把已存在数据的key存在布隆过滤器中。当有新的请求时,先到布隆过滤器中查询是否存在,如果不存在该条数据直接返回;如果存在该条数据再查询缓存查询数据库。
Redisson实现布隆过滤器在亿级流量系统的应用 **场景:**正常redis有1000条缓存数据,忽然遭到爬虫/流量攻击攻击,大量不存在的于redis的数据批量查询, 由于redis不存在这些数据,会到数据库进行查询。由于数据库对于瞬时高并发访问的承载能力弱,所以可能 对数据库造成影响,甚至崩溃。 绕过redis服务器进入数据库查询的方式叫做缓存穿透。
缓存穿透攻击: 指恶意用户在短时间内大量查询不存在的数据,导致大量请求在被送达数据库进行查询, 当请求数量超过数据库负载上限时,使系统响应出现高延迟甚至瘫痪的行为。
如何预防缓存穿透?布隆过滤器。 布隆过滤器:由一个很长的二进制向量和一系列随机映射函数组成,可以用于检索一个元素是否在一个集合中。 特点: 1.一定的误识别率(即某一元素存在于某集合中,但是实际上该元素并不在集合中) 2.但是没有识别错误的情形(即如果某个元素确实没有在该集合中,那么Bloom Filter 是不会报告该元素存在于集合中的,所以不会漏报)。 也就是:我说你不在你就一定不在,我说在,可能不在。 3.删除困难
了解hash: Hash (散列函数):是把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值。 由于这种转换,可能存在 不同的输入可能会散列成相同的输出。 Hash算法可以将一个数据转换为一个标志,这个标志,就是视频说的hash,hash后若不存在,就由0变为1。存在,不变。
视频hash1(858),hash2(858),hash3(858)的原理是: Hash面临的问题就是冲突。假设 Hash 函数是良好的,如果我们的位阵列长度为 m 个点,那么如果我们想将冲突率降低到例如 1%, 这个散列表就只能容纳 m/100 个元素。显然这就不叫空间有效了(Space-efficient)。 解决方法也简单,就是使用多个 Hash,如果它们有一个说元素不在集合中,那肯定就不在。如果它们都说在,虽然也有一定可能性它们在说谎,不过直觉上判断这种事情的概率是比较低的。 (我搬运的,前半句我看不懂,只看懂后半句,理解就是:hash有冲突的可能,多个hash减少冲突率。原来1个说在,但可能不在。若2个,1个说在,另一个说不在,就肯定不在)
减少误判的措施: 1.增加二进制数组位数。 2.增加Hash次数。
开发中如何使用? redisson组件内置布隆过滤器,导入直接使用。
项目中如何使用: 应用启动时初始化布隆过滤器(将商品在布隆过滤器中先从0转换成1) 用户请求时判断布隆过滤器是否有编号,若布隆过滤器有,读取redis,若redis没有,则读取数据库,载入redis缓存。 若布隆过滤器没有,返回数据不存在消息。
假如布隆过滤器初始化后,有商品被删除了, 怎么办? 布隆过滤器某一位二进制可能被多个编号hash引用(看hash的特点:不同的输入可能会散列成相同的输出) 所以,不能直接处理删除布隆过滤器的数据。 解决方案: 1.定时异步重建布隆过滤器 2.计数BloomFilter(可以知道该位被几个引用)
我一开始觉得,删就删了呗,管他干嘛,但是不管他,冲突率会高,处理后,这个数据就不会再引起冲突。 即:A商品被删后,当你查询A商品时,布隆过滤器说他在(布隆过滤器特点:我说你不在你就一定不在,我说在,可能不在),那我就要去redis查,但实际数据库查不到这个A商品,redis也没有。 这就存在冲突了。如果处理之后,布隆过滤器说不在就不在。
Redisson 1.引入maven依赖
1 2 3 4 5 <dependency > <groupId > org.redisson</groupId > <artifactId > redisson</artifactId > <version > 3.13.1</version > </dependency >
2、关于redis的配置
redis配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 redis.timeout =10000 redis.maxIdle =300 redis.maxTotal =2000 redis.maxWaitMillis =1000 redis.nodes =192.168.25.128:7000,192.168.25.128:7001,192.168.25.128:7002,192.168.25.128:7003,192.168.25.128:7004,192.168.25.128:7005,192.168.25.128:7006,192.168.25.128:7007
3、配置文件的书写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Configuration @PropertySource("classpath:conf/redis.properties") public class RedisConfig { @Value("${redis.nodes}") private String clusterNodes; @Bean public RedissonClient getBloomFilter () { Config config = new Config (); ClusterServersConfig clusterServersConfig = config.useClusterServers(); String[] cNodes = clusterNodes.split("," ); for (String node : cNodes) { clusterServersConfig.addNodeAddress("redis://" + node); } return Redisson.create(config); } }
4、service的的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public interface RedissonBloomFilterService { boolean initNewFilter (String key, Long expireDate, TimeUnit timeUnit, long expectedInsertions, double falseProbability) ; boolean add (final String key, final String value) ; boolean add (final String key, final List<String> list) ; boolean containsValue (final String key, final String value) ; boolean containKey (final String key) ; }
实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 @Slf4j @Service public class RedissonBloomServiceImpl implements RedissonBloomFilterService { public static String prefix = "redission_bf_" ; private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd" ); private static final int TIME_FORMAT_LENGTH = 8 ; @Resource private RedissonClient redissonClient; private Map<String, ConcurrentHashMap<String, RBloomFilter>> bloomFilterMap = new ConcurrentHashMap <>(); @Override public synchronized boolean initNewFilter (String key, Long expireDate, TimeUnit timeUnit, long expectedInsertions, double falseProbability) { String timeFormatter = formatter.format(LocalDateTime.now()); RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(prefix + timeFormatter + key); boolean isSuccess = bloomFilter.tryInit(expectedInsertions, falseProbability); if (isSuccess) { bloomFilter.expire(expireDate, timeUnit); bloomFilter.addListener(new ExpiredObjectListener () { @Override public void onExpired (String key) { log.info("onExpired callback running key is {}" , key); if (key.startsWith(prefix)) { String firstKey = key.substring(prefix.length() + TIME_FORMAT_LENGTH); ConcurrentHashMap<String, RBloomFilter> map = bloomFilterMap.get(firstKey); if (Objects.isNull(map)) { log.warn("listener callback key is empty key is {}" , key); return ; } map.remove(key); } } }); ConcurrentHashMap<String, RBloomFilter> map = bloomFilterMap.computeIfAbsent(key, (k) -> { return new ConcurrentHashMap <String, RBloomFilter>(4 ); }); map.put(prefix + timeFormatter + key, bloomFilter); isSuccess = true ; }else { log.error("initNewFilter fail key is {}" , key); } return isSuccess; } @Override public boolean add (String key, String value) { String timeFormatter = formatter.format(LocalDateTime.now()); ConcurrentHashMap<String, RBloomFilter> map = bloomFilterMap.get(key); if (Objects.isNull(map)) { log.error("add name key one value is null, name is {},value is {}" , key, value); return false ; } RBloomFilter rBloomFilter = map.get(prefix + timeFormatter + key); if (Objects.isNull(rBloomFilter)) { log.error("add name key one value is null, name is {},redis key is {},value is {}" , key, prefix + timeFormatter + key, value); return false ; } return rBloomFilter.add(value); } @Override public boolean add (String key, List<String> list) { String timeFormatter = formatter.format(LocalDateTime.now()); ConcurrentHashMap<String, RBloomFilter> map = bloomFilterMap.get(key); if (Objects.isNull(map)) { log.error("add name key list value is null, name is {}" , key); return false ; } RBloomFilter rBloomFilter = map.get(prefix + timeFormatter + key); if (Objects.isNull(rBloomFilter)) { log.error("add name key list value is null, name is {},redis key is {}" , key,prefix + timeFormatter + key); return false ; } list.forEach(value -> rBloomFilter.add(value)); return true ; } @Override public boolean containsValue (String key, String value) { ConcurrentHashMap<String, RBloomFilter> map = bloomFilterMap.get(key); if (Objects.isNull(map)) { log.error("containsValue key is exist, key is {}" , key); return false ; } for (Map.Entry<String, RBloomFilter> entry : map.entrySet()) { if (entry.getValue().contains(value)) { return true ; } } return false ; } @Override public boolean containKey (String key) { return bloomFilterMap.containsKey(key); } }
测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Test public void testRedissonBloom () throws InterruptedException { redissonBloomFilterService.initNewFilter("bloomkey1" , 60L , SECONDS, 50 , 0.1 ); redissonBloomFilterService.initNewFilter("bloomkey2" , 60L , SECONDS, 50 , 0.1 ); for (int i = 0 ; i < 50 ; i++) { redissonBloomFilterService.add("bloomkey1" , "" + i); if (i > 30 ) { redissonBloomFilterService.add("bloomkey2" , "" + i); } } int j1 = 0 ; int j2 = 0 ; for (int i = 0 ; i < 100 ; i++) { boolean exists = redissonBloomFilterService.containsValue("bloomkey1" , "" + i); if (exists) { System.out.println("bloomkey1有误判" + (++j1) + "i是" + i); } boolean exists2 = redissonBloomFilterService.containsValue("bloomkey2" , "" + i); if (exists2) { System.out.println("bloomkey2" + "有误判" + (++j2) + "i是" + i); } } CountDownLatch beginCount = new CountDownLatch (1 ); beginCount.await(); }