Redis 中的缓存击穿、缓存雪崩和缓存穿透是什么?


Redis 中的缓存击穿、缓存雪崩和缓存穿透是什么?

  1. 缓存击穿: 缓存击穿指的是某个热门的缓存键在过期后,同时有大量并发请求到达,导致所有请求都穿透缓存直接访问数据库,造成数据库压力激增。

    解决方法包括:

    • 使用互斥锁来保护缓存访问,只允许一个线程重新生成缓存。
    • 针对缓存失效时的并发请求使用分布式锁,确保只有一个线程重新生成缓存。
  2. 缓存雪崩: 缓存雪崩指的是大量缓存键在相同时间失效,导致大量请求落到数据库上,造成数据库压力激增。

    解决方法包括:

    • 为缓存键设置不同的失效时间,使失效时间分散。
    • 使用热点数据预热,提前加载热门数据到缓存。
  3. 缓存穿透:缓存穿透指的是 请求查询的是一个不存在的数据,大多是故意的发起的恶意的请求,利用数据库中不存在的数据 —> 理论上数据库中不存在,缓存中也就不会存在,导致每次请求都直接访问数据库,增加数据库负载。这时候缓存的中间件,比如Redis从功能上和逻辑上的使用是正常的。

    比如有些小黑子对于我们开发的网站进行恶意的请求,将一些数据库不存在的 ID 疯狂的打在我们的服务器上,如果没做好缓存穿透的预防,还真给你们小黑子得逞了。

    解决办法:

    • 防止非法请求:检查非法请求,封禁其 IP 以及账号,防止它再次为非作歹。。
    • 缓存空值:针对特殊情况,提前在缓存中 允许缓存空值或者可以给他一个默认值。
    • 使用布隆过滤器:通过布隆过滤器给数据做一个标记,当发生缓存穿透时也不会请求数据库造成压力,直接通过布隆过滤器和 Redis 判断返回。

    缓存击穿缓存雪崩 的问题之间还有关联性,两者关系属于“事件升级”了,起码是正常的用户请求。 找老板审批资金,结果资金太大前台、中间层都找不到能做主的,然后直接递到老板桌上,属于有理有据的合理事件处理,但是这样的事件一多,老板也会懵逼。

    缓存穿透 的问题就是利用 机制的漏洞采取的手段,属于大量要处理的事件绕过公司前台,中间层,直接找到了黑心老板的桌上要他打钱,直接给老总干懵逼了。


  4. 缓存(数据内容)过期问题: 缓存中的数据过期后可能会导致数据不一致或数据不可用。

    解决方法包括:

    • 设置合理的缓存失效时间,避免缓存数据长时间不更新。
    • 使用缓存的时候检查数据是否过期,如果过期则重新生成缓存。
  5. 缓存内存问题: 如果缓存数据量很大,可能会导致内存占用过多。

    解决方法包括:

    • 设置合理的内存限制,避免缓存数据过多。
    • 使用LRU(Least Recently Used)策略或淘汰算法来淘汰不常用的缓存数据。
  6. 缓存数据一致性问题: 缓存数据和数据库数据不一致。

    解决方法包括:

    • 使用缓存更新策略,当数据库数据发生变化时,及时更新缓存。
    • 使用双写策略,即同时更新数据库和缓存,确保数据一致性。
  7. 缓存安全问题: 某些敏感数据可能不应该被缓存,如果被缓存可能引发安全问题。

    解决方法包括:

    • 避免缓存敏感数据。
    • 使用加密或其他安全措施来保护缓存数据。
  8. 缓存监控和调优问题: 缓存需要监控和调优,以确保性能和稳定性。

    解决方法包括:

    • 使用监控工具来监测缓存的命中率、内存占用等性能指标。
    • 定期调整缓存配置,优化性能。
在项目中,通过给不同的缓存设置不同的随机过期时间(N + n)来解决缓存雪崩问题。

缓存穿透 和 缓存击穿、缓存雪崩的场景以及解决方法

都是缓存惹的祸

在项目开发中,我们的数据都是要持久化到磁盘中去,比如使用 MySQL 进行持久化存储,但是呢由于流量越来越大,查询速度也逐渐变慢了起来,于是我们决定!使用缓存!然而使用缓存导致会经常面临三座大山!缓存穿透!!缓存击穿!!缓存雪崩!!,接下来我们将会逐一分析他们导致的原因以及解决方法。

缓存雪崩

pAAD3rt.png

缓存雪崩

介绍

缓存雪崩是指在某个时间点,大量缓存同时失效或被清空,导致大量请求直接打到数据库或后端系统,造成系统负载激增,甚至引发系统崩溃。

这通常是由于缓存中的大量数据在同一时间失效引起的。

想象一个在线电商系统,用户访问频繁,需要频繁查询商品信息。假设某一系列的商品突然全部同一时间失效,那就会造成我们的缓存雪崩。或者某一个时刻 Redis 缓存中间件故障了,导致服务全部打到了数据库,也会导致缓存雪崩的情况。

解决办法

如果是防止缓存键同时失效事故:

1)过期时间随机化:设置缓存的过期时间,加上一个随机值,避免同一时间大量缓存失效。

2)使用多级缓存:引入多级缓存机制,如本地缓存和分布式缓存相结合,减少单点故障风险。

3)缓存预热:系统启动时提前加载缓存数据,避免大量请求落到冷启动状态下的数据库。

4)加互斥锁:保证同一时间只有一个请求来构建缓存,别的只能等它构建完成再从缓存中读取。

如果是 缓存中间件故障引起事故:

1)服务熔断:暂停业务的返回数据,直接返回错误。

2)构建集群:构建多个 Redis 集群保证其高可用。

缓存击穿

pAVwNGQ.webp

缓存击穿

pAADJVf.png

介绍

缓存击穿是指针对某一热点数据的大量请求导致缓存失效,进而直接请求数据库,增加数据库负载。

这种情况通常发生在某个特定的缓存 key 在失效时,恰好有大量请求到达。

想象一下大家都在抢茅台,但在某一时刻茅台的缓存失效了,大家的请求打到了数据库中,这就是缓存击穿,**那他跟缓存雪崩有什么区别呢?**缓存雪崩是多个 key 同时,缓存击穿是某个热点 key 崩溃。也可以认为缓存击穿是缓存雪崩的子集。

解决办法

1)加互斥锁:保证同一时间只有一个请求来构建缓存,别的只能等它构建完成再从缓存中读取。跟缓存雪崩相同。

2)永久:不要给热点数据设置过期时间。参考缓存设置规范,不过期就是设置永久有效,参数为 -1。

缓存穿透

缓存穿透

pAADYa8.png

介绍

缓存穿透是指查询一个不存在的数据,由于缓存和数据库中均不存在,导致每次请求都直接访问数据库,增加数据库负载。

攻击者可以通过构造不存在的 key 发起大量请求,造成系统宕机。

比如有些小黑子对于我们开发的网站进行恶意的请求,将一些数据库不存在的 ID 疯狂的打在我们的服务器上,如果没做好缓存穿透的预防,还真给你们小黑子得逞了。

解决办法

  • 防止非法请求:检查非法请求,封禁其 IP 以及账号,防止它再次为非作歹。。
  • 缓存空值:针对特殊情况,提前在缓存中 允许缓存空值或者可以给他一个默认值。
  • 使用布隆过滤器:通过布隆过滤器给数据做一个标记,当发生缓存穿透时也不会请求数据库造成压力,直接通过布隆过滤器和 Redis 判断返回。(在数据写入数据库的同时将这个 ID 同步到到布隆过滤器中,当请求的 id 不存在布隆过滤器中则说明该请求查询的数据一定没有在数据库中保存,就不要去数据库查询了。——也就是说是在真实数据创建的时候就把该数据的标记存到布隆过滤器中)

什么是布隆过滤器

布隆过滤器 (Bloom Filter)是由 Burton Howard Bloom 于 1970 年提出,它是一种 space efficient 的概率型数据结构,用于判断一个元素是否在集合中。

  • 当布隆过滤器说,某个数据存在时,这个数据可能不存在;当布隆过滤器说,某个数据不存在时,那么这个数据一定不存在。

  • 哈希表也能用于判断元素是否在集合中,但是布隆过滤器只需要哈希表的 1/8 或 1/4 的空间复杂度就能完成同样的问题。

  • **布隆过滤器可以插入元素,但不可以删除已有元素。**只能重构。

  • 布隆过滤器其中的元素越多,false positive rate(误报率)越大,但是 false negative (漏报)是不可能的。

总结:

1、使用时进行布隆过滤器的初始化,一次性给够容量,不要让实际数量大于初始化数量,避免重构布隆过滤器。

2、如果实际数量大于初始化数量,这个时候就需要进行重构了,重新分配一个更大数量的过滤器,再将所有旧数据重新初始化进过滤器。

布隆过滤器的原理

BloomFilter 的算法是,首先分配一块内存空间做 bit 数组,数组的 bit 位初始值全部设为 0。

加入元素时,采用 k 个相互独立的 Hash 函数计算,然后将元素 Hash 映射的 K 个位置全部设置为 1。

检测 key 是否存在,仍然用这 k 个 Hash 函数计算出 k 个位置,如果位置全部为 1,则表明 key 存在,否则不存在。

如下图所示:

pAVwmPe.png

img

哈希函数会出现碰撞,所以布隆过滤器会存在误判。

这里的误判率是指,BloomFilter 判断某个 key 存在,但它实际不存在的概率,因为它存的是 key 的 Hash 值,而非 key 的值。

所以有概率存在这样的 key,它们内容不同,但多次 Hash 后的 Hash 值都相同。

对于 BloomFilter 判断不存在的 key ,则是 100% 不存在的,反证法,如果这个 key 存在,那它每次 Hash 后对应的 Hash 值位置肯定是 1,而不会是 0。布隆过滤器判断存在不一定真的存在。

布隆过滤器为什么不允许删除元素呢?

参考布隆过滤器的存储原理和元素的存储方式,删除意味着需要将对应的 k 个 bits 位置设置为 0,其中有可能是其他元素对应的位。

因此 remove 会引入 false negative,这是绝对不被允许的。

使用场景

1.黑白名单校验、识别垃圾邮件

发现存在黑名单中的,就执行特定操作。比如:识别垃圾邮件,只要是邮箱在黑名单中的邮件,就识别为垃圾邮件。

假设黑名单的数量是数以亿计的,存放起来就是非常耗费存储空间的,布隆过滤器则是一个较好的解决方案。

把所有黑名单都放在布隆过滤器中,在收到邮件时,判断邮件地址是否在布隆过滤器中即可。如果邮件地址出现在布隆过滤器中的“黑名单”,就执行判断黑名单逻辑。

2.解决[缓存穿透]问题

把已存在数据的key存在布隆过滤器中,相当于redis前面挡着一个布隆过滤器

当有新的请求时,先到布隆过滤器中查询是否存在:

  • 如果布隆过滤器中不存在该条数据则直接返回;

  • 如果布隆过滤器中已存在,才去查询缓存redis,如果redis里没查询到则再查询Mysql数据库

image-20240903101913811

Redis 集成布隆过滤器

Redis 4.0 的时候官方提供了插件机制,布隆过滤器正式登场。以下网站可以下载官方提供的已经编译好的可拓展模块。

https://redis.com/redis-enterprise-software/download-center/modules/

下载地址:https://github.com/RedisBloom/RedisBloom/releases/tag/v2.2.14


Redis 布隆过滤器——解决缓存穿透问题

我们来用布隆过滤器来解决缓存穿透问题,缓存穿透:意味着有特殊请求在查询一个不存在的数据,即数据不存在 Redis 也不存在于数据库。

当用户购买商品创建订单的时候,我们往 mq 发送消息,把订单 ID 添加到布隆过滤器。

img

订单同步到布隆过滤器

在添加到布隆过滤器之前,我们通过BF.RESERVE命令手动创建一个名字为 orders error_rate = 0.1 ,初始容量为 10000000 的布隆过滤器:

# BF.RESERVE {key} {error_rate} {capacity} [EXPANSION {expansion}] [NONSCALING]
BF.RESERVE orders 0.1 10000000
  • key:filter 的名字;
  • error_rate:期望的错误率,默认 0.1,值越低,需要的空间越大;
  • capacity:初始容量,默认 100,当实际元素的数量超过这个初始化容量时,误判率上升。
  • EXPANSION:可选参数,当添加到布隆过滤器中的数据达到初始容量后,布隆过滤器会自动创建一个子过滤器,子过滤器的大小是上一个过滤器大小乘以 expansion;expansion 的默认值是 2,也就是说布隆过滤器扩容默认是 2 倍扩容;
  • NONSCALING:可选参数,设置此项后,当添加到布隆过滤器中的数据达到初始容量后,不会扩容过滤器,并且会抛出异常((error) ERR non scaling filter is full) 说明:BloomFilter 的扩容是通过增加 BloomFilter 的层数来完成的。每增加一层,在查询的时候就可能会遍历多层 BloomFilter 来完成,每一层的容量都是上一层的两倍(默认)。

如果不使用BF.RESERVE命令创建,而是使用 Redis 自动创建的布隆过滤器默认的 error_rate 0.01capacity是 100。

布隆过滤器的 error_rate 越小,需要的存储空间就越大,对于不需要过于精确的场景,error_rate 设置稍大一点也可以。

布隆过滤器的 capacity 设置的过大,会浪费存储空间,设置的过小,就会影响准确率,所以在使用之前一定要尽可能地精确估计好元素数量,还需要加上一定的冗余空间以避免实际元素可能会意外高出设置值很多。

添加 Redission 依赖:

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.16.7</version>
</dependency>

使用 Spring boot 默认的 Redis 配置方式配置 Redission:

spring:
  application:
    name: redission

  redis:
    host: 127.0.0.1
    port: 6379
    ssl: false

创建布隆过滤器:

@Service
public class BloomFilterService {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 创建布隆过滤器
     * @param filterName 过滤器名称
     * @param expectedInsertions 预测插入数量
     * @param falseProbability 误判率
     * @param <T>
     * @return
     */
    public <T> RBloomFilter<T> create(String filterName, long expectedInsertions, double falseProbability) {
        RBloomFilter<T> bloomFilter = redissonClient.getBloomFilter(filterName);
        bloomFilter.tryInit(expectedInsertions, falseProbability);
        return bloomFilter;
    }

}

单元测试:

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedissionApplication.class)
public class BloomFilterTest {

    @Autowired
    private BloomFilterService bloomFilterService;

    @Test
    public void testBloomFilter() {
        // 预期插入数量
        long expectedInsertions = 10000L;
        // 错误比率
        double falseProbability = 0.01;
        RBloomFilter<Long> bloomFilter = bloomFilterService.create("ipBlackList", expectedInsertions, falseProbability);

        // 布隆过滤器增加元素
        for (long i = 0; i < expectedInsertions; i++) {
            bloomFilter.add(i);
        }
        long elementCount = bloomFilter.count();
        log.info("elementCount = {}.", elementCount);

        // 统计误判次数
        int count = 0;
        for (long i = expectedInsertions; i < expectedInsertions * 2; i++) {
            if (bloomFilter.contains(i)) {
                count++;
            }
        }
        log.info("误判次数 = {}.", count);
        bloomFilter.delete();
    }
}

图片

解决方案

  • 缓存空值:当请求的数据不存在 Redis 也不存在数据库的时候,设置一个缺省值(比如:None)。当后续再次进行查询则直接返回空值或者缺省值。
  • 布隆过滤器:在数据写入数据库的同时将这个 ID 同步到到布隆过滤器中,当请求的 id 不存在布隆过滤器中则说明该请求查询的数据一定没有在数据库中保存,就不要去数据库查询了

BloomFilter 要缓存全量的 key,这就要求全量的 key 数量不大,100 亿 条数据以内最佳,因为 100 亿条数据大概要占用 3.5GB 的内存。

知识点
Redis