Redis 缓存穿透、缓存击穿、缓存雪崩详解与解决方案

article/2025/8/7 1:12:09

在分布式系统中,Redis 凭借高性能和高并发处理能力,成为常用的缓存组件。然而,在实际应用中,缓存穿透、缓存击穿、缓存雪崩这三大问题会严重影响系统的性能与稳定性。本文将详细解析这三个问题的成因,并提供对应的解决方案,同时结合 Java 示例代码和图示帮助你更好地理解和实践。

一、缓存穿透

1. 问题描述

缓存穿透指的是大量请求访问 Redis 缓存中不存在的数据,导致请求直接穿透到数据库,给数据库带来巨大压力。例如,黑客恶意构造大量不存在的商品 ID 请求,每次请求都无法命中缓存,只能查询数据库,可能导致数据库被压垮。

图中展示了缓存穿透的过程,大量不存在的请求绕过 Redis 缓存,直接访问数据库。

2. 成因分析

  • 恶意攻击:攻击者故意发送不存在的键值请求,使缓存无法命中。
  • 业务逻辑漏洞:应用程序未对请求参数进行有效校验,导致不合理的查询进入缓存层。

3. 解决方案

(1)布隆过滤器

布隆过滤器是一种概率型数据结构,用于判断某个元素是否存在于集合中。它可以在请求进入 Redis 之前,快速判断数据是否存在,若不存在则直接返回,避免请求穿透到数据库。

示例代码(基于 Google Guava 库)

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import redis.clients.jedis.Jedis;
public class BloomFilterDemo {
    private static final int EXPECTED_ELEMENTS = 10000; // 预计元素数量
    private static final double FALSE_POSITIVE_RATE = 0.01; // 误判率
    private static final BloomFilter<Integer> bloomFilter = BloomFilter.create(
            Funnels.integerFunnel(), EXPECTED_ELEMENTS, FALSE_POSITIVE_RATE);
    static {
        // 初始化布隆过滤器,假设数据库中存在的商品ID为1 - 10000
        for (int i = 1; i <= EXPECTED_ELEMENTS; i++) {
            bloomFilter.put(i);
        }
    }
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        int productId = 10001; // 假设不存在的商品ID
        if (!bloomFilter.mightContain(productId)) {
            System.out.println("数据大概率不存在,直接返回");
            return;
        }
        // 从Redis查询数据
        String cacheValue = jedis.get("product:" + productId);
        if (cacheValue == null) {
            // 缓存未命中,查询数据库(此处省略数据库查询逻辑)
            String dbValue = "模拟从数据库查询到的值";
            if (dbValue != null) {
                // 将数据存入Redis
                jedis.set("product:" + productId, dbValue);
            } else {
                // 数据库也不存在,设置一个空值缓存,避免后续重复查询数据库
                jedis.setex("product:" + productId, 60, "");
            }
        } else {
            System.out.println("缓存命中,返回数据");
        }
    }
}

(2)缓存空对象

当数据库查询结果为空时,也将空值存入 Redis 缓存,并设置较短的过期时间。后续相同请求可直接命中缓存,避免穿透到数据库。

import redis.clients.jedis.Jedis;
public class CacheNullObjectDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        int productId = 10001; // 假设不存在的商品ID
        // 从Redis查询数据
        String cacheValue = jedis.get("product:" + productId);
        if (cacheValue == null) {
            // 缓存未命中,查询数据库(此处省略数据库查询逻辑)
            String dbValue = null;
            if (dbValue != null) {
                // 将数据存入Redis
                jedis.set("product:" + productId, dbValue);
            } else {
                // 数据库也不存在,设置一个空值缓存,避免后续重复查询数据库
                jedis.setex("product:" + productId, 60, "");
                System.out.println("数据库中不存在该数据,已设置空值缓存");
            }
        } else {
            if (cacheValue.equals("")) {
                System.out.println("缓存命中空值,数据不存在");
            } else {
                System.out.println("缓存命中,返回数据");
            }
        }
    }
}

二、缓存击穿

1. 问题描述

缓存击穿指的是某个热点数据(如热门商品信息、高访问量接口数据)的缓存过期瞬间,大量并发请求同时访问该数据,导致请求直接落到数据库,造成数据库压力瞬间增大。

图中展示了缓存击穿的场景,热点数据缓存过期时,大量请求同时访问数据库。

2. 成因分析

  • 缓存过期时间集中:热点数据的缓存过期时间设置不合理,同时到期。
  • 高并发访问:大量用户同时请求同一热点数据。

3. 解决方案

(1)互斥锁

在缓存过期时,只允许一个线程去查询数据库并更新缓存,其他线程等待该线程更新完成后,直接从缓存获取数据。

示例代码

import redis.clients.jedis.Jedis;
import java.util.concurrent.locks.ReentrantLock;
public class CacheBreakdownMutexDemo {
    private static final ReentrantLock lock = new ReentrantLock();
    private static final int CACHE_EXPIRE_TIME = 60; // 缓存过期时间,单位:秒
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        int productId = 1; // 假设热门商品ID
        // 从Redis查询数据
        String cacheValue = jedis.get("product:" + productId);
        if (cacheValue == null) {
            lock.lock();
            try {
                // 再次检查缓存,避免多个线程重复查询数据库
                cacheValue = jedis.get("product:" + productId);
                if (cacheValue == null) {
                    // 缓存未命中,查询数据库(此处省略数据库查询逻辑)
                    String dbValue = "模拟从数据库查询到的热门商品数据";
                    if (dbValue != null) {
                        // 将数据存入Redis
                        jedis.setex("product:" + productId, CACHE_EXPIRE_TIME, dbValue);
                        System.out.println("缓存更新成功");
                    }
                }
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("缓存命中,返回数据");
        }
    }
}

(2)逻辑过期

给缓存数据设置一个逻辑过期时间,当缓存数据即将过期时,后台异步线程提前更新缓存,避免大量请求直接访问数据库。

import redis.clients.jedis.Jedis;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class CacheBreakdownLogicExpireDemo {
    private static final int CACHE_EXPIRE_TIME = 60; // 缓存过期时间,单位:秒
    private static final int LOGIC_EXPIRE_TIME = 5; // 逻辑过期时间,单位:秒
    private static final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        int productId = 1; // 假设热门商品ID
        // 从Redis查询数据
        String cacheValue = jedis.get("product:" + productId);
        if (cacheValue == null) {
            // 缓存未命中,查询数据库(此处省略数据库查询逻辑)
            String dbValue = "模拟从数据库查询到的热门商品数据";
            if (dbValue != null) {
                // 设置逻辑过期时间和缓存数据
                jedis.setex("product:" + productId, CACHE_EXPIRE_TIME, dbValue);
                jedis.setex("product:" + productId + ":expire", LOGIC_EXPIRE_TIME, "1");
                // 启动异步线程更新缓存
                executorService.schedule(() -> {
                    String newDbValue = "模拟从数据库查询到的最新热门商品数据";
                    if (newDbValue != null) {
                        jedis.setex("product:" + productId, CACHE_EXPIRE_TIME, newDbValue);
                        jedis.setex("product:" + productId + ":expire", LOGIC_EXPIRE_TIME, "1");
                        System.out.println("缓存异步更新成功");
                    }
                }, LOGIC_EXPIRE_TIME, TimeUnit.SECONDS);
                System.out.println("缓存更新成功");
            }
        } else {
            // 检查逻辑过期时间
            String expireFlag = jedis.get("product:" + productId + ":expire");
            if (expireFlag == null) {
                System.out.println("缓存命中,返回数据");
            } else {
                // 逻辑过期,返回旧数据,等待异步线程更新
                System.out.println("缓存逻辑过期,返回旧数据");
            }
        }
    }
}

三、缓存雪崩

1. 问题描述

缓存雪崩是指由于 Redis 缓存中的大量数据同时过期或 Redis 服务宕机,导致大量请求直接落到数据库,造成数据库负载过高,甚至崩溃。

图中展示了缓存雪崩的情况,大量缓存数据同时失效,请求如潮水般涌向数据库。

2. 成因分析

  • 缓存过期时间集中:大量缓存数据设置了相同或相近的过期时间,导致同时失效。
  • Redis 故障:Redis 服务器发生故障,无法提供服务。

3. 解决方案

(1)均匀设置过期时间

在设置缓存过期时间时,添加一个随机时间偏移,避免大量数据同时过期。

示例代码

import redis.clients.jedis.Jedis;
import java.util.Random;
public class CacheAvalancheRandomExpireDemo {
    private static final int BASE_EXPIRE_TIME = 60; // 基础过期时间,单位:秒
    private static final int RANDOM_OFFSET = 10; // 随机偏移时间,单位:秒
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        int productId = 1; // 假设商品ID
        // 设置随机过期时间
        int expireTime = BASE_EXPIRE_TIME + new Random().nextInt(RANDOM_OFFSET);
        String data = "模拟商品数据";
        jedis.setex("product:" + productId, expireTime, data);
        System.out.println("缓存设置成功,过期时间:" + expireTime + "秒");
    }
}

(2)多级缓存

采用本地缓存(如 Guava Cache、Caffeine)和 Redis 缓存相结合的方式。当 Redis 缓存失效时,先从本地缓存获取数据,减轻数据库压力。同时,可使用 Redis 集群提高缓存服务的可用性。

(3)服务熔断与降级

当数据库压力过大时,启用服务熔断机制,暂时拒绝部分请求;或者进行服务降级,返回默认数据或提示信息,保证核心服务的可用性。

四、总结

缓存穿透、缓存击穿和缓存雪崩是 Redis 应用中常见的性能问题,通过合理运用布隆过滤器、互斥锁、随机过期时间等技术手段,可以有效解决这些问题。在实际开发中,需要根据业务场景选择合适的解决方案,同时结合监控和预警机制,保障系统的稳定性和可靠性。


http://www.hkcw.cn/article/rkwPwSskrT.shtml

相关文章

论文阅读笔记——Quo Vadis, Action Recognition? A New Model and the Kinetics Dataset

I3D 论文 UCF-101&#xff08;13000多个视频&#xff09;和 HMDB-51&#xff08;7000多个视频&#xff09;数据集过小&#xff0c;提出了 Kinetics 数据集&#xff0c;并且在其之上预训练之后能够迁移到其他小的数据集。 2DLSTM&#xff1a;使用2D CNN的好处是可以直接从 Ima…

Azure devops 系统之五-部署ASP.NET web app

今天介绍如何通过vscode 来创建一个asp.net web app,并部署到azure 上。 创建 ASP.NET Web 应用 在您的计算机上打开一个终端窗口并进入工作目录。使用 dotnet new webapp 命令创建一个新的 .NET Web 应用,然后将目录切换到新创建的应用。 dotnet new webapp -n MyFirstAzu…

slider滑块async await

isselectionrangeenabled 特色属性 实际场景 视频缓存进度和观看进度 scrollbar 也可以 <StackPanel> <!-- 选择范围头尾相同&#xff0c;但 Slider 仍可操作 --> <Slider IsSelectionRangeEnabled"True" SelectionStart"30" SelectionEn…

LangChain-结合GLM+SQL+函数调用实现数据库查询(一)

业务流程 实现步骤 1. 加载数据库配置 在项目的根目录下创建.env 文件&#xff0c;设置文件内容&#xff1a; DB_HOSTxxx DB_PORT3306 DB_USERxxx DB_PASSWORDxxx DB_NAMExxx DB_CHARSETutf8mb4 加载环境变量&#xff0c;从 .env 文件中读取数据库配置信息 使用 os.getenv…

性能优化 - 工具篇:常用的性能测试工具

文章目录 Pre1. 常用的性能测试工具2. nmon —— 获取系统级性能数据2.1 安装与启动2.2 采样并生成报表 3. jvisualvm —— 获取 JVM 性能数据3.1 启动与连接3.2 CPU 分析&#xff08;Sampler & Profiler&#xff09;3.3 内存监视与 Heap Dump3.4 线程分析 4. JMC&#xff…

箱式不确定集

“箱式不确定集&#xff08;Box Uncertainty Set&#xff09;”可以被认为是一种 相对简单但实用的不确定集建模方式。 ✅ 一、什么是“简单的不确定集”&#xff1f; 在鲁棒优化领域&#xff0c;“简单不确定集”通常指的是&#xff1a; 特点描述形式直观数学表达简洁&#…

GoldenEye

GoldenEye: 1 About Release Back to the Top Name: GoldenEye: 1Date release: 4 May 2018Author: creosoteSeries: GoldenEye 下载&#xff1a; GoldenEye-v1.ova (Size: 805 MB)Download: https://drive.google.com/open?id1M7mMdSMHHpiFKW3JLqq8boNrI95Nv4tqDownload (Mir…

[AI算法] 什么事RoPE scaling

文章目录 RopeScaling 的作用&#x1f4a1; RopeScaling 的核心思想&#xff1a; 常见的 RoPE Scaling 方法Dynamic NTK-Aware Scaling核心原理实现方式&#xff08;伪代码示例&#xff09;优点与效果应用场景总结对比表 YaRN技术 RopeScaling 的作用 ✅ 场景背景&#xff1a;…

功能丰富的PDF处理免费软件推荐

软件介绍 今天给大家介绍一款超棒的PDF工具箱&#xff0c;它处理PDF文档的能力超强&#xff0c;而且是完全免费使用的&#xff0c;没有任何限制。 TinyTools&#xff08;PC&#xff09;这款软件&#xff0c;下载完成后即可直接打开使用。在使用过程中&#xff0c;操作完毕后&a…

统一多模态预训练中的涌现特性

25年5月来自字节、深圳先进技术研究院、Monash 大学、香港科大和 UC Santa Cruz 的论文“Emerging Properties in Unified Multimodal Pretraining”。 统一多模态理解与生成已在尖端专有系统中展现出令人瞩目的能力。本研究的 BAGEL&#xff0c;是一个原生支持多模态理解与生…

从认识AI开始-----Transformer:大模型的核心架构

前言 在NLP领域中&#xff0c;RNN、LSTM及GRU曾是处理序列问题的主力模型&#xff0c;但它们都面临着两个关键问题&#xff1a; 无法并行计算&#xff1a;序列数据需要完成前一步后再处理下一步&#xff0c;这会使得训练效率低下长期依赖问题&#xff1a;即使是LSTM、GRU&…

Mnist手写数字

运行实现&#xff1a; import torch from torch.utils.data import DataLoader from torchvision import transforms from torchvision.datasets import MNIST import matplotlib.pyplot as pltclass Net(torch.nn.Module):#net类神经网络主体def __init__(self):#4个全链接层…

win32相关(互斥体)

互斥体 内核级临界资源怎么处理&#xff1f; 有两个不同进程中的线程&#xff0c;访问内核中的临界资源&#xff0c;该怎么实现线程安全 互斥体其实就是一个内核级的跨进程访问令牌&#xff0c;与在同一个进程中的临界区不同的是&#xff0c;同一个进程中的不同线程&#xff0c…

【配置vscode默认终端为git bash】

配置vscode默认终端为git bash 点击左下角小齿轮&#xff0c;点击设置&#xff0c;搜索terminal.integrated.profiles.windows&#xff0c;点击在setting.json中编辑 第一部分是当前的所有的终端&#xff0c;第二部分是配置默认的终端"terminal.integrated.defaultProfi…

C# 序列化技术全面解析:原理、实现与应用场景

在软件开发中&#xff0c;数据持久化和网络通信是两个至关重要的环节。想象一下&#xff0c;当我们需要将一个复杂的对象保存到文件中&#xff0c;或者通过网络发送到另一台计算机时&#xff0c;如何有效地表示这个对象&#xff1f;这就是序列化技术要解决的问题。序列化&#…

如何检查popover气泡组件样式?调试悬停元素CSS样式的解决方案

1. 问题 当我们要检查这种弹出层的CSS样式时&#xff0c;会发现特别棘手&#xff0c;因为鼠标移走就消失了。如果是display:none控制的&#xff0c;可能还能找到&#xff0c;如果是用js通过v-if控制的&#xff0c;就无法调试了。 2. 解决方案 使用 setTimeout debugger 就…

DDR5 ECC详细原理介绍与基于协议讲解

本文篇幅较长,涉及背景原理介绍方便大家理解其运作方式 以及 基于DDR5协议具体展开介绍。 背景原理介绍 上图参考:DDR 内存中的 ECC 写入操作时,On-die ECC的工作过程如下: SoC将需要写入到Memory中的数据发送给控制器控制器将需要写入的数据直接发送给DRAM芯片在DDR5 DR…

设计模式——外观设计模式(结构型)

摘要 本文介绍了外观设计模式&#xff0c;它是一种结构型设计模式&#xff0c;通过引入一个外观类来封装复杂子系统的调用细节&#xff0c;对外提供简单统一的接口。文中通过生活类比、关键角色介绍、使用场景分析以及结构说明等方面对这一模式进行了全面阐述&#xff0c;还涉…

计算机网络(5)——数据链路层

1.概述 数据链路层负责一套链路上从一个节点向另一个物理链路直接相连的相邻节点传输数据报。换言之&#xff0c;主要解决相邻节点间的可靠数据传输 节点(nodes)&#xff1a;路由器和主机 链路(links)&#xff1a;连接相邻节点的通信信道 2.数据链路层服务 2.1 组帧 组帧(fra…

深度优先搜索(DFS)邻接矩阵实现

代码&#xff1a; // 访问标记数组&#xff0c;需要提前初始化为false bool visited[MAX_VERTEX_NUM]; void DFS(AMGraph G, int v) { // 图G为邻接矩阵类型&#xff0c;v是当前访问的顶点// 步骤1&#xff1a;访问顶点vcout << v; // 输出顶点编号…