缓存三剑客问题

我们来详细聊聊经典的"缓存三剑客"问题。这是指在使用缓存(尤其是 Redis 或 Memcached)时,最常遇到且对系统危害最大的三种典型问题:​缓存穿透、缓存击穿、缓存雪崩

理解并解决这三个问题,是构建高可用、高性能系统的关键。


核心概念:为什么要用缓存?

首先,快速回顾缓存的作用:为了缓解数据库(如 MySQL)的读写压力,将频繁访问的"热数据"存放在读写速度极快的内存中(缓存)。数据的查询顺序变为:​先查缓存,缓存未命中再查数据库,并将结果写入缓存

"缓存三剑客"问题就出现在这个流程的异常情况中。


第一剑:缓存穿透

1. 问题描述

缓存穿透 是指查询一个数据库中根本不存在的数据。由于数据不存在,缓存中自然也不会有(缓存未命中),导致这个请求会直接穿透缓存,每次都要去数据库查询。

  • 关键特征​:数据在数据库和缓存中都不存在。

  • 危害​:如果有人恶意发起大量这类请求(比如用不存在的用户ID查询用户信息),会瞬间给数据库带来巨大压力,甚至导致数据库宕机。

2. 解决方案
  1. 缓存空对象

    • 做法 ​:即使从数据库没查到,也向缓存中写入一个空值(如 null),并设置一个较短的过期时间(例如 3-5 分钟)。

    • 优点​:实现简单,能有效应对短期的大量攻击。

    • 缺点​:可能会在缓存中存储大量无意义的空键,占用内存;可能存在短期数据不一致(比如数据后来被录入了,但缓存里还是空值)。

  2. 布隆过滤器

    • 做法 ​:在缓存之前,设置一个布隆过滤器。布隆过滤器是一个高效的数据结构,用于快速判断"某个元素一定不存在 "或"可能存在"于某个集合中。

    • 流程​:

      1. 将所有可能查询的数据的 key 哈希后映射到布隆过滤器的位数组中。

      2. 请求来时,先通过布隆过滤器判断 key 是否存在。

        • 如果不存在,则直接返回空,拒绝访问数据库。

        • 如果存在,才继续后续的缓存查询流程。

    • 优点​:内存占用极小,能从根本上彻底解决穿透问题。

    • 缺点​:实现稍复杂;有误判率("可能存在"意味着它可能会把一些合法的、但不在过滤器里的新 key 误判为不存在,但不会误判存在的数据为不存在);数据变更时维护布隆过滤器较麻烦。


第二剑:缓存击穿

1. 问题描述

缓存击穿 是指一个访问非常频繁的"热点数据"​ ​(比如某明星的微博)在缓存过期(失效)的瞬间。由于这个 key 可能被大量并发请求访问,在它失效的瞬间,所有对这些数据的请求都会穿透缓存,直接打到数据库上,仿佛缓存被"击穿"了一个洞。

  • 关键特征​:数据存在,但缓存刚好过期。key 是"热点"。

  • 危害​:在热点 key 失效的瞬间,巨大的并发可能压垮数据库。

2. 解决方案
  1. 设置热点数据永不过期

    • 做法​:对于极热点的 key,可以不对其设置过期时间。然后通过后台任务或程序逻辑,在数据更新时主动刷新缓存。

    • 优点​:简单,一劳永逸。

    • 缺点​:需要人工识别热点数据;数据一致性需要靠逻辑维护。

  2. 互斥锁

    • 做法 ​:当缓存失效时,不立即去查询数据库。而是先尝试获取一个分布式锁(如用 Redis 的 SETNX命令)。只有一个线程能成功获取锁,这个线程负责去查询数据库并重建缓存。其他未获取到锁的线程则等待一段时间后重试查询缓存。

    • 优点​:能很好地保护数据库,逻辑严谨。

    • 缺点​:实现复杂;如果获取锁的线程挂掉,可能需要处理锁超时;性能上有一定损耗(等待)。

  3. 逻辑过期

    • 做法 ​:不给缓存数据设置物理过期时间,而是在缓存 value 中额外存储一个逻辑过期时间。当业务线程发现数据逻辑上已过期时,它不会立即重建缓存,而是尝试获取互斥锁。拿到锁的线程会启动一个新线程去异步重建缓存,而自己则返回旧的、已过期的数据。其他线程在锁被占用期间,也直接返回旧数据。

    • 优点​:性能极佳,用户无感知,永远有数据返回。

    • 缺点​:实现最复杂;会有一段时间的数据延迟(返回旧数据)。


第三剑:缓存雪崩

1. 问题描述

缓存雪崩 是指缓存中大量的 key 在同一时间点或时间段内集中失效 ,或者缓存服务直接宕机。导致所有原本应该访问缓存的请求,瞬间全部涌向数据库,数据库无法承受巨大的压力而崩溃,进而导致整个系统崩溃,就像雪崩一样。

  • 关键特征​:大量 key 同时失效 或 缓存服务不可用。

  • 与击穿的区别​:击穿是单个热点 key 失效,雪崩是大量 key 同时失效。

2. 解决方案
  1. 设置随机的过期时间

    • 做法​:在为缓存数据设置过期时间时,在基础过期时间上加上一个随机值(如 1-5 分钟的随机数)。这样可以让 key 的过期时间尽量分散,避免同时失效。

    • 优点​:简单有效,是预防雪崩的首选方案。

  2. 构建高可用的缓存集群

    • 做法​:通过 Redis 的哨兵模式或集群模式,实现缓存服务的高可用。即使个别节点宕机,整个集群仍然可以提供服务。

    • 目的​:防止因缓存服务宕机而引发的雪崩。

  3. 服务熔断与降级

    • 做法​:当应用系统检测到数据库压力过大或响应过慢时,启动熔断机制,暂时停止访问数据库,直接返回预设的默认值(如"系统繁忙,请稍后再试")或兜底数据。给数据库"止血",等缓存服务恢复后,再关闭熔断。

    • 目的​:牺牲部分用户体验和非核心功能,保证核心业务和系统整体不崩溃。

  4. 持久化存储预热

    • 做法​:在缓存服务重启或大规模失效后,系统正式对外提供服务前,先通过一个脚本或程序,将高频访问的数据提前加载到缓存中。

总结与对比

问题类型 核心原因 关键特征 主要解决方案
缓存穿透 查询不存在的数据 数据库和缓存中都没有 1. 缓存空对象 2. 布隆过滤器
缓存击穿 热点 key​ 突然过期 单个热点 key 失效,并发高 1. 永不过期 2. 互斥锁 3. 逻辑过期
缓存雪崩 大量 key​ 同时失效或缓存宕机 大规模缓存失效,系统级故障 1. 设置随机过期时间 2. 缓存高可用集群 3. 服务熔断与降级

应对法则​:

  1. 防穿透​:守住第一道门,判断请求是否合法。

  2. 防击穿​:保护热点,避免单点并发。

  3. 防雪崩​:分散风险,保证服务可用。

在实际项目中,通常需要组合运用这些策略,才能构建一个健壮的缓存系统。

相关推荐
曹天骄3 小时前
[特殊字符] 多环境 DCDN 缓存与 version 切换刷新方案
缓存
七夜zippoe3 小时前
压缩与缓存调优实战指南:从0到1根治性能瓶颈(二)
缓存·优化·压缩·底层原理
快乐非自愿4 小时前
Vue 缓存之坑,变量赋值方式和响应式数据
前端·vue.js·缓存
七夜zippoe5 小时前
压缩与缓存调优实战指南:从0到1根治性能瓶颈(一)
缓存·压缩·调优·痛点
乌萨奇也要立志学C++7 小时前
【Linux】Ext系列文件系统 从磁盘结构到文件存储的原理剖析
android·linux·缓存·1024程序员节
weixin_419658318 小时前
Spring的三级缓存和SpringMVC的流程
java·spring·缓存
aristo_boyunv8 小时前
Redis发布订阅【充当消息中间件】
数据库·redis·缓存
艾德金的溪17 小时前
redis-7.4.6部署安装
前端·数据库·redis·缓存
我的offer在哪里18 小时前
Redis
数据库·redis·缓存