• 缓存穿透、缓存击穿、缓存雪崩区别和解决方案


    生命无罪,健康万岁,我是laity。

    我曾七次鄙视自己的灵魂:

    第一次,当它本可进取时,却故作谦卑;

    第二次,当它在空虚时,用爱欲来填充;

    第三次,在困难和容易之间,它选择了容易;

    第四次,它犯了错,却借由别人也会犯错来宽慰自己;

    第五次,它自由软弱,却把它认为是生命的坚韧;

    第六次,当它鄙夷一张丑恶的嘴脸时,却不知那正是自己面具中的一副;

    第七次,它侧身于生活的污泥中,虽不甘心,却又畏首畏尾。

    前言

    关于缓存异常,我们常见的有三个问题:缓存雪崩、缓存击穿、缓存穿透。 这三个问题一旦发生,会导致大量请求直接落到数据库层面。如果请求的并发量很大,会影响数据库的运行,严重的会导致数据库宕机。

    为了避免异常带来的损失,我们需要了解每种异常的原因以及解决方案,提高系统的可靠性。

    高并发下缓存雪崩

    Redis中的数据大面积失效(时间过期)的情景

    • 缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
    • 解决方案:
      • 均匀过期:给热点数据设置不同的过期时间,给每个key的失效时间加一个随机值;
        • 原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
      • 设置热点数据永不过期:不设置失效时间,有更新的话,需要更新缓存;
      • 服务降级:指服务针对不同的数据采用不同的处理方式:
        • 业务访问的是非核心数据,直接返回预定义信息、空值或者报错;
        • 业务访问核心数据,则允许访问缓存,如果缓存缺失,可以读取数据库。

    Redis 缓存实例发生故障宕机的场景

    • 实现服务熔断或者请求限流机制
      • 通过监测Redis以及数据库实例所在服务器负载指标,如果发现Redis服务宕机,导致数据库的负载压力增大,我们可以启动服务熔断机制,暂停对缓存服务的访问。
      • 但是这种方法对业务应用的影响比较大,我们也可以通过限流的方式降低这种影响。

    举个例子:比如业务系统正常运行时,请求入口每秒最大允许进入的请求数是1万个,其中9000请求个可以被缓存处理,余下1000个会发送给数据库处理。一旦发生雪崩,数据库每秒处理的请求突然增加到1万个,此时我们就可以启动限流机制。在前端请求入口处,只允许每秒进入1000个请求,其他的直接拒绝掉。这样就可以避免大量并发请求发送给数据库。

    • 事前预防
      • 通过主从节点的方式构建 Redis 缓存高可靠集群。 如果 Redis 缓存的主节点故障宕机了,从节点还可以切换成为主节点,继续提供缓存服务,避免了由于缓存实例宕机而导致的缓存雪崩问题。

    高并发下缓存穿透

    就是去查询一个一定不存在的数据

    • 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
    • 在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。
    • 解决方案:
      • 缓存空结果、并且设置短的过期时间。(null结果进行缓存)
        • 如果有大量的Key穿透,缓存空对象会占用宝贵的内存空间。针对这种情况可以给空对象设置过期时间。
        • 设置过期时间之后,可能会有缓存与数据库不一致的情况(后期可能又有这个条数据了)。
      • 布隆过滤:快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力。
      • 接口层增加校验:用户鉴权、参数校验(请求参数是否合法、请求字段是否不存在等等。

    高并发下缓存击穿

    一个key过期,但是被高访问

    • 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
    • 这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所有对这个 key 的数据查询都落到 db,我们称为缓存击穿。
    • 解决方案:
      • 设置热点数据永不过期:不设置失效时间,有更新的话,需要更新缓存;
      • 加互斥锁:单机可以使用synchronized、lock,分布式可以使用lua脚本或分布式锁Redisson。

    zookeeper与etcd,用来做锁都比redis要好很多

    关于缓存异常有兴趣的同学可以看下这篇:关于缓存异常:缓存雪崩、击穿、穿透的解决方案
    关于分布式锁有兴趣的同学可以看下这篇:Redis分布式锁的正确打开方式

    布隆过滤器

    布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。

    • 优点:空间效率和查询时间都远远超过一般的算法。
    • 缺点:有一定的误识别率,删除困难。

    布隆过滤器原理

    布隆过滤器本质上是一个初始值都为 0 的 二进制数组N 个哈希函数组成。
    当我们想标记某个数据存在时(例如,数据x已被写入数据库),布隆过滤器会通过三个操作完成标记:

    • 首先,使用 N 个哈希函数,分别计算这个数据的哈希值,得到 N 个哈希值。
    • 然后,我们把这 N 个哈希值对 bit 数组的长度取模,得到每个哈希值在数组中的对应位置。
    • 最后,我们把对应位置的 bit 位设置为 1,这就完成了在布隆过滤器中标记数据的操作。

    如下图所示:
    在这里插入图片描述

    三次哈希,对应的二进制数组下标分别是 1、3、7,将原始数据从 0 变为 1。

    同样的,我们标记数据y的逻辑也是一样的。如下图:
    在这里插入图片描述

    三次哈希,对应的二进制数组下标分别是 1、5、9,将原始数据从 0 变为 1。

    下标 1,之前已经被操作设置成 1,则本次认为是哈希冲突,不需要改动。

    Hash 规则:如果在 Hash 后,原始位它是 0 的话,将其从 0 变为 1;如果本身这一位就是 1 的话,则保持不变。

    正是因为存在哈希冲突,导致布隆过滤器的判断存在误差。

    也因为哈希冲突的存在,导致布隆过滤器不能轻易删除数据,存在误删的风险。

    布隆过滤器减少误差的方法:

    • 增加二进制位数组的长度,这样hash后的数据会更加离散化,出现冲突的概率会大大降低;
    • 增加Hash的次数,变相的增加数据特征,特征越多,冲突的概率越小。

    布隆过滤器如何使用

    比如,当查询一件商品的缓存信息时,我们首先要判断这件商品是否存在。

    • 通过三个哈希函数对商品id计算哈希值;
    • 然后,在布隆数组中查找访问对应的位值,0或1;
    • 判断,三个值中,只要有一个不是1,那么我们认为数据是不存在的。

    如下图:
    在这里插入图片描述
    注意:布隆过滤器只能精确判断数据不存在情况,对于存在我们只能说是可能,因为存在Hash冲突情况,当然这个概率非常低。

    在Java中使用布隆过滤器

    至于在Java应用中使用布隆过滤器,我们可以通过Redisson实现,它内置了布隆过滤器。

    整合redisson做为分布式锁等功能框架

    首先引入依赖

            <!-- Redis- 红锁:分布式锁,分布式对象等功能的框架 -->
            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson</artifactId>
                <version>3.17.7</version>
            </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    代码示例:

    package com.laity.product;
    
    import org.junit.jupiter.api.Test;
    import org.redisson.api.RBloomFilter;
    import org.redisson.api.RedissonClient;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    /**
     * @author: Laity
     * @Project: JavaLaity
     * @Package: com.laity.product.config.MyRedissonConfig
     * @Date: 2022年10月18日 01:39
     * @Description: 测试基于redisson使用布隆过滤
     */
    @SpringBootTest
    public class RedissonBloomFilter {
    
        @Autowired
        private RedissonClient redissonClient;
    
        @Test
        void redissonBloom(){
            RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("test-bloom-filter");
            // 初始化布隆过滤器,数组长度100W,误判率 1%
            bloomFilter.tryInit(1000000L, 0.01);
            // 添加数据
            bloomFilter.add("Shawn");
            // 判断是否存在
            System.out.println(bloomFilter.contains("laity"));
            System.out.println(bloomFilter.contains("itlaity"));
        }
    }
    
    • 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

    运行结果:

    false   // 肯定不存在
    true    // 可能存在,有1%的误判率
    
    • 1
    • 2

    与其让平凡与胆怯缚住自己的手脚,不如倾生命的全部能量奋力一搏。我是Laity,正在前行的Laity。

  • 相关阅读:
    “池化技术” - 深度剖释底层内存管理细节,明晰“池化技术”内存管理技术
    JavaSE进阶之(十三)Java 异常处理的 21 个最佳实践
    ESP32 之 ESP-IDF 教学(十九)—— 在工程或组件中嵌入二进制数据
    Redis6笔记04 主从复制,集群,应用问题,Redis6新功能
    2023亚太杯数学建模思路 - 案例:FPTree-频繁模式树算法
    Stable Diffusion文生图模型训练入门实战(完整代码)
    flutter系列之:flutter中常用的ListView layout详解
    HTML/CSS 基础 2
    前端面试常见手写题
    Python代码运行速度提升技巧!Python远比你想象中的快~
  • 原文地址:https://blog.csdn.net/duyun0/article/details/127942059