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

缓存穿透、缓存击穿和缓存雪崩详解

缓存在高并发系统中是保护数据库的重要防线,但实践中常会遇到三个棘手的现象:缓存穿透、缓存击穿和缓存雪崩。理解它们的区别和应对方案,是后端工程师的必修课。下面我就以一个电商商品查询场景为例,结合代码逐一拆解。


1. 缓存穿透

是什么

查询一个数据库和缓存中都不存在的数据。由于缓存没有命中,每次请求都会穿透到数据库,恶意攻击者可通过构造大量不存在的 ID 瞬间压垮数据库。

举例

商品 ID 自增,攻击者并发请求productId = -1,此 ID 不可能存在。每次请求都绕过缓存直接访问数据库,造成数据库查询压力激增。

解决方案

方案一:缓存空对象
当数据库查询结果为 null 时,也将一个空值标识缓存起来,并设置较短的过期时间(如30秒)。这样后续攻击请求会直接命中缓存,不会到达数据库。

public Product getProductById(Long productId) { String cacheKey = "product:" + productId; // 1.查缓存 Product product = (Product) redisTemplate.opsForValue().get(cacheKey); if (product != null) { return product; // 命中,直接返回 } // 2.查数据库(假设这里调用 productMapper.selectById) product = productMapper.selectById(productId); if (product != null) { // 数据库有,写入缓存,过期1小时 redisTemplate.opsForValue().set(cacheKey, product, 1, TimeUnit.HOURS); } else { // 数据库没有,缓存空对象,过期30秒 redisTemplate.opsForValue().set(cacheKey, new Product(), 30, TimeUnit.SECONDS); } return product; }

注意点:空对象占用内存空间有限,但要控制过期时间,避免长期存在。若空值也有业务含义,可缓存一个特殊标记对象(如NullProduct)。

方案二:布隆过滤器
维护一个存储所有合法 key 的布隆过滤器,请求先经过布隆过滤器判定,如果认为不存在则直接返回,不再查询缓存和数据库。这种方式内存占用小,但存在误判率(判定不存在时一定不存在,判定存在时可能不存在)。适合数据量极大的场景。

// 初始化布隆过滤器(使用 Guava 库) BloomFilter<Long> bloomFilter = BloomFilter.create( Funnels.longFunnel(), 1000000, // 预计数据量 0.01); // 误判率 // 查询时先判断 if (!bloomFilter.mightContain(productId)) { return null; // 直接返回,避免穿透 } // 后续走缓存-数据库流程...

生产上常用 Redisson 的布隆过滤器或 Redis 插件来实现分布式布隆过滤器。


2. 缓存击穿

是什么

某个热点数据(如秒杀商品)缓存恰好过期,此时大量并发请求同时查询该 key,因缓存失效,所有请求瞬间打到数据库,导致数据库瞬时压力过大。

举例

爆款手机缓存 key 在 12:00:00 过期,恰逢促销活动,每秒上万请求涌入,同一时刻发现缓存为空,全部穿透到数据库,数据库 CPU 直接 100%。

解决方案

核心思路:保证同一个 key 同一时刻只有一个线程去加载数据,其他线程等待或快速失败。

方案一:互斥锁(分布式锁)
使用 Redis 的SETNX或 Redisson 的分布式锁实现互斥。

public Product getProductWithLock(Long productId) { String cacheKey = "product:" + productId; Product product = (Product) redisTemplate.opsForValue().get(cacheKey); if (product != null) { return product; } // 获取分布式锁,锁的key = "lock:product:" + productId RLock lock = redissonClient.getLock("lock:product:" + productId); try { // 尝试加锁,等待5秒,锁有效期30秒 if (lock.tryLock(5, 30, TimeUnit.SECONDS)) { // 双重检查,避免锁内再次查缓存 product = (Product) redisTemplate.opsForValue().get(cacheKey); if (product != null) { return product; } // 查数据库 product = productMapper.selectById(productId); if (product != null) { redisTemplate.opsForValue().set(cacheKey, product, 1, TimeUnit.HOURS); } else { // 缓存空对象,防止穿透 redisTemplate.opsForValue().set(cacheKey, new Product(), 30, TimeUnit.SECONDS); } } else { // 获取锁失败,可快速失败或自旋等待后重试 Thread.sleep(50); return getProductWithLock(productId); // 递归重试 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); } return product; }

方案二:逻辑过期 + 异步刷新
不设置物理过期时间(TTL),在 value 中存储一个逻辑过期时间字段。读取时若发现逻辑时间已过期,返回旧值的同时异步更新缓存。这样永远不出现缓存失效瞬间的并发穿透。

// 存储实体,内部包含数据和过期时间戳 public class ProductWrapper { private Product product; private long expireTime; // 逻辑过期时间戳 } public Product getProductLogicalExpire(Long productId) { String cacheKey = "product:" + productId; ProductWrapper wrapper = (ProductWrapper) redisTemplate.opsForValue().get(cacheKey); if (wrapper == null) { // 缓存不存在,直接查库并写入(可能是冷数据,并发不高) Product product = productMapper.selectById(productId); wrapper = new ProductWrapper(product, System.currentTimeMillis() + 3600_000); redisTemplate.opsForValue().set(cacheKey, wrapper); return product; } if (wrapper.getExpireTime() > System.currentTimeMillis()) { return wrapper.getProduct(); // 未过期,直接返回 } // 逻辑过期,尝试获取锁进行异步刷新 String lockKey = "lock:product:" + productId; boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS); if (locked) { // 异步更新缓存 executorService.submit(() -> { Product product = productMapper.selectById(productId); ProductWrapper newWrapper = new ProductWrapper(product, System.currentTimeMillis() + 3600_000); redisTemplate.opsForValue().set(cacheKey, newWrapper); redisTemplate.delete(lockKey); }); } // 返回旧数据(即使过期也先用着) return wrapper.getProduct(); }

3. 缓存雪崩

是什么

同一时刻大量缓存 key 同时过期,或缓存服务器宕机,导致海量请求直接访问数据库,数据库不堪重负,甚至引发连锁崩溃。

举例

某整点活动,所有商品缓存同时设置了1小时过期。恰好在活动高峰时大量 key 集中失效,数据库瞬间瘫痪。

解决方案

方案一:过期时间加随机值
在原有过期时间上追加一个随机秒数,避免集中过期。

// 设置缓存时,基础过期时间+随机偏移 int baseExpireSeconds = 3600; // 1小时 int randomSeconds = new Random().nextInt(600); // 0~600秒随机 redisTemplate.opsForValue().set(cacheKey, product, baseExpireSeconds + randomSeconds, TimeUnit.SECONDS);

方案二:高可用架构
对 Redis 使用主从+哨兵集群模式,避免单点宕机。缓存层做好故障转移和自动恢复。

方案三:多级缓存 + 限流降级
在应用本地增加一层 Caffeine 缓存,或使用 Nginx 本地缓存。当 Redis 不可用时,请求优先走本地缓存。同时引入 Hystrix / Sentinel 限流,防止数据库被打死。

// 使用 Caffeine 作为一级缓存,Redis 为二级 Cache<Long, Product> localCache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.MINUTES) .maximumSize(10000) .build(); public Product getProductMultiLevel(Long productId) { // 1. 查本地缓存 Product product = localCache.getIfPresent(productId); if (product != null) return product; // 2. 查 Redis product = (Product) redisTemplate.opsForValue().get("product:" + productId); if (product != null) { localCache.put(productId, product); return product; } // 3. 查数据库(加锁防击穿,此处省略) // ... return product; }

三者对比总结

现象原因核心解法
穿透查询不存在的数据缓存空对象 / 布隆过滤器
击穿热点 key 过期,并发查库互斥锁 / 逻辑过期异步刷新
雪崩大量 key 同时过期或服务宕机过期时间随机化 / 高可用 / 多级缓存

实际开发中,三种策略往往组合使用。例如:商品查询接口同时采用布隆过滤器防穿透+分布式锁防击穿+过期时间随机化防雪崩。理解它们的本质,才能在高并发场景下设计出稳定的缓存体系。

http://www.jsqmd.com/news/1344848/

相关文章:

  • 【Web安全】API安全测试实战指南(OWASP API Top 10全覆盖,保姆级教程)
  • 北京工商代办服务怎么选?2026年注册公司、税务解异常及代理记账机构实务评测 - 优质品牌商家
  • Android分区存储下外置存储设备(U盘/SD卡)的发现、监听与安全访问实战
  • Hugging Face Diffusers惊现高危漏洞:加载一个模型就能让攻击者接管你的服务器
  • 高效开发必备:Linux与开发工具核心指令大全
  • 长沙代理记账推荐|正规财税服务机构甄选参考 - 行业深度分析
  • 三明刑事上诉律师事务所怎么选更稳妥结合福建熹龙律师事务所 - 热点品牌推荐
  • DeepSeek V4 Flash接入Codex教程
  • 基于大模型的代码库理解与AI编程助手构建实践
  • YOLO[多种场景下的铅笔]目标检测数据集
  • URP自定义光照Shader实战:完整光照与阴影实现
  • 具身智能协同机制研究:TVA与VLA的统一建模(3)
  • 2026年六西格玛DOE实验设计怎么入门——众智商学院张明老师制造业工艺优化多因子分析实操 - 众智商学院cppm官方
  • CSS box-shadow 盒阴影完全指南:从参数顺序到多层立体投影
  • 基于Coze平台构建短视频自动化生成工作流:从文案到发布的完整实践
  • 青岛厂房空气能热泵销售安装哪家好认准青岛岳峰制冷工程 - 热点品牌推荐
  • C语言编程思维:从语法到实战,构建健壮程序的系统方法
  • Unity PlayerPrefs编辑器:原理、应用与高效调试指南
  • 从AI玩具到工程化:用“配方”思维构建可复现的生成式AI工作流
  • 动图怎么变成视频 2026亲测可用的免费方法 - 图片处理研究员
  • Unity Inspector变量显示控制:public、private与特性Attribute实战指南
  • 2026亲测有效教程:不下载软件怎么把视频转成GIF - 图片处理研究员
  • 腾讯云跨账号服务器迁移实战:镜像共享与零停机部署指南
  • 哈尔滨网站建设制作哪家好:揭秘本地优质服务商的选择逻辑与避坑指南
  • Python读取Chrome历史记录SQLite数据库并导出Excel完整教程
  • Spring Cloud Gateway构建系统边界防护:拦截遗留系统数据污染与能量渗透
  • 数据格式转换工具部署与API集成实践指南
  • 2026年厂房通风降温怎么选?省电空调与负压风机厂家综合评估 - 优质品牌商家
  • 2026免费无水印的视频转GIF工具推荐 亲测好用的方法 - 图片处理研究员
  • 昇腾大模型推理优化:Kthena与Mooncake实现KVCache复用与分布式加速