当前位置: 首页 > article >正文

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

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

相关文章:

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

在分布式系统中&#xff0c;Redis 凭借高性能和高并发处理能力&#xff0c;成为常用的缓存组件。然而&#xff0c;在实际应用中&#xff0c;缓存穿透、缓存击穿、缓存雪崩这三大问题会严重影响系统的性能与稳定性。本文将详细解析这三个问题的成因&#xff0c;并提供对应的解决…...

sass高阶应用

Sass(尤其是 SCSS 语法)除了基础功能外,还提供了许多高级特性,可以实现更灵活、可维护的样式系统。以下是 Sass 的 高级语法和应用技巧,适合中大型项目或组件库开发。 文章目录 一、控制指令(Control Directives)1. `@if / @else`2. `@for` 循环3. `@each` 遍历列表/Map…...

docker docker-ce docker.io

Ubuntu安装 ​​更新软件包列表​​ 首先确保软件包列表是最新的&#xff1a; sudo apt-get update 使用正确的卸载命令​​ 替换 docker-engine 为 docker-ce 或 docker.io&#xff1a; sudo apt-get remove docker docker-ce docker.io containerd runc ​​检查已安装的 Do…...

DQN和DDQN(进阶版)

来源&#xff1a; *《第五章 深度强化学习 Q网络》.ppt --周炜星、谢文杰 一、前言 Q表格、Q网络与策略函数 Q表格是有限的离散的&#xff0c;而神经网络可以是无限的。 对于动作有限的智能体来说&#xff0c;使用Q网络获得当下状态的对于每个动作的 状态-动作值 。那么 a…...

【组件】翻牌器效果

目录 效果组件代码背景素材 效果 组件代码 <template><divclass"card-flop":style"{height: typeof height number ? ${height}px : height,--box-width: typeof boxWidth number ? ${boxWidth}px : boxWidth,--box-height: typeof boxHeight nu…...

CentOS 7 环境中部署 LNMP(Linux + Nginx + MySQL 5.7 + PHP)

在 CentOS 7 环境中部署 LNMP&#xff08;Linux Nginx MySQL 5.7 PHP&#xff09; 环境的详细步骤如下。此方案确保各组件版本兼容&#xff0c;并提供完整的配置验证流程。 1. 更新系统 sudo yum update -y 2. 安装 MySQL 5.7 2.1 添加 MySQL 官方 YUM 仓库 由于MySQL并不…...

NX811NX816美光颗粒固态NX840NX845

NX811NX816美光颗粒固态NX840NX845 美光NX系列固态硬盘颗粒深度解析&#xff1a;技术、性能与市场全景透视 一、技术架构与核心特性解析 1. NX811/NX816&#xff1a;入门级市场的平衡之选 技术定位&#xff1a;基于176层TLC&#xff08;Triple-Level Cell&#xff09;3D NAN…...

捋捋wireshark

本猿搬砖时会用到wireshark分析pcap包&#xff0c;但频率不高&#xff0c;记过一些笔记&#xff0c;今天捋捋&#xff0c;希望能给初学者节省一点时间。 wireshark是个网络封包分析软件&#xff08;network packet analyzer&#xff09;&#xff0c;可以用来抓流量包&#xff…...

c++学习之---模版

目录 一、函数模板&#xff1a; 1、基本定义格式&#xff1a; 2、模版函数的优先匹配原则&#xff1a; 二、类模板&#xff1a; 1、基本定义格式&#xff1a; 2、类模版的优先匹配原则&#xff08;有坑哦&#xff09;&#xff1a; 3、缺省值的设置&#xff1a; 4、ty…...

MyBatis-Flex 全面指南:下一代轻量级持久层框架实战入门

&#x1f680; MyBatis-Flex 全面指南&#xff1a;下一代轻量级持久层框架实战入门 本文将带你全面了解 MyBatis-Flex 的特性、常见用法、最佳实践&#xff0c;帮助你高效构建更简洁、更灵活的 Java 持久层代码。 &#x1f9e9; 什么是 MyBatis-Flex&#xff1f; MyBatis-Flex…...

第十六章 EMQX黑名单与连接抖动检测

系列文章目录 第一章 总体概述 第二章 在实体机上安装ubuntu 第三章 Windows远程连接ubuntu 第四章 使用Docker安装和运行EMQX 第五章 Docker卸载EMQX 第六章 EMQX客户端MQTTX Desktop的安装与使用 第七章 EMQX客户端MQTTX CLI的安装与使用 第八章 Wireshark工具的安装与使用 …...

WebSphere(WAS)

WebSphere &#xff08;WebSphere Application Server&#xff09;为 SOA 环境提供软件&#xff0c;以实现动态的、互联的业务流程&#xff0c;为所有业务情形提供高度有效的应用程序基础架构。WebSphere 是 IBM 的应用程序和集成软件平台&#xff0c;包含所有必要的中间件基础…...

新编辑器编写指南--给自己的备忘

欢迎使用Markdown编辑器 你好&#xff01; 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章&#xff0c;了解一下Markdown的基本语法知识。 新的改变 我们对Markdown编辑器进行了一些功能拓展与语法支持&#x…...

xPSR

在 ARM Cortex-M3 中&#xff0c;​xPSR&#xff08;组合程序状态寄存器&#xff09;​​ 是核心的状态控制寄存器&#xff0c;由三个子状态寄存器合并而成&#xff0c;用于记录处理器的运算状态、中断状态和执行环境。以下是其深度解析&#xff1a; &#x1f50d; ​一、xPSR …...

鸿蒙网络数据传输案例实战

一、案例效果截图 二、案例运用到的知识点 核心知识点 网络连接管理&#xff1a;connection模块HTTP数据请求&#xff1a;http模块RPC数据请求&#xff1a;rcp模块文件管理能力&#xff1a;fileIo模块、fileUri模块 其他知识点 ArkTS 语言基础V2版状态管理&#xff1a;Comp…...

【JavaEE】-- 网络原理

文章目录 1. 网络发展史1.1 广域网1.2 局域网 2. 网络通信基础2.1 IP地址2.2 端口号2.3 认识协议2.4 五元组2.5 协议分层2.5.1 分层的作用2.5.2 OSI七层模型&#xff08;教科书&#xff09;2.5.3 TCP/IP五层&#xff08;或四层&#xff09;模型&#xff08;工业中常用&#xff…...

1.RV1126-OPENCV 交叉编译

一.下载opencv-3.4.16.zip到自己想装的目录下 二.解压并且打开 opencv 目录 先用 unzip opencv-3.4.16.zip 来解压 opencv 的压缩包&#xff0c;并且进入 opencv 目录(cd opencv-3.4.16) 三. 修改 opencv 的 cmake 脚本的内容 先 cd platforms/linux 然后修改 arm-gnueabi.to…...

PySide6 GUI 学习笔记——常用类及控件使用方法(标签控件QLabel)

文章目录 标签控件QLabel及其应用举例标签控件QLabel的常用方法及信号应用举例Python 代码示例1Python 代码示例2 小结 标签控件QLabel及其应用举例 QLabel 是 PySide6.QtWidgets 模块中的一个控件&#xff0c;用于在界面上显示文本或图像。它常用于作为标签、提示信息或图片展…...

CSS (mask)实现服装动态换色:创意与技术的完美融合

在网页开发中&#xff0c;我们常常会遇到需要对图片元素进行个性化处理的需求&#xff0c;比如改变图片中特定部分的颜色。今天&#xff0c;我们就来探讨一种通过 CSS 和 JavaScript 结合&#xff0c;实现服装动态换色的有趣方法。 一、代码整体结构分析 上述代码构建了一个完…...

基于51单片机的音乐盒汽车喇叭调音量proteus仿真

地址&#xff1a; https://pan.baidu.com/s/1l3CSSMi4uMV5-XLefnKoSg 提取码&#xff1a;1234 仿真图&#xff1a; 芯片/模块的特点&#xff1a; AT89C52/AT89C51简介&#xff1a; AT89C51 是一款常用的 8 位单片机&#xff0c;由 Atmel 公司&#xff08;现已被 Microchip 收…...

彻底理解Spring三级缓存机制

文章目录 前言一、Spring解决循环依赖时&#xff0c;为什么要使用三级缓存&#xff1f; 前言 Spring解决循环依赖的手段&#xff0c;是通过三级缓存&#xff1a; singletonObjects&#xff1a;存放所有生命周期完整的单例对象。&#xff08;一级缓存&#xff09;earlySingleto…...

MacOs 安装局域网 gitlab 记录

1、安装git brew install git > Downloading https://homebrew.bintray.com/bottles/git-2.7.0.el_capitan.bottle ######################################################################## 100.0% > Pouring git-2.7.0.el_capitan.bottle.tar.gz > Caveats The O…...

Flutter 与 Android 原生布局组件对照表(完整版)

本对照表用于帮助 Android 开发者快速理解 Flutter 中的布局组件与原生布局的关系。 &#x1f4d8; Flutter ↔ Android 布局组件对照表 Flutter WidgetAndroid View/Layout说明ContainerFrameLayout / View通用容器&#xff0c;可设置背景、边距、对齐等RowLinearLayout (hor…...

【产品经理从0到1】自媒体端产品设计

后台的定义 “后台” 与“前台”都是相对独立的平台&#xff0c;前台是服务于互联网用户的平台 &#xff0c;后台主要是支撑前台页面内容、数据及对前台业务情况的统计分析的系统&#xff1b; 后台与前台的区别 第1&#xff1a;使用用户不同 前台用户&#xff1a;互联网用户…...

017搜索之深度优先DFS——算法备赛

深度优先搜索 如果说广度优先搜索是逐层扩散&#xff0c;那深度优先搜索就是一条道走到黑。 深度优先遍历是用递归实现的&#xff0c;预定一条顺序规则&#xff08;如上下左右顺序&#xff09; &#xff0c;一直往第一个方向搜索直到走到尽头或不满足要求后返回上一个叉路口按…...

解决 maven编译项目-Fatal error compiling: 无效的目标发行版: 21 -> [Help 1]

目录 1. 问题描述 2. 排查思路 3. 设置-指定maven使用jdk21 4. 参考资料 1. 问题描述 在idea中使用maven编译时,在系统环境变量中已经设置了jdk为21,但是在执行mvn package时,确提示 Fatal error compiling: 无效的目标发行版: 21 -> [Help 1] [ERROR] Failed to e…...

Thinkphp6实现websocket

项目需要连接一台自动售货机&#xff0c;售货机要求两边用websocket连接,监听9997端口。本文实现了一个基于PHP的WebSocket服务器&#xff0c;用于连接自动售货机&#xff0c;支持start/stop/restart命令操作 1.新建文件 新建文件 /command/socket.php <?php namespace a…...

web-css

一.CSS选择器&#xff1a; 1.基础选择器 基本选择器&#xff1a; >.标签选择器 格式&#xff1a;标签名称{} >.类选择器&#xff08;重&#xff09; 格式&#xff1a;.class属性的值{} >.id选择器 格式&#xff1a;#id属性的值{} >.通配符&#xff08;表示所有&am…...

关于 smali:2. 从 Java 到 Smali 的映射

一、对照 Java 代码与 Smali 代码差异 1.1 方法调用差异&#xff1a;Java vs Smali Java 方法分类&#xff1a; 方法类型Java 示例Smali 指令特点说明静态方法Utils.print("hi")invoke-static没有 this 指针实例方法obj.show()invoke-virtual有 this&#xff0c;虚…...

三、zookeeper 常用shell命令

作者&#xff1a;IvanCodes 日期&#xff1a;2025年5月28日 专栏&#xff1a;Zookeeper教程 ZooKeeper Shell (zkCli.sh) 是与ZooKeeper服务器交互的核心工具。本教程将详细介绍常用命令&#xff0c;并重点解析ZooKeeper数据节点 (ZNode) 的特性与分类。 思维导图 一、连接 Zo…...