看完这篇Redis缓存三大问题,保你能和面试官互扯。
-
将要添加的元素给m个哈希函数
-
得到对应于位数组上的m个位置
-
将这m个位置设为1
那么为什么会有误判率呢?
假设在我们多次存入值后,在布隆过滤器中存在x、y、z这三个值,布隆过滤器的存储结构图如下所示:
当我们要查询的时候,比如查询a这个数,实际中a这个数是不存在布隆过滤器中的,经过2个哈希函数计算后得到a的哈希值分别为2和13,结构原理图如下:
经过查询后,发现2和13位置所存储的值都为1,但是2和13的下标分别是x和z经过计算后的下标位置的修改,该布隆过滤器中实际不存在a,那么布隆过滤器就会误判改值可能存在,因为布隆过滤器不存
元素值 ,所以存在
误判率
。
那么具体布隆过布隆过滤的判断的准确率和一下 两个因素 有关:
-
布隆过滤器大小:越大,误判率就越小,所以说布隆过滤器一般长度都是非常大的。
-
哈希函数的个数:哈希函数的个数越多,那么误判率就越小。
那么为什么不能删除元素呢?
原因很简单,因为删除元素后,将对应元素的下标设置为零,可能别的元素的下标也引用改下标,这样别的元素的判断就会收到影响,原理图如下:
当你删除z元素之后,将对应的下标10和13设置为0,这样导致x和y元素的下标受到影响,导致数据的判断不准确,所以直接不提供删除元素的api。
以上说的都是布隆过滤器的原理,只有理解了原理,在实际的运用才能如鱼得水,下面就来实操代码,手写一个简单的布隆过滤器。
对于要手写一个布隆过滤器,首先要明确布隆过滤器的核心:
-
若干哈希函数
-
存值的Api
-
判断值得Api
实现的代码如下:
public class MyBloomFilter { // 布隆过滤器长度 private static final int SIZE = 2 << 10; // 模拟实现不同的哈希函数 private static final int[] num= new int[] {5, 19, 23, 31,47, 71}; // 初始化位数组 private BitSet bits = new BitSet(SIZE); // 用于存储哈希函数 private MyHash[] function = new MyHash[num.length]; // 初始化哈希函数 public MyBloomFilter() { for (int i = 0; i < num.length; i++) { function [i] = new MyHash(SIZE, num[i]); } } // 存值Api public void add(String value) { // 对存入得值进行哈希计算 for (MyHash f: function) { // 将为数组对应的哈希下标得位置得值改为1 bits.set(f.hash(value), true); } } // 判断是否存在该值得Api public boolean contains(String value) { if (value == null) { return false; } boolean result= true; for (MyHash f : func) { result= result&& bits.get(f.hash(value)); } return result; } }
哈希函数代码如下:
public static class MyHash { private int cap; private int seed; // 初始化数据 public MyHash(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 test { String value = "4243212355312"; MyBloomFilter filter = new MyBloomFilter(); System.out.println(filter.contains(value)); filter.add(value); System.out.println(filter.contains(value)); }
以上就是手写了一个非常简单的布隆过滤器,但是实际项目中可能由牛人或者大公司已经帮你写好的,如谷歌的 Google Guava
,只需要在项目中引入一下依赖:
com.google.guava guava 27.0.1-jre
实际项目中具体的操作代码如下:
public static void MyBloomFilterSysConfig { @Autowired OrderMapper orderMapper // 1.创建布隆过滤器 第二个参数为预期数据量10000000,第三个参数为错误率0.00001 BloomFilter bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf-8")),10000000, 0.00001); // 2.获取所有的订单,并将订单的id放进布隆过滤器里面 List orderList = orderMapper.findAll() for (Order order;orderList ) { Long id = order.getId(); bloomFilter.put("" + id); } }
在实际项目中会启动一个 系统任务 或者 定时任务 ,来初始化布隆过滤器,将热点查询数据的id放进布隆过滤器里面,当用户再次请求的时候,使用布隆过滤器进行判断,改订单的id是否在布隆过滤器中存在,不存在直接返回null,具体操作代码:
// 判断订单id是否在布隆过滤器中存在 bloomFilter.mightContain("" + id)
布隆过滤器的缺点就是要维持容器中的数据,因为订单数据肯定是频繁变化的,实时的要更新布隆过滤器中的数据为最新。
缓存击穿
缓存击穿是指一个 key
非常热点,在不停的扛着大并发, 大并发 集中对这一个点进行访问,当这个key在失效的瞬间,持续的 大并发 就穿破缓存,直接请求数据库,瞬间对数据库的访问压力增大。
缓存击穿这里强调的是 并发 ,造成缓存击穿的原因有以下两个:
-
该数据没有人查询过 ,第一次就大并发的访问。(冷门数据)
-
添加到了缓存,reids有设置数据失效的时间 ,这条数据刚好失效,大并发访问(热点数据)
对于缓存击穿的解决方案就是加锁,具体实现的原理图如下:
当用户出现
大并发 访问的时候,在查询缓存的时候和查询数据库的过程加锁,只能第一个进来的请求进行执行,当第一个请求把该数据放进缓存中,接下来的访问就会直接集中缓存,防止了
缓存击穿
。
业界比价普遍的一种做法,即根据key获取value值为空时,锁上,从数据库中 load
数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。这里要注意,分布式环境中要使用 分布式锁 , 单机 的话用普通的锁( synchronized
、 Lock
)就够了。
下面以一个获取商品库存的案例进行代码的演示, 单机版 的锁实现具体实现的代码如下:
// 获取库存数量 public String getProduceNum(String key) { try { synchronized (this) { //加锁 // 缓存中取数据,并存入缓存中 int num= Integer.parseInt(redisTemplate.opsForValue().get(key)); if (num> 0) { //没查一次库存-1 redisTemplate.opsForValue().set(key, (num- 1) + ""); System.out.println("剩余的库存为num:" + (num- 1)); } else { System.out.println("库存为0"); } } } catch (NumberFormatException e) { e.printStackTrace(); } finally { } return "OK"; }
分布式的锁实现具体实现的代码如下:
public String getProduceNum(String key) { // 获取分布式锁 RLock lock = redissonClient.getLock(key); try { // 获取库存数 int num= Integer.parseInt(redisTemplate.opsForValue().get(key)); // 上锁 lock.lock(); if (num> 0) { //减少库存,并存入缓存中 redisTemplate.opsForValue().set(key, (num - 1) + ""); System.out.println("剩余库存为num:" + (num- 1)); } else { System.out.println("库存已经为0"); } } catch (NumberFormatException e) { e.printStackTrace(); } finally { //解锁 lock.unlock(); } return "OK"; }
缓存雪崩
缓存雪崩 是指在某一个时间段,缓存集中过期失效。此刻无数的请求直接绕开缓存,直接请求数据库。
造成缓存雪崩的原因,有以下两种:
-
reids宕机
-
大部分数据失效
比如天猫双11,马上就要到双11零点,很快就会迎来一波抢购,这波商品在23点集中的放入了缓存,假设缓存一个小时,那么到了凌晨24点的时候,这批商品的缓存就都过期了。
而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰,对数据库造成压力,甚至压垮数据库。
缓存雪崩的原理图如下,当正常的情况下,key没有大量失效的用户访问原理图如下:
当某一时间点,key大量失效,造成的缓存雪崩的原理图如下:
对于缓存雪崩的解决方案有以下两种: