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

缓存三大问题深度解析:穿透、击穿与雪崩

缓存三大问题深度解析:穿透、击穿与雪崩

缓存技术是高并发系统中提升性能的关键手段,但在实际应用中,我们经常会遇到缓存雪崩、缓存穿透和缓存击穿这三大问题。下面将详细分析它们的产生原因、解决方案以及方案背后的原理和优缺点。

一、缓存穿透

1. 定义

缓存穿透是指查询一个根本不存在的数据,由于缓存中没有该数据,每次都会查询数据库,导致数据库压力过大的情况。这种情况可能是误操作,也可能是恶意攻击。

2. 产生原因

  1. 查询不存在的数据:正常业务中用户误操作查询了不存在的数据
  2. 恶意攻击:黑客通过大量请求不存在的数据,消耗系统资源
  3. 缓存设计缺陷:没有对空值进行缓存处理

3. 解决方案及原理分析

方案1:缓存空对象

public Object getFromCache(String key) {// 1. 查询缓存Object value = redisTemplate.opsForValue().get(key);if (value != null) {return value;}// 2. 查询数据库value = database.query(key);if (value != null) {// 数据库中有数据,正常缓存redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);} else {// 数据库中没有数据,缓存空对象redisTemplate.opsForValue().set(key, NULL_VALUE, 60, TimeUnit.SECONDS); // 空值过期时间较短}return value;
}

原理:当数据库中不存在某条记录时,我们将空值也缓存起来,但设置较短的过期时间
优点:实现简单,有效防止缓存穿透
缺点:占用额外的缓存空间,可能会有短期的数据不一致问题

方案2:布隆过滤器

// 布隆过滤器实现缓存穿透防护
public class CacheService {private BloomFilter<String> bloomFilter;@PostConstructpublic void initBloomFilter() {// 初始化布隆过滤器,预计元素数量为1000000,期望误判率为0.01bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 1000000, 0.01);// 将数据库中所有可能存在的key加载到布隆过滤器中List<String> allKeys = database.queryAllPossibleKeys();for (String key : allKeys) {bloomFilter.put(key);}}public Object getFromCache(String key) {// 先通过布隆过滤器快速判断key是否可能存在if (!bloomFilter.mightContain(key)) {return null; // 布隆过滤器说不存在,肯定不存在}// 布隆过滤器说可能存在,继续查询缓存和数据库Object value = redisTemplate.opsForValue().get(key);if (value == null) {value = database.query(key);if (value != null) {redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);}}return value;}
}

原理:布隆过滤器是一种空间效率很高的概率性数据结构,用于判断一个元素是否在集合中。它可能会误判(说存在的元素可能不存在),但不会漏判(说不存在的元素一定不存在)。
优点:空间效率高,适用于大数据量场景,误判率可控
缺点:有一定的误判率,需要额外维护布隆过滤器,不支持删除操作

方案3:接口层参数校验

// 在Controller层进行参数校验
@RestController
public class CacheController {@GetMapping("/data/{id}")public ResponseEntity<?> getData(@PathVariable String id) {// 参数合法性校验if (!isValidId(id)) {return ResponseEntity.badRequest().body("Invalid parameter");}// 正常业务逻辑return ResponseEntity.ok(cacheService.getFromCache(id));}private boolean isValidId(String id) {// 实现参数校验逻辑// 例如:检查ID格式、范围等return id != null && id.matches("^[A-Za-z0-9]{1,32}$");}
}

原理:在API接口层对请求参数进行严格校验,过滤掉明显不合理的请求,从源头避免恶意请求到达后端
优点:实现简单,直接拦截无效请求,保护后端系统
缺点:只能拦截部分明显的非法请求,对于伪装成合法格式的恶意请求无法有效拦截

二、缓存击穿

1. 定义

缓存击穿是指一个热点数据的缓存突然失效(例如过期),此时大量并发请求同时访问这个热点数据,直接打在数据库上,造成数据库瞬时压力激增的现象。

2. 产生原因

  1. 热点数据过期:某个被大量访问的热点数据缓存过期
  2. 高并发场景:系统存在大量并发请求访问同一数据
  3. 缓存重建耗时:从数据库查询并重建缓存的过程耗时较长

3. 解决方案及原理分析

方案1:互斥锁(分布式锁)


public Object getFromCacheWithLock(String key) {// 1. 尝试从缓存获取Object value = redisTemplate.opsForValue().get(key);if (value != null) {return value;}// 2. 缓存不存在,获取分布式锁String lockKey = "lock:" + key;if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS)) {try {// 3. 再次检查缓存,防止其他线程已更新value = redisTemplate.opsForValue().get(key);if (value == null) {// 4. 查询数据库并更新缓存value = database.query(key);if (value != null) {redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);}}} finally {// 5. 释放锁redisTemplate.delete(lockKey);}} else {// 6. 获取锁失败,短暂休眠后重试try {Thread.sleep(50);} catch (InterruptedException e) {Thread.currentThread().interrupt();}return getFromCacheWithLock(key); // 重试}return value;
}

原理:当缓存失效时,只有获取到锁的线程才能去查询数据库并重建缓存,其他线程等待或重试
优点:确保只有一个线程去查询数据库,有效防止缓存击穿
缺点:增加了系统响应时间,在高并发下可能导致线程阻塞

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

// 热点数据永不过期,通过定时任务更新
@Scheduled(fixedRate = 1800000) // 每30分钟更新一次热点数据
public void updateHotData() {List<HotItem> hotItems = database.queryHotItems();for (HotItem item : hotItems) {redisTemplate.opsForValue().set(item.getKey(), item.getValue());}
}

原理:对于热点数据,不设置过期时间,而是通过后台定时任务主动更新缓存
优点:避免了缓存过期带来的问题,保证热点数据始终可用
缺点:需要识别热点数据,可能占用较多缓存空间

方案3:预热缓存+定时续期

// 系统启动时预热缓存
@PostConstruct
public void warmupCache() {List<HotItem> hotItems = database.queryHotItems();for (HotItem item : hotItems) {// 设置稍长的过期时间redisTemplate.opsForValue().set(item.getKey(), item.getValue(), 7200, TimeUnit.SECONDS);}
}// 定时任务检查并续期即将过期的热点数据
@Scheduled(fixedRate = 300000) // 每5分钟检查一次
public void renewExpiringCache() {List<HotItem> hotItems = database.queryHotItems();for (HotItem item : hotItems) {// 使用EXPIRE命令的返回值判断剩余过期时间Long ttl = redisTemplate.getExpire(item.getKey(), TimeUnit.SECONDS);// 如果剩余时间小于阈值(如1小时),则续期if (ttl != null && ttl > 0 && ttl < 3600) {redisTemplate.expire(item.getKey(), 7200, TimeUnit.SECONDS);// 同时更新缓存值redisTemplate.opsForValue().set(item.getKey(), item.getValue(), 7200, TimeUnit.SECONDS);}}
}

原理:系统启动时预热热点数据,并通过定时任务为即将过期的热点数据续期,避免缓存失效
优点:在保持数据相对新鲜的同时,避免了热点数据过期导致的缓存击穿
缺点:实现复杂度增加,需要维护预热和续期逻辑

三、缓存雪崩

1. 定义

缓存雪崩是指在某一时间段内,缓存中的大量数据同时失效或Redis服务器宕机,导致所有请求直接打在数据库上,造成数据库压力骤增,甚至导致数据库宕机的现象。

2. 产生原因

  1. 大量缓存同时过期:缓存设置了相同的过期时间,导致在同一时间点大量缓存失效
  2. 缓存服务器宕机:Redis等缓存服务不可用,所有请求直接访问后端数据库
  3. 缓存预热不充分:系统重启或新服务上线时,缓存未及时加载数据

3. 解决方案及原理分析

方案1:设置随机过期时间

// 避免缓存雪崩:设置随机过期时间
int baseExpireTime = 3600; // 基础过期时间1小时
int randomExpireTime = new Random().nextInt(1800); // 随机增加0-30分钟
redisTemplate.opsForValue().set("key", value, baseExpireTime + randomExpireTime, TimeUnit.SECONDS);

原理:通过为不同缓存项设置不同的过期时间,避免大量缓存同时失效,将缓存失效时间分散到不同时间段
优点:实现简单,效果明显,无需额外组件
缺点:无法完全避免缓存失效的情况,只是降低了集中失效的概率

方案2:多级缓存架构


// 多级缓存架构示例(伪代码)
public Object getFromCache(String key) {// 1. 先查本地缓存(Caffeine)Object value = localCache.getIfPresent(key);if (value != null) return value;// 2. 再查分布式缓存(Redis)value = redisCache.get(key);if (value != null) {// 回写本地缓存localCache.put(key, value);return value;}// 3. 最后查数据库value = database.query(key);// 更新缓存redisCache.set(key, value, getRandomExpireTime());localCache.put(key, value);return value;
}

原理:构建本地缓存(L1)和分布式缓存(L2)的多级架构,当L2缓存失效时,L1缓存可以作为兜底方案
优点:提高了系统可用性,即使分布式缓存失效,本地缓存仍可提供服务
缺点:实现复杂度增加,本地缓存可能导致数据一致性问题

方案3:设置热点数据永不过期

// 热点数据永不过期,通过后台定时更新
@Scheduled(fixedRate = 3600000) // 每小时更新一次
public void updateHotCache() {List<HotData> hotDataList = database.queryHotData();for (HotData data : hotDataList) {redisTemplate.opsForValue().set(data.getKey(), data.getValue());}
}

原理:对于核心业务的热点数据,不设置过期时间,通过后台定时任务定期更新缓存数据
优点:确保热点数据始终可用,避免缓存过期带来的问题
缺点:占用缓存空间较大,需要额外的定时任务维护机制

方案4:限流降级熔断

// 伪代码:使用Sentinel实现限流降级
@SentinelResource(value = "cacheService", blockHandler = "fallbackMethod")
public Object getFromCacheWithSentinel(String key) {// 正常缓存查询逻辑return doGetFromCache(key);
}// 降级方法
public Object fallbackMethod(String key, BlockException ex) {log.warn("Cache service is blocked, using fallback");// 返回默认值或错误提示return getDefaultValue();
}

原理:通过限流保护数据库,当缓存失效导致流量激增时,限制进入系统的请求数量
优点:保护系统在极端情况下不会崩溃,提高系统稳定性
缺点:会导致部分用户请求被拒绝,影响用户体验

http://www.dtcms.com/a/585039.html

相关文章:

  • 如何开公司做网站百度官方营销推广平台加载中
  • Linux:make自动化和实战演练
  • Qt实战:自定义搜索跳转控件 | 附完整源码
  • nanochat大语言模型讲解一
  • Vue3:watch与watchEffect的异同
  • 做网站代理wordpress文章半透明
  • (论文速读)LyT-Net:基于YUV变压器的轻量级微光图像增强网络
  • 操作系统?进程!!!
  • Diffusion 到 Flow Matching ( 从 DDPM 到 Stable Diffusion ) 丝滑入门
  • 无监督学习与互信息
  • 数据集预处理:规范化和标准化
  • vue学习之组件与标签
  • 软件测试之bug分析定位技巧
  • Rust 练习册 :Pig Latin与语言游戏
  • Tomcat的基本使用作用
  • 完整网站建设教程网站建设需要会什么软件
  • 【ASP.Net MVC 】使用Moq让单元测试变得更简单
  • Linux:线程的概念与控制
  • 零基础学AI大模型之嵌入模型性能优化
  • 【二叉搜索树】:程序的“决策树”,排序数据的基石
  • Canvas/SVG 冷门用法:实现动态背景与简易数据可视化
  • 昆明做网站做的好的公司智能建站系统 网站建设的首选
  • kali安装npm/sourcemap
  • 协作机器人的关节是通过什么工艺加工的
  • 轻松开启数字化时代,一键部署实现CRM落地
  • 长春市网站推广网站开发技术人员
  • JavaScript 指南
  • C++ LeetCode 力扣刷题 541. 反转字符串 II
  • C++死锁深度解析:从成因到预防与避免
  • 达梦DMDSC知识