缓存三剑客问题
我们来详细聊聊经典的“缓存三剑客”问题。这是指在使用缓存(尤其是 Redis 或 Memcached)时,最常遇到且对系统危害最大的三种典型问题:缓存穿透、缓存击穿、缓存雪崩。
理解并解决这三个问题,是构建高可用、高性能系统的关键。
核心概念:为什么要用缓存?
首先,快速回顾缓存的作用:为了缓解数据库(如 MySQL)的读写压力,将频繁访问的“热数据”存放在读写速度极快的内存中(缓存)。数据的查询顺序变为:先查缓存,缓存未命中再查数据库,并将结果写入缓存。
“缓存三剑客”问题就出现在这个流程的异常情况中。
第一剑:缓存穿透
1. 问题描述
缓存穿透是指查询一个数据库中根本不存在的数据。由于数据不存在,缓存中自然也不会有(缓存未命中),导致这个请求会直接穿透缓存,每次都要去数据库查询。
-
关键特征:数据在数据库和缓存中都不存在。
-
危害:如果有人恶意发起大量这类请求(比如用不存在的用户ID查询用户信息),会瞬间给数据库带来巨大压力,甚至导致数据库宕机。
2. 解决方案
-
缓存空对象
-
做法:即使从数据库没查到,也向缓存中写入一个空值(如
null
),并设置一个较短的过期时间(例如 3-5 分钟)。 -
优点:实现简单,能有效应对短期的大量攻击。
-
缺点:可能会在缓存中存储大量无意义的空键,占用内存;可能存在短期数据不一致(比如数据后来被录入了,但缓存里还是空值)。
-
-
布隆过滤器
-
做法:在缓存之前,设置一个布隆过滤器。布隆过滤器是一个高效的数据结构,用于快速判断“某个元素一定不存在”或“可能存在”于某个集合中。
-
流程:
-
将所有可能查询的数据的 key 哈希后映射到布隆过滤器的位数组中。
-
请求来时,先通过布隆过滤器判断 key 是否存在。
-
如果不存在,则直接返回空,拒绝访问数据库。
-
如果存在,才继续后续的缓存查询流程。
-
-
-
优点:内存占用极小,能从根本上彻底解决穿透问题。
-
缺点:实现稍复杂;有误判率(“可能存在”意味着它可能会把一些合法的、但不在过滤器里的新 key 误判为不存在,但不会误判存在的数据为不存在);数据变更时维护布隆过滤器较麻烦。
-
第二剑:缓存击穿
1. 问题描述
缓存击穿是指一个访问非常频繁的“热点数据”(比如某明星的微博)在缓存过期(失效)的瞬间。由于这个 key 可能被大量并发请求访问,在它失效的瞬间,所有对这些数据的请求都会穿透缓存,直接打到数据库上,仿佛缓存被“击穿”了一个洞。
-
关键特征:数据存在,但缓存刚好过期。key 是“热点”。
-
危害:在热点 key 失效的瞬间,巨大的并发可能压垮数据库。
2. 解决方案
-
设置热点数据永不过期
-
做法:对于极热点的 key,可以不对其设置过期时间。然后通过后台任务或程序逻辑,在数据更新时主动刷新缓存。
-
优点:简单,一劳永逸。
-
缺点:需要人工识别热点数据;数据一致性需要靠逻辑维护。
-
-
互斥锁
-
做法:当缓存失效时,不立即去查询数据库。而是先尝试获取一个分布式锁(如用 Redis 的
SETNX
命令)。只有一个线程能成功获取锁,这个线程负责去查询数据库并重建缓存。其他未获取到锁的线程则等待一段时间后重试查询缓存。 -
优点:能很好地保护数据库,逻辑严谨。
-
缺点:实现复杂;如果获取锁的线程挂掉,可能需要处理锁超时;性能上有一定损耗(等待)。
-
-
逻辑过期
-
做法:不给缓存数据设置物理过期时间,而是在缓存 value 中额外存储一个逻辑过期时间。当业务线程发现数据逻辑上已过期时,它不会立即重建缓存,而是尝试获取互斥锁。拿到锁的线程会启动一个新线程去异步重建缓存,而自己则返回旧的、已过期的数据。其他线程在锁被占用期间,也直接返回旧数据。
-
优点:性能极佳,用户无感知,永远有数据返回。
-
缺点:实现最复杂;会有一段时间的数据延迟(返回旧数据)。
-
第三剑:缓存雪崩
1. 问题描述
缓存雪崩是指缓存中大量的 key 在同一时间点或时间段内集中失效,或者缓存服务直接宕机。导致所有原本应该访问缓存的请求,瞬间全部涌向数据库,数据库无法承受巨大的压力而崩溃,进而导致整个系统崩溃,就像雪崩一样。
-
关键特征:大量 key 同时失效 或 缓存服务不可用。
-
与击穿的区别:击穿是单个热点 key 失效,雪崩是大量 key 同时失效。
2. 解决方案
-
设置随机的过期时间
-
做法:在为缓存数据设置过期时间时,在基础过期时间上加上一个随机值(如 1-5 分钟的随机数)。这样可以让 key 的过期时间尽量分散,避免同时失效。
-
优点:简单有效,是预防雪崩的首选方案。
-
-
构建高可用的缓存集群
-
做法:通过 Redis 的哨兵模式或集群模式,实现缓存服务的高可用。即使个别节点宕机,整个集群仍然可以提供服务。
-
目的:防止因缓存服务宕机而引发的雪崩。
-
-
服务熔断与降级
-
做法:当应用系统检测到数据库压力过大或响应过慢时,启动熔断机制,暂时停止访问数据库,直接返回预设的默认值(如“系统繁忙,请稍后再试”)或兜底数据。给数据库“止血”,等缓存服务恢复后,再关闭熔断。
-
目的:牺牲部分用户体验和非核心功能,保证核心业务和系统整体不崩溃。
-
-
持久化存储预热
-
做法:在缓存服务重启或大规模失效后,系统正式对外提供服务前,先通过一个脚本或程序,将高频访问的数据提前加载到缓存中。
-
总结与对比
问题类型 | 核心原因 | 关键特征 | 主要解决方案 |
---|---|---|---|
缓存穿透 | 查询不存在的数据 | 数据库和缓存中都没有 | 1. 缓存空对象 |
缓存击穿 | 热点 key 突然过期 | 单个热点 key 失效,并发高 | 1. 永不过期 |
缓存雪崩 | 大量 key 同时失效或缓存宕机 | 大规模缓存失效,系统级故障 | 1. 设置随机过期时间 |
应对法则:
-
防穿透:守住第一道门,判断请求是否合法。
-
防击穿:保护热点,避免单点并发。
-
防雪崩:分散风险,保证服务可用。
在实际项目中,通常需要组合运用这些策略,才能构建一个健壮的缓存系统。