Redis 缓存穿透、缓存击穿与缓存雪崩详解:问题、解决方案与最佳实践

devtools/2025/3/14 23:09:40/

目录

引言

1. 缓存穿透

1.1 什么是缓存穿透?

示例:

1.2 缓存穿透的原因

1.3 缓存穿透的解决方案

1.3.1 缓存空对象

1.3.2 布隆过滤器(Bloom Filter)

1.3.3 参数校验

2. 缓存击穿

2.1 什么是缓存击穿?

示例:

2.2 缓存击穿的原因

2.3 缓存击穿的解决方案

2.3.1 互斥锁(Mutex Lock)

2.3.2 永不过期 + 后台更新

2.3.3 缓存预热

3. 缓存雪崩

3.1 什么是缓存雪崩?

示例:

3.2 缓存雪崩的原因

3.3 缓存雪崩的解决方案

3.3.1 设置随机过期时间

3.3.2 多级缓存

3.3.3 限流与降级

4. 缓存穿透、缓存击穿与缓存雪崩的区别

5. 最佳实践

6. 总结


引言

在使用 Redis 作为缓存系统时,缓存穿透缓存击穿缓存雪崩是三个常见的问题。它们不仅会影响系统的性能,还可能导致数据库压力过大甚至系统崩溃。本文将深入探讨这三种问题的定义、原因、解决方案以及最佳实践,并通过 Java 代码示例 帮助读者全面理解并有效应对这些问题。


1. 缓存穿透

1.1 什么是缓存穿透?

缓存穿透是指查询一个 不存在的数据,导致请求直接穿透缓存层,直接访问数据库。由于数据库中也不存在该数据,因此每次请求都会绕过缓存,直接访问数据库,从而导致数据库压力过大。

示例:
  • 用户请求一个不存在的商品 ID,缓存中没有该数据,请求直接打到数据库。

  • 恶意攻击者故意请求大量不存在的数据,导致数据库压力激增。

1.2 缓存穿透的原因

  1. 恶意攻击:攻击者故意请求大量不存在的数据。

  2. 业务逻辑问题:业务代码未对请求参数进行校验,导致非法请求直接访问数据库。

1.3 缓存穿透的解决方案

1.3.1 缓存空对象

当查询数据库发现数据不存在时,将空结果(如 null缓存到 Redis 中,并设置一个较短的过期时间。这样,后续相同的请求可以直接从缓存中获取空结果,避免直接访问数据库。

import redis.clients.jedis.Jedis;public class CachePenetration {private Jedis redis;private Database db;public CachePenetration(Jedis redis, Database db) {this.redis = redis;this.db = db;}public String getData(String key) {// 从缓存中获取数据String data = redis.get(key);if (data != null) {return "NULL".equals(data) ? null : data; // 返回空结果}// 从数据库中查询数据data = db.query(key);if (data == null) {redis.setex(key, 300, "NULL"); // 缓存空对象,过期时间 300 秒return null;}redis.setex(key, 3600, data); // 缓存真实数据,过期时间 1 小时return data;}
}
1.3.2 布隆过滤器(Bloom Filter)

布隆过滤器是一种概率型数据结构,用于快速判断一个元素是否存在于集合中。它可以有效过滤掉不存在的数据请求,避免缓存穿透。

  • 优点:内存占用少,查询效率高。

  • 缺点:存在一定的误判率(False Positive),但可以通过调整参数降低误判率。

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import redis.clients.jedis.Jedis;public class CachePenetration {private Jedis redis;private Database db;private BloomFilter<String> bloomFilter;public CachePenetration(Jedis redis, Database db) {this.redis = redis;this.db = db;this.bloomFilter = BloomFilter.create(Funnels.stringFunnel(), 1000000, 0.001);}public String getData(String key) {// 使用布隆过滤器判断 key 是否存在if (!bloomFilter.mightContain(key)) {return null; // 如果 key 不在布隆过滤器中,直接返回}// 从缓存中获取数据String data = redis.get(key);if (data != null) {return data;}// 从数据库中查询数据data = db.query(key);if (data == null) {redis.setex(key, 300, "NULL"); // 缓存空对象return null;}redis.setex(key, 3600, data); // 缓存真实数据return data;}
}
1.3.3 参数校验

在业务逻辑中对请求参数进行校验,过滤掉非法请求。例如,检查商品 ID 是否为正整数,或者是否符合某种格式。

public class CachePenetration {private Jedis redis;private Database db;public CachePenetration(Jedis redis, Database db) {this.redis = redis;this.db = db;}private boolean validateKey(String key) {try {int id = Integer.parseInt(key);return id > 0; // 检查 key 是否为正整数} catch (NumberFormatException e) {return false;}}public String getData(String key) {if (!validateKey(key)) {return null; // 非法请求直接返回}// 其他逻辑...return null;}
}

2. 缓存击穿

2.1 什么是缓存击穿?

缓存击穿是指 某个热点数据在缓存中过期,同时有大量并发请求访问该数据,导致所有请求直接访问数据库,从而导致数据库压力激增。

示例:
  • 某个热门商品的缓存过期,同时有大量用户请求该商品,导致数据库压力激增。

2.2 缓存击穿的原因

  1. 热点数据过期:某个热点数据的缓存过期。

  2. 高并发请求:大量并发请求同时访问该热点数据。

2.3 缓存击穿的解决方案

2.3.1 互斥锁(Mutex Lock)

缓存失效时,使用互斥锁确保只有一个线程去加载数据,其他线程等待加载完成后再从缓存中获取数据。

import redis.clients.jedis.Jedis;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class CacheBreakdown {private Jedis redis;private Database db;private Lock lock = new ReentrantLock();public CacheBreakdown(Jedis redis, Database db) {this.redis = redis;this.db = db;}public String getData(String key) {// 从缓存中获取数据String data = redis.get(key);if (data != null) {return data;}// 尝试获取锁if (lock.tryLock()) {try {// 从数据库中查询数据data = db.query(key);if (data != null) {redis.setex(key, 3600, data); // 更新缓存}} finally {lock.unlock(); // 释放锁}return data;} else {try {Thread.sleep(100); // 等待其他线程加载数据} catch (InterruptedException e) {Thread.currentThread().interrupt();}return getData(key); // 重试}}
}
2.3.2 永不过期 + 后台更新

对于热点数据,可以设置缓存永不过期,并通过后台任务定期更新缓存

import redis.clients.jedis.Jedis;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;public class CacheBreakdown {private Jedis redis;private Database db;public CacheBreakdown(Jedis redis, Database db) {this.redis = redis;this.db = db;// 启动后台任务定期更新缓存ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);scheduler.scheduleAtFixedRate(this::updateCache, 0, 1, TimeUnit.HOURS);}public String getData(String key) {// 从缓存中获取数据String data = redis.get(key);if (data != null) {return data;}// 从数据库中查询数据data = db.query(key);if (data != null) {redis.set(key, data); // 缓存永不过期}return data;}private void updateCache() {String hotData = db.queryHotData();redis.set("hot_data", hotData); // 更新缓存}
}
2.3.3 缓存预热

在系统启动或低峰期,提前加载热点数据到缓存中,避免缓存击穿。

import redis.clients.jedis.Jedis;public class CacheBreakdown {private Jedis redis;private Database db;public CacheBreakdown(Jedis redis, Database db) {this.redis = redis;this.db = db;preheatCache();}private void preheatCache() {String hotData = db.queryHotData();redis.set("hot_data", hotData); // 缓存预热}
}

3. 缓存雪崩

3.1 什么是缓存雪崩?

缓存雪崩是指 大量缓存数据在同一时间失效,导致大量请求直接访问数据库,从而导致数据库压力激增甚至崩溃。

示例:
  • 缓存中的数据设置了相同的过期时间,导致大量数据在同一时间失效。

  • Redis 实例宕机,导致所有缓存失效。

3.2 缓存雪崩的原因

  1. 缓存集中失效缓存中的数据设置了相同的过期时间。

  2. Redis 实例宕机:Redis 服务不可用,导致所有缓存失效。

  3. 热点数据失效:某些热点数据的缓存失效,导致大量请求直接访问数据库。

3.3 缓存雪崩的解决方案

3.3.1 设置随机过期时间

缓存数据设置随机的过期时间,避免大量缓存数据在同一时间失效。

import redis.clients.jedis.Jedis;
import java.util.Random;public class CacheAvalanche {private Jedis redis;private Database db;private Random random = new Random();public CacheAvalanche(Jedis redis, Database db) {this.redis = redis;this.db = db;}public void setCache(String key, String value) {int expireTime = 3600 + random.nextInt(600); // 过期时间在 1 小时到 1 小时 10 分钟之间redis.setex(key, expireTime, value);}
}
3.3.2 多级缓存

使用多级缓存架构(如本地缓存 + Redis 缓存),即使 Redis 缓存失效,本地缓存仍然可以提供服务。

import redis.clients.jedis.Jedis;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;public class CacheAvalanche {private Jedis redis;private Database db;private Map<String, String> localCache = new ConcurrentHashMap<>();public CacheAvalanche(Jedis redis, Database db) {this.redis = redis;this.db = db;}public String getData(String key) {// 先从本地缓存获取String data = localCache.get(key);if (data != null) {return data;}// 再从 Redis 缓存获取data = redis.get(key);if (data != null) {localCache.put(key, data); // 更新本地缓存return data;}// 最后从数据库获取data = db.query(key);if (data != null) {redis.setex(key, 3600, data); // 更新 Redis 缓存localCache.put(key, data); // 更新本地缓存}return data;}
}
3.3.3 限流与降级

缓存雪崩发生时,通过限流和降级机制保护数据库。例如,使用限流工具(如 Redis 的 INCR 命令)限制请求速率,或者返回默认值或错误页面。

import redis.clients.jedis.Jedis;public class CacheAvalanche {private Jedis redis;private Database db;public CacheAvalanche(Jedis redis, Database db) {this.redis = redis;this.db = db;}public String getData(String key) {// 限流:每秒最多处理 100 个请求if (redis.incr("request_rate") > 100) {return "Too many requests, please try again later.";}// 其他逻辑...return null;}
}

4. 缓存穿透、缓存击穿与缓存雪崩的区别

特性缓存穿透缓存击穿缓存雪崩
定义查询不存在的数据,导致请求直接访问数据库热点数据缓存失效,导致大量请求直接访问数据库大量缓存数据在同一时间失效,导致请求直接访问数据库
原因恶意攻击或业务逻辑问题热点数据过期或高并发请求缓存集中失效或 Redis 实例宕机
影响数据库压力过大数据库压力激增数据库压力激增甚至崩溃
解决方案缓存空对象、布隆过滤器、参数校验互斥锁、永不过期 + 后台更新、缓存预热设置随机过期时间、多级缓存、限流与降级

5. 最佳实践

  1. 合理设置缓存过期时间:避免缓存集中失效。

  2. 使用布隆过滤器:有效防止缓存穿透。

  3. 多级缓存架构:提高系统的容错能力。

  4. 限流与降级机制:保护数据库不被压垮。

  5. 监控与报警:实时监控缓存命中率和数据库负载,及时发现并解决问题。


6. 总结

缓存穿透、缓存击穿和缓存雪崩是 Redis 使用过程中常见的问题,它们会导致数据库压力过大甚至系统崩溃。通过合理的设计和优化,可以有效避免这些问题:

  • 缓存穿透:通过缓存空对象、布隆过滤器和参数校验来解决。

  • 缓存击穿:通过互斥锁、永不过期 + 后台更新和缓存预热来解决。

  • 缓存雪崩:通过设置随机过期时间、多级缓存和限流降级来解决。


http://www.ppmy.cn/devtools/167133.html

相关文章

Docker基础命令说明

Docker基础操作命令众多&#xff0c;这些命令可以按如下方式进行分类&#xff1a; 镜像操作容器操作网络操作数据卷操作LOG查询 等方面进行分类。 一、镜像操作命令 docker images&#xff1a;用于列出本地系统中所有的 Docker 镜像。镜像就像是一个模板&#xff0c;它包含…

vue中,watch里,this为undefined的两种解决办法

提示&#xff1a;vue中&#xff0c;watch里&#xff0c;this为undefined的两种解决办法 文章目录 [TOC](文章目录) 前言一、问题二、方法1——使用function函数代替箭头函数()>{}三、方法2——使用that总结 前言 ‌‌‌‌‌尽量使用方法1——使用function函数代替箭头函数()…

TCP/IP原理详细解析

前言 TCP/IP是一种面向连接&#xff0c;可靠的传输&#xff0c;传输数据大小无限制的。通常情况下&#xff0c;系统与系统之间的http连接需要三次握手和四次挥手&#xff0c;这个执行过程会产生等待时间。这方面在日常开发时需要注意一下。 TCP/IP 是互联网的核心协议族&…

第13章贪心算法

贪心算法 局部最优求得总体最优 适用于桌上有6张纸币&#xff0c;面额为100 100 50 50 50 10&#xff0c;问怎么能拿走3张纸币&#xff0c;总面额最大&#xff1f;—拿单位价值最高的 只关注局部最优----关注拿一张的最大值拆解-----拿三次最大的纸币 不适用于桌面三件物品&am…

RabbitMQ五种消息模型

RabbitMQ 是一款基于 AMQP 协议的高性能消息中间件&#xff0c;广泛应用于分布式系统中&#xff0c;用于实现服务之间的异步通信、解耦和负载均衡。RabbitMQ 提供了五种常见的消息模型&#xff0c;每种模型都有其独特的特点和适用场景。本文将详细介绍这五种消息模型&#xff0…

在 VMware 中安装 Ubuntu 的超详细实战分享

目录 1. 安装准备VMware 软件获取Ubuntu 镜像获取 2. 创建新的虚拟机基础配置自定义硬件设置 3. Ubuntu 系统安装过程启动虚拟机正式安装 Ubuntu安装过程中常见问题 4. 安装后优化安装 VMware Tools系统更新与软件安装分辨率与显示设置 5. 常见故障及解决方案黑屏或安装卡顿网络…

接口测试工具:postman详解

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 Postman 是一款功能强大的 API 开发和测试工具&#xff0c;以下是一些高级用法的详细介绍和操作步骤。 一、环境和全局变量 环境变量允许你设置特定于环境&#…

Gartner发布量子网络安全策略指南:2030年量子计算将能够破坏传统的加密算法

攻击者采用“先收集后解密”策略&#xff0c;为企业带来隐患。加密数据流目前无法读取&#xff0c;但可以保存&#xff0c;直到量子计算能够解密。I&O 领导者可以通过实施后量子密码学策略来降低这种风险。 主要发现 密码相关量子计算机 (CRQC) 将能够在数小时而不是数年内…