Redis--缓存穿透、击穿、雪崩以及预热问题(面试高频问题!)

缓存穿透、击穿、雪崩以及预热问题

  • 如何解决缓存穿透?
    • 方案一:缓存空对象
    • 方案二:布隆过滤器
      • 什么是布隆过滤器?
      • 优缺点
    • 方案三:接口限流
  • 如何解决缓存击穿问题?
    • 方案一:分布式锁
      • 方案一改进成双重判定锁
      • 高并发情况使用double check+ trylock解决
    • 方案二:缓存预热方案
      • 什么是缓存预热?
      • 如何进行缓存预热?
    • 方案三:热点数据永不过期
  • 如何解决缓存雪崩?
    • 对于大量缓存数据同时失效的解决办法
      • 方案一:均匀设置过期时间
      • 方案二:使用锁机制避免数据库频繁访问
      • 方案三:后台更新缓存
    • 对于Redis 故障宕机的解决办法
      • 方案一:服务熔断或请求限流机制
      • 方案二:构建 Redis 缓存高可靠集群

如何解决缓存穿透?

缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

方案一:缓存空对象

当查询结果为空时,也将结果进行缓存但是设置一个较短的过期时间。这样在接下来的一段时间内,如果再次请求相同的数据,就可以直接从缓存中获取,而不是再次访问数据库,可以一定程度上解决缓存穿透问题。

这种方式是比较简单的一种实现方案,会存在一些弊端。那就是当短时间内存在大量恶意请求缓存系统会存在大量的内存占用。如果要解决这种海量恶意请求带来的内存占用问题,需要搭配一套风控系统,对用户请求缓存不存在数据进行统计,进而封禁用户。整体设计就较为复杂,不推荐使用。
在这里插入图片描述

方案二:布隆过滤器

什么是布隆过滤器?

隆过滤器是一种数据结构,用于快速判断一个元素是否存在于一个集合中。具体来说,布隆过滤器包含一个位数组和一组哈希函数。位数组的初始值全部置为 0。在插入一个元素时,将该元素经过多个哈希函数映射到位数组上的多个位置,并将这些位置的值置为 1。

1字节(Byte)=8位(Bit)
image.png
在查询一个元素是否存在时,会将该元素经过多个哈希函数映射到位数组上的多个位置,如果所有位置的值都为 1,则认为元素存在;如果存在任一位置的值为 0,则认为元素不存在。
在这里插入图片描述

优缺点

优点:

  • 高效地判断一个元素是否属于一个大规模集合。
  • 节省内存。

缺点:

  • 可能存在一定的误判。

方案三:接口限流

根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制,例如将异常 IP 列入黑名单。

后面提到的缓存击穿和雪崩都可以配合接口限流来解决,毕竟这些问题的关键都是有很多请求落到了数据库上造成数据库压力过大。

如何解决缓存击穿问题?

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

方案一:分布式锁

因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大。

假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。
在这里插入图片描述
伪代码如下:

public String selectTrain(String id) {
    String cacheData = cache.get(id);
    // 查询缓存不存在,去数据库查询并放入到缓存
    if (StrUtil.isBlank(cacheData)) {
        // 为避免大量请求同时访问数据库,通过分布式锁减少数据库访问量
        Lock lock = getLock(id);
        lock.lock();
        try {
            // 获取数据库中存在的数据
            String dbData = trainMapper.selectId(id);
            if (StrUtil.isNotBlank(dbData)) {
                // 将查询到的数据放入缓存,下次查询就有数据了
                cahce.set(id, dbData);
                cacheData = dbData;
            }
        } finally {
            lock.unlock();
        }
    }
    return cacheData;
}

这种方案有效地避免了缓存击穿问题,因为只有一个线程能够在同一时间内查询数据库,其他线程需要等待,不会同时穿透到后端存储系统。性能较低,不适合高并发场景

方案一改进成双重判定锁

上边还有一个问题就是,假如 100w 的请求读取一个缓存,100w 的请求全部卡在 lock.lock 获取分布式锁处,只有一个线程会执行逻辑请求数据库并放入缓存。
问题来了,剩下正在获取分布锁的请求,就是 100w 个请求减去一个获取到锁的请求,还是会继续请求数据库获取数据。大家读一下上面的伪代码就明白了。

这会造成两个实际的问题:

  1. 全部用户获取锁后查询数据库,会对数据库造成无用的性能浪费,因为这 100w 的请求,只有第一次是有效的。
  2. 查询数据库会造成用户响应时间变长,接口吞吐量下降

双重判断:获取锁后,在查询数据库之前,再次检查一下缓存中是否存在数据。这是一个双重判断,如果缓存中存在数据,就直接返回;如果不存在,才继续执行查询数据库的操作。
伪代码如下:

public String selectTrain(String id) {
    // 查询缓存不存在,去数据库查询并放入到缓存
    String cacheData = cache.get(id);
    if (StrUtil.isBlank(cacheData)) {
        // 为避免大量请求同时访问数据库,通过分布式锁减少数据库访问量
        Lock lock = getLock(id);
        lock.lock();
        try {
            // 获取锁后双重判定
            cacheData = cache.get(id);
            // 理论上只有第一个请求加载数据库是有效的,因为它加载后会把数据放到缓存
            // 后面的请求再请求数据库加载缓存就没有必要了
            if (StrUtil.isBlank(cacheData)) {
                // 获取数据库中存在的数据
                String dbData = trainMapper.selectId(id);
                if (StrUtil.isNotBlank(dbData)) {
                    // 将查询到的数据放入缓存,下次查询就有数据了
                    cahce.set(id, dbData);
                    cacheData = dbData;
                }
            }
        } finally {
            lock.unlock();
        }
    }
    return cacheData;
}

下面是这种场景下解决方案的一般步骤:

  1. 获取锁:在查询数据库前,首先尝试获取一个分布式锁。只有一个线程能够成功获取锁,其他线程需要等待。
  2. 查询数据库:如果双重判断确认数据确实不存在于缓存中,那么就执行查询数据库的操作,获取数据。
  3. 写入缓存:获取到数据后,将数据写入缓存,并设置一个合适的过期时间,以防止缓存永远不会被更新。
  4. 释放锁:最后,释放获取的锁,以便其他线程可以继续使用这个锁。

高并发情况使用double check+ trylock解决

有一万个请求同一时间访问触发了缓存击穿,如果用双重判定锁,逻辑是这样的:

  1. 第一个请求加锁、查询缓存是否存在、查询数据库、放入缓存、解锁,假设我们用了 50 毫秒;
  2. 第二个请求拿到锁查询缓存、解锁用了 1 毫秒;
  3. 那最后一个请求需要等待 10049 毫秒后才能返回,用户等待时间过长,极端情况下可能会触发应用的内存溢出。

像上面这种场景,类似于秒杀的架构,我们要做的就是不让用户请求在服务端阻塞过长时间。那就可以使用尝试获取锁 tryLock API,它的语义是如果拿锁失败直接返回,而不是阻塞等待直到获取锁。

public String selectTrain(String id) {
    // 查询缓存不存在,去数据库查询并放入到缓存
    String cacheData = cache.get(id);
    if (StrUtil.isBlank(cacheData)) {
        // 为避免大量请求同时访问数据库,通过分布式锁减少数据库访问量
        Lock lock = getLock(id);
        // 尝试获取锁,获取失败直接返回用户请求,并提醒用户稍后再试
        if (!lock.tryLock()) {
            throw new RuntimeException("当前访问人数过多,请稍候再试...");
        }
        try {
            // 获取数据库中存在的数据
            String dbData = trainMapper.selectId(id);
            if (StrUtil.isNotBlank(dbData)) {
                // 将查询到的数据放入缓存,下次查询就有数据了
                cahce.set(id, dbData);
                cacheData = dbData;
            }
        } finally {
            lock.unlock();
        }
    }
    return cacheData;
}

方案二:缓存预热方案

什么是缓存预热?

缓存预热是指在应用程序启动或系统负载低峰期,提前将应用程序需要访问缓存的数据加载到缓存中,以便在实际的请求到来时能够快速响应。

缓存预热的目的是避免在实际请求到来时由于缓存冷启动而导致的延迟或性能下降。缓存冷启动是指在缓存中没有预先加载数据的情况下,第一次请求到达时需要从后端系统或数据库获取数据,并将其存储到缓存中。这个过程可能需要花费较长的时间,延迟了实际请求的响应时间。

如何进行缓存预热?

缓存预热有很多种方式,比如定时任务从数据库中查询进行预热等。我们这里在创建完短链接后就将短链接记录新增到缓存中。

@Transactional(rollbackFor = Exception.class)
@Override
public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
    verificationWhitelist(requestParam.getOriginUrl());
    String shortLinkSuffix = generateSuffix(requestParam);
    String fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
            .append("/")
            .append(shortLinkSuffix)
            .toString();
    ShortLinkDO shortLinkDO = xxx;
    ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
            .fullShortUrl(fullShortUrl)
            .gid(requestParam.getGid())
            .build();
    try {
        baseMapper.insert(shortLinkDO);
        shortLinkGotoMapper.insert(linkGotoDO);
    } catch (DuplicateKeyException ex) {
        if (!shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)) {
            shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        }
        throw new ServiceException(String.format("短链接:%s 生成重复", fullShortUrl));
    }
    // 将短链接新增到缓存中进行预热
    stringRedisTemplate.opsForValue().set(
            String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
            requestParam.getOriginUrl(),
            LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS
    );
    shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
    return ShortLinkCreateRespDTO.builder()
            .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
            .originUrl(requestParam.getOriginUrl())
            .gid(requestParam.getGid())
            .build();
}

将缓存预热的话,有个小技巧,是否要设置缓存的过期时间?如果设置,那设置多少合适?

因为咱们短链接创建时是可以设置过期时间的,所以对于设置了过期时间的短链接,我们在缓存中也设置对应的时间即可。

那对于永久有效的短链接难道就不设置过期时间么?大家知道,短链接一般来说具有时效性,很多时候只会在一定时间内使用,过了这个时间后,用的人就很少了。所以,即使短链接永久有效,我们也得设置过期时间。不然,大量不使用的短链接放在缓存中,存储压力会比较大。

如果短链接设置的永久有效,我们默认一个月的过期时间。如果一个月后还有人访问,再去数据库加载数据,再设置一个月的过期时间即可。

方案三:热点数据永不过期

热点数据永不过期,指的就是可以预知的热点数据,在活动开始前,设置过期时间为 -1。这样的话,就不会有缓存击穿的风险。

这个可以搭配热点数据预加载一起完成。等对应热点缓存的活动结束后,这些数据访问量就比较低了,可以通过后台任务的方案对指定缓存设置过期时间,这样可以有效降低 Redis 存储压力。

如何解决缓存雪崩?

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

对于大量缓存数据同时失效的解决办法

方案一:均匀设置过期时间

避免所有缓存在同一时间点失效,可以采用随机分布的方式设置缓存失效时间,或者使用带有随机偏移的失效时间。

通过以上几种方案组合使用,可以一定程度上减少缓存雪崩的可能性。

方案二:使用锁机制避免数据库频繁访问

当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。

方案三:后台更新缓存

对于一些热点数据,可以设置永不过期,以保证这部分数据始终在缓存中可用。同时,需要保障缓存设置的内存淘汰策略是不淘汰或者从带过期时间 Key 中去淘汰。

咱们以两种系统举例,分别是电商系统以及 12306 铁路购票系统:

电商系统:比如需要参加秒杀的商品数据,我们为了避免因设计过期时间自动过期或者不合适的 Redis 过期策略自动清楚,需要将参与秒杀的商品数据直接设置为永不过期。
12306:同上,如果在售票期间的列车数据缓存就不要设置过期时间了。
如果商品过了秒杀时间或者 12306 列车时间过了售票时间,这些数据岂不是会占用缓存空间么?

这种一般都会有定时任务在活动结束或过了售票周期后统一删除。

对于Redis 故障宕机的解决办法

方案一:服务熔断或请求限流机制

因为 Redis 故障宕机而导致缓存雪崩问题时,我们可以启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误,不用再继续访问数据库,从而降低对数据库的访问压力,保证数据库系统的正常运行,然后等到 Redis 恢复正常后,再允许业务应用访问缓存服务。
为了减少对业务的影响,我们可以启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制。

方案二:构建 Redis 缓存高可靠集群

服务熔断或请求限流机制是缓存雪崩发生后的应对方案,我们最好通过主从节点的方式构建 Redis 缓存高可靠集群

如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,避免了由于 Redis 故障宕机而导致的缓存雪崩问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/947985.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

嵌入式 Linux LED 驱动开发实验

一、Linux 下 LED 灯驱动原理 a)地址映射 在编写驱动之前,我们需要先简单了解一下 MMU 这个神器, MMU 全称叫做 Memory Manage Unit,也就是内存管理单元。在老版本的 Linux 中要求处理器必须有 MMU,但是现在 Linux 内核已经支持无 MMU 的处理器了。 MMU 主要完成的功能如…

网络安全:交换机技术

单播,组播广播 单播(unicast): 是指封包在计算机网络的传输中,目的地址为单一目标的一种传输方式。它是现今网络应用最为广泛,通常所使用的网络协议或服务大多采用单播传输,例如一切基于TCP的协议。组播(multicast): 也叫多播&am…

C# 在PDF中添加和删除水印注释 (Watermark Annotation)

目录 使用工具 C# 在PDF文档中添加水印注释 C# 在PDF文档中删除水印注释 PDF中的水印注释是一种独特的注释类型,它通常以透明的文本或图片形式叠加在页面内容之上,为文档添加标识或信息提示。与传统的静态水印不同,水印注释并不会永久嵌入…

fpga系列 HDL:verilog 常见错误与注意事项 位宽不匹配+case 语句中没有覆盖所有情况

位宽不匹配问题 信号或操作数的位宽不匹配,可能导致仿真或综合错误。 module top (input wire [3:0] a,output wire [7:0] b );assign b a; endmodulecase 语句中没有覆盖所有情况 module top (input wire [1:0] sel,input wire [7:0] a,input wire [7:0] b,in…

groupby 操作的不同参数

groupby 是数据分析中一个非常强大的操作,可以根据指定的规则将数据拆分成多个组,并对每个组进行聚合、转换或过滤等操作。我们逐个解释这些参数的作用,并通过数值举例进行说明。 参数解释 by:分组依据 by 参数指定了分组的依据&…

鸢尾花种类预测--数据集介绍

1.6 案例:鸢尾花种类预测--数据集介绍 学习目标 目标 知道sklearn中获取数据集的方法知道sklearn中对数据集的划分方法 本实验介绍了使用Python进行机器学习的一些基本概念。 在本案例中,将使用K-Nearest Neighbor(KNN)算法对鸢尾…

基于深度学习的视觉检测小项目(二) 环境和框架搭建

一、环境和框架要求 SAM的环境要求: Python>3.7 PyTorch>1.7 torchvision>0.8 YOLO V8的环境要求:YOLO集成在ultralytics库中,ultralytics库的环境要求: Python>3.7 PyTorch>1.10.0 1、确定pytorch版本…

Javascript算法——回溯算法(组合问题)

相关资料来自《代码随想录》,版权归原作者所有,只是学习记录 回溯 回溯模板 void backtracking(参数) {if (终止条件) {存放结果;return;}for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {处理节点…

Java Excel转PDF POI+Itext5

由于现在存在需求,通过Java将数据文本生成特点格式Excel,再输出为PDF。 调研了一些方案,最终决定使用POI写入Excel,再使用Itext5生成PDF。 在网上找了一些Itext的转换工具类,进行了一些改动。 目前市面上 Excel 转 PDF 的组件…

linux nginx maccms管理后台无法进入页面不存在和验证码不显示的问题

windows中运行maccms非常顺利,轻松搭建了。并一切正常。而我在linux中搭建缺遇到了一个非常奇怪的问题。进入管理后台,明明"admin.php"(比如重命名成a.php)的页面是存在的,访问时缺提示页面不存在!稍后就自动跳到首页了…

C# 服务调用RFC函数获取物料信息,并输出生成Excel文件

这个例子是C#服务调用RFC函数,获取物料的信息,并生成Excel文件 上接文章:C#服务 文章目录 创建函数创建结构编写源代码创建批处理文件运行结果-成功部署服务器C#代码配置文件注意!! 创建函数 创建结构 编写源代码 创建…

在 SQL 中,区分 聚合列 和 非聚合列(nonaggregated column)

文章目录 1. 什么是聚合列?2. 什么是非聚合列?3. 在 GROUP BY 查询中的非聚合列问题示例解决方案 4. 为什么 only_full_group_by 要求非聚合列出现在 GROUP BY 中?5. 如何判断一个列是聚合列还是非聚合列?6. 总结 在 SQL 中&#…

Postman测试big-event

报错500。看弹幕,知道可能是yml或sql有问题。 所以检查idea工作台, 直接找UserMapper检查,发现完全OK。 顺着这个error发现可能是sql有问题。因为提示是sql问题,而且是有now()的那个sql。 之后通过给的课件,复制课件…

SpringBoot 2.6 集成es 7.17

引言 在现代应用开发中,Elasticsearch作为一个强大的搜索引擎和分析引擎,已经成为许多项目不可或缺的一部分。Spring Boot作为Java生态中最受欢迎的微服务框架之一,其对Elasticsearch的支持自然也是开发者关注的焦点。本文将详细介绍如何在S…

沙箱模拟支付宝支付3--支付的实现

1 支付流程实现 演示案例 主要参考程序员青戈的视频【支付宝沙箱支付快速集成版】支付宝沙箱支付快速集成版_哔哩哔哩_bilibili 对应的源码在 alipay-demo: 使用支付宝沙箱实现支付功能 - Gitee.com 以下是完整的实现步骤 1.首先导入相关的依赖 <?xml version"1…

自行下载foremos命令

文章目录 问题描述其他小伙伴的成功解决方案&#xff0c;但对我不适用解决思路失败告终 最终解决成功解决思路解决步骤 问题描述 在kali系统终端中输入foremost&#xff0c;显示无此命令 其他小伙伴的成功解决方案&#xff0c;但对我不适用 解决思路 正常来说使用命令 apt-g…

商米电子秤服务插件

概述 SunmiScaleUTS封装商米电子秤服务模块&#xff0c;支持商米旗下S2, S2CC, S2L CC等设备&#xff0c;设备应用于超市、菜市场、水果店等,用于测量商品的重量,帮助实现快捷、准确、公正的交易等一系列商业场景。 功能说明 SDK插件下载 一. 电子秤参数 型号:S2, S2CC, …

快速将索尼手机联系人导出为 HTML 文件

我想将 Sony Xperia 手机上的联系人导出到计算机上进行备份&#xff0c;并在需要时进行编辑。这可以做到吗&#xff1f;如何做到&#xff1f;作为助手我需要下载什么工具吗&#xff1f; 当您的 Android 手机上存储了如此多的重要联系人&#xff0c;而您又不想丢失它们时&#…

linux安装redis及Python操作redis

目录 一、Redis安装 1、下载安装包 2、解压文件 3、迁移文件夹 4、编译 5、管理redis文件 6、修改配置文件 7、启动Redis 8、将redis服务交给systemd管理 二、Redis介绍 1、数据结构 ①字符串String ②列表List ③哈希Hash ④集合Set ⑤有序集合Sorted Set 2、…

聆听音乐 1.5.9 | 畅听全网音乐,支持无损音质下载

聆听音乐手机版是面向广大音乐爱好者的移动应用程序&#xff0c;用户可以随时随地通过手机享受丰富的音乐资源。它提供了多种魅力功能&#xff0c;让用户在手机上畅享更舒适的音乐体验&#xff0c;每位用户都能享受精彩纷呈的收听体验。此外&#xff0c;软件还支持无损音质音乐…