看完这篇Redis缓存三大问题,保你能和面试官互扯。

  1. 将要添加的元素给m个哈希函数

  2. 得到对应于位数组上的m个位置

  3. 将这m个位置设为1

那么为什么会有误判率呢?

假设在我们多次存入值后,在布隆过滤器中存在x、y、z这三个值,布隆过滤器的存储结构图如下所示:

当我们要查询的时候,比如查询a这个数,实际中a这个数是不存在布隆过滤器中的,经过2个哈希函数计算后得到a的哈希值分别为2和13,结构原理图如下:

经过查询后,发现2和13位置所存储的值都为1,但是2和13的下标分别是x和z经过计算后的下标位置的修改,该布隆过滤器中实际不存在a,那么布隆过滤器就会误判改值可能存在,因为布隆过滤器不存
元素值 ,所以存在
误判率

那么具体布隆过布隆过滤的判断的准确率和一下 两个因素 有关:

  1. 布隆过滤器大小:越大,误判率就越小,所以说布隆过滤器一般长度都是非常大的。

  2. 哈希函数的个数:哈希函数的个数越多,那么误判率就越小。

那么为什么不能删除元素呢?

原因很简单,因为删除元素后,将对应元素的下标设置为零,可能别的元素的下标也引用改下标,这样别的元素的判断就会收到影响,原理图如下:

当你删除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在失效的瞬间,持续的 大并发 就穿破缓存,直接请求数据库,瞬间对数据库的访问压力增大。

缓存击穿这里强调的是 并发 ,造成缓存击穿的原因有以下两个:

  1. 该数据没有人查询过 ,第一次就大并发的访问。(冷门数据)

  2. 添加到了缓存,reids有设置数据失效的时间 ,这条数据刚好失效,大并发访问(热点数据)

对于缓存击穿的解决方案就是加锁,具体实现的原理图如下:

当用户出现
大并发 访问的时候,在查询缓存的时候和查询数据库的过程加锁,只能第一个进来的请求进行执行,当第一个请求把该数据放进缓存中,接下来的访问就会直接集中缓存,防止了
缓存击穿

业界比价普遍的一种做法,即根据key获取value值为空时,锁上,从数据库中 load 数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。这里要注意,分布式环境中要使用 分布式锁单机 的话用普通的锁( synchronizedLock )就够了。

下面以一个获取商品库存的案例进行代码的演示, 单机版 的锁实现具体实现的代码如下:

// 获取库存数量
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";
}

缓存雪崩

缓存雪崩 是指在某一个时间段,缓存集中过期失效。此刻无数的请求直接绕开缓存,直接请求数据库。

造成缓存雪崩的原因,有以下两种:

  1. reids宕机

  2. 大部分数据失效

比如天猫双11,马上就要到双11零点,很快就会迎来一波抢购,这波商品在23点集中的放入了缓存,假设缓存一个小时,那么到了凌晨24点的时候,这批商品的缓存就都过期了。

而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰,对数据库造成压力,甚至压垮数据库。

缓存雪崩的原理图如下,当正常的情况下,key没有大量失效的用户访问原理图如下:

当某一时间点,key大量失效,造成的缓存雪崩的原理图如下:

对于缓存雪崩的解决方案有以下两种: