• Redis缓存击穿、缓存穿透、缓存雪崩


    https://blog.csdn.net/CSDN2497242041/article/details/122531814
    Redis缓存击穿、缓存穿透、缓存雪崩

    前言:设计一个Redis缓存系统,不得不要考虑的问题就是:缓存穿透、缓存击穿与失效时的雪崩效应。先来看一个常见的缓存使用方式:读请求来了,先查下缓存,缓存有值命中,就直接返回;缓存没命中,就去查数据库,然后把数据库的值更新到缓存,再返回。


      一、缓存穿透

    缓存穿透是指缓存和数据库中都没有数据,用户请求的数据在缓存中没有命中,同时在数据库中也不存在,这样不会更新缓存,导致用户每次请求这个不存在数据都要到数据库中去查询。

    通俗点说,读请求访问时,缓存和数据库都没有某个值,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这样就会导致每次对这个值的查询请求都会穿透到数据库,这就是缓存穿透。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。比如发起id为“-1”的数据或id为特别大不存在的数据,这时的用户很可能是攻击者,攻击会导致数据库压力过大。

    缓存穿透一般都是这几种情况产生的:

    • 业务不合理的设计:比如大多数用户都没开守护,但是你的每个请求都去缓存,查询某个userid查询有没有守护。

    • 业务/运维/开发失误的操作:比如缓存和数据库的数据都被误删除了。

    • 黑客非法请求攻击:比如黑客故意捏造大量非法请求,读取不存在的业务数据。

    如何避免缓存穿透呢? 一般有三种方法:

    • ​​​接口参数校验:如果是非法请求,我们在API入口,对参数进行校验,过滤非法值;

    • 返回空对象:如果缓存未命中并且查询数据库也为空,我们可以给缓存设置个空值或者默认值,这样下次请求该key时直接从缓存中查询返回空对象,请求不会落到持久层数据库。但是如有有写请求进来的话,需要更新缓存以保证缓存一致性,同时,为了避免存储过多空对象,最后给缓存设置适当的过期时间;(业务上比较常用,简单有效)

    • 布隆过滤器:使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库。布隆过滤器的巨大用处就是,能够迅速判断一个元素是否在一个集合中。布隆过滤器认为不在的,一定不会在集合中;布隆过滤器认为在的,可能在也可能不在集合中。


    二、缓存击穿

    缓存击穿是指缓存中没有但数据库中有的数据,大量的请求同时查询一个热点 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去。缓存击穿危害就是数据库瞬时压力骤增,造成大量请求阻塞。

    缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。缓存击穿和缓存雪崩看着有点像,缓存雪崩是指数据库压力过大甚至down机,缓存击穿只是大量并发请求到了DB数据库层面,可以认为缓存击穿是缓存雪崩的一个子集吧。

    解决方案有两种:

    • 使用互斥锁方案:在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。这种思路比较简单,就是让一个线程回写缓存,其他线程等待回写缓存线程执行完,重新读缓存即可。无论是使用“分布式锁”,还是“JVM 锁”,加锁时要按 key 维度去加锁。

    使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这里要注意,分布式环境中要使用分布式锁单机的话用普通的锁(synchronizedLock)就够了。

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

    1. // 获取库存数量
    2. public String getProduceNum(String key) {
    3. try {
    4. synchronized (this) { //加锁
    5. // 缓存中取数据,并存入缓存中
    6. int num= Integer.parseInt(redisTemplate.opsForValue().get(key));
    7. if (num> 0) {
    8. //没查一次库存-1
    9. redisTemplate.opsForValue().set(key, (num- 1) + "");
    10. System.out.println("剩余的库存为num:" + (num- 1));
    11. } else {
    12. System.out.println("库存为0");
    13. }
    14. }
    15. } catch (NumberFormatException e) {
    16. e.printStackTrace();
    17. } finally {
    18. }
    19. return "OK";
    20. }

    分布式的锁实现具体实现的代码如下:

    1. public String getProduceNum(String key) {
    2. // 获取分布式锁
    3. RLock lock = redissonClient.getLock(key);
    4. try {
    5. // 获取库存数
    6. int num= Integer.parseInt(redisTemplate.opsForValue().get(key));
    7. // 上锁
    8. lock.lock();
    9. if (num> 0) {
    10. //减少库存,并存入缓存中
    11. redisTemplate.opsForValue().set(key, (num - 1) + "");
    12. System.out.println("剩余库存为num:" + (num- 1));
    13. } else {
    14. System.out.println("库存已经为0");
    15. }
    16. } catch (NumberFormatException e) {
    17. e.printStackTrace();
    18. } finally {
    19. //解锁
    20. lock.unlock();
    21. }
    22. return "OK";
    23. }
    • 热点数据永不过期(软过期):直接将缓存设置为不过期,把过期时间存在key对应的value里,然后由定时任务去异步线程加载数据更新缓存。这种方式适用于比较极端的场景,例如流量特别特别大的场景,使用时需要考虑业务能接受数据不一致的时间,还有就是异常情况的处理,不要到时候缓存刷新不上,一直是脏数据,那就凉了。

    软过期指对缓存中的数据设置失效时间,就是不使用缓存服务提供的过期时间,而是业务层在数据中存储过期时间信息,由业务程序判断是否过期并更新,在发现了数据即将过期时,将缓存的时效延长,程序可以派遣一个线程去数据库中获取最新的数据,其他线程这时看到延长了的过期时间,就会继续使用旧数据,等派遣的线程获取最新数据后再更新缓存;也可以通过定时任务异步更新服务来更新设置软过期的缓存,这样应用层就不用关心缓存击穿的问题了。


    三、缓存雪崩

    缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,请求直接落到数据库上,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

    缓存雪崩其实有点像“升级版的缓存击穿”,缓存击穿是一个热点 key,缓存雪崩是一组热点 key。

    解决方案有二种:

    • 设置不同的过期时间缓存雪崩一般是由于大量数据同时过期造成的,可以给缓存的过期时间加上一个随机值时间,使得每个 key 的过期时间离散分布开来,防止同一时间内大量的key失效。比如采用一个较大固定值+一个较小的随机值,5小时—0到1800秒。

    • 搭建高可用的Redis集群:Redis 故障宕机也可能引起缓存雪崩,此时就需要构造Redis高可用集群了。


    四、缓存预热

    4.1、什么是缓存预热?

    缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统,这样就可以避免在用户请求的时候,先查询数据库,然后再将数据回写到缓存。

    如果不进行预热, 那么 Redis 初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。

    4.2、缓存预热的操作方法

    • 数据量不大的时候,工程启动的时候进行加载缓存动作;

    • 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;

    • 数据量太大的时候,优先保证热点数据进行提前加载到缓存。


     五、布隆过滤器

    布隆过滤器(Bloom Filter,简称BF)由Burton Howard Bloom在1970年提出,是一种空间效率高的概率型数据结构。布隆过滤器专门用来检测集合中是否存在特定的元素。

    如果在平时我们要判断一个元素是否在一个集合中,通常会采用查找比较的方法,下面分析不同的数据结构查找效率:

    • 采用线性表存储,查找时间复杂度为O(N)

    • 采用平衡二叉排序树(AVL、红黑树)存储,查找时间复杂度为O(logN)

    • 采用哈希表存储,考虑到哈希碰撞,整体时间复杂度也要O[log(n/m)]

    当需要判断一个元素是否存在于海量数据集合中,不仅查找时间慢,还会占用大量存储空间。接下来看一下布隆过滤器如何解决这个问题。

    5.1、布隆过滤器设计思想

    布隆过滤器由一个长度为m比特的位数组(bit array)与k个哈希函数(hash function)组成的数据结构。位数组初始化均为0,所有的哈希函数都可以分别把输入数据尽量均匀地散列。

    当要向布隆过滤器中插入一个元素时,该元素经过k个哈希函数计算产生k个哈希值,以哈希值作为位数组中的下标,将所有k个对应的比特值由0置为1;

    当要查询一个元素时,同样将其经过哈希函数计算产生哈希值,然后检查对应的k个比特值:如果有任意一个比特为0,表明该元素一定不在集合中;如果所有比特均为1,表明该集合有可能性在集合中。为什么不是一定在集合中呢?因为不同的元素计算的哈希值有可能一样,会出现哈希碰撞,导致一个不存在的元素有可能对应的比特位为1,这就是所谓“假阳性”(false positive)。相对地,“假阴性”(false negative)在BF中是绝不会出现的。

    总结一下:布隆过滤器认为不在的,一定不会在集合中;布隆过滤器认为在的,可能在也可能不在集合中。

    举个例子:下图是一个布隆过滤器,共有18个比特位,3个哈希函数。集合中三个元素x,y,z通过三个哈希函数散列到不同的比特位,并将比特位置为1。当查询元素w时,通过三个哈希函数计算,发现有一个比特位的值为0,可以肯定认为该元素不在集合中。

     5.2、布隆过滤器优缺点

    优点:

    • 节省空间:不需要存储数据本身,只需要存储数据对应hash比特位

    • 时间复杂度低:插入和查找的时间复杂度都为O(k),k为哈希函数的个数

    缺点:

    • 存在假阳性:布隆过滤器判断存在,可能出现元素不在集合中;判断准确率取决于哈希函数的个数

    • 不能删除元素:如果一个元素被删除,但是却不能从布隆过滤器中删除,这也是造成假阳性的原因了

    5.3、布隆过滤器适用场景

    • 爬虫系统url去重:网页爬虫对URL的去重,避免爬取相同的URL地址

    • 垃圾邮件过滤:从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱

    • 黑名单


    参考链接:

    一张图搞懂 Redis 缓存雪崩、缓存穿透、缓存击穿

    布隆过滤器的方式解决缓存穿透问题

    缓存穿透、缓存击穿、缓存雪崩的解决方案 - 简书

  • 相关阅读:
    if——while表达式详解
    java算法:抽象数据类型ADT
    java算法:FIFO队列
    Android_NetworkInfo以及判断手机是否联网
    java算法:堆栈ADT及实例
    java算法:数据项
    java算法:一流的ADT
    java算法:复合数据结构
    java算法:字符串
    java算法:基于应用ADT例子
  • 原文地址:https://www.cnblogs.com/sunny3158/p/16570634.html
Copyright © 2020-2023  润新知