/** * 缓存击穿 * @author * */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"classpath:config/spring/spring-dao.xml", "classpath:config/spring/spring-bean.xml", "classpath:config/spring/spring-redis.xml"}) public class CacheBreakDownTest { private static final Logger logger = LoggerFactory.getLogger(CacheBreakDownTest.class); private static final int THREAD_NUM = 100;//线程数量 @Resource private UserDao UserDao; @Resource private RedisTemplate redisTemplate; private int count = 0; //初始化一个计数器 private CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM); private BloomFilter<String> bf; List<UserDto> allUsers; @PostConstruct public void init(){ //将数据从数据库导入到本地 allUsers = UserDao.getAllUser(); if(allUsers == null || allUsers.size()==0){ return; } //创建布隆过滤器(默认3%误差) bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), allUsers.size()); //将数据存入布隆过滤器 for(UserDto userDto : allUsers){ bf.put(userDto.getUserName()); } } @Test public void cacheBreakDownTest(){ for(int i=0;i<THREAD_NUM;i++){ new Thread(new MyThread()).start(); //计数器减一 countDownLatch.countDown(); } try { Thread.currentThread().join(); } catch (InterruptedException e) { e.printStackTrace(); } } class MyThread implements Runnable{ @Override public void run() { try { //所有子线程等待,当子线程全部创建完成再一起并发执行后面的代码 countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } //随机产生一个字符串 String randomUser = UUID.randomUUID().toString(); // String randomUser = allUsers.get(new Random().nextInt(allUsers.size())).getUserName(); String key = "Key:"+randomUser; //如果布隆过滤器中不存在这个用户直接返回,将流量挡掉 if(!bf.mightContain(randomUser)){ System.out.println("bloom filter don't has this user"); return; } //查询缓存,如果缓存中存在直接返回缓存数据 ValueOperations<String,String> operation = (ValueOperations<String, String>) redisTemplate.opsForValue(); synchronized (countDownLatch) { Object cacheUser = operation.get(key); if(cacheUser!=null){ System.out.println("return user from redis"); return; } //如果缓存不存在查询数据库 List<UserDto> user = UserDao.getUserByUserName(randomUser); if(user == null || user.size() == 0){ return; } //将mysql数据库查询到的数据写入到redis中 System.out.println("write to redis"); operation.set("Key:"+user.get(0).getUserName(), user.get(0).getUserName()); } } } }
demo2
@RunWith(SpringRunner.class) @SpringBootTest public class BloomFilterTest { private BloomFilter<Integer> bloomFilter; private int size = 1000000; @Before public void init(){ //不设置第三个参数时,误判率默认为0.03 //bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size); //进行误判率的设置,自动计算需要几个hash函数。bit数组的长度与size和fpp参数有关 //过滤器内部会对size进行处理,保证size为2的n次幂。 bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, 0.01); for(int i = 0; i < size; i++){ bloomFilter.put(i); } } @Test public void testBloomFilter(){ for(int i = 0; i < size; i++){ if(!bloomFilter.mightContain(i)){ //不会打印,因为不存在的情况不会出现误判 System.out.println("不存在的误判" + i); } } List<Integer> list = new ArrayList<>(1000); for (int i = size + 10000; i < size + 20000; i++) { if (bloomFilter.mightContain(i)) { list.add(i); } } //根据设置的误判率 System.out.println("存在的误判数量:" + list.size()); } }
布隆过滤器有以下应用场景:
1、黑名单,比如邮件黑名单过滤器,判端邮件地址是否在黑名单中。
2、网络爬虫,判端url是否已经被爬取过。
3、首次访问,判端访问网站的IP是否是第一次访问。
4、缓存击穿,防止非法攻击,频繁发送无法命中缓存的请求,导致缓存击穿,最总引起缓存雪崩。
5、检查英文单词是否拼写正确。
6、K-V系统快速判断某个key是否存在,典型的例子有Hbase,Hbase的每个Region中都包含一个BloomFilter,用于在查询时快速判断某个key在该region中是否存在,如果不存在,直接返回,节省掉后续的查询。
扩展,如何让布隆过滤器支持删除。
进行计数删除,但是计数删除需要存储一个数值,而不是原先的 bit 位,会增大占用的内存大小。这样的话,增加一个值就是将对应索引槽上存储的值加一,删除则是减一,判断是否存在则是看值是否大于0。