1. 完整回答
缓存穿透、缓存击穿和缓存雪崩,都是使用 Redis 缓存时可能遇到的问题。它们的共同点是大量请求绕过缓存,直接访问数据库,导致数据库压力骤增,但触发原因和解决方案不同。
缓存穿透指查询的数据在缓存和数据库中都不存在。比如不断请求不存在的商品 ID,每次缓存未命中都会查询数据库。常见的解决方案有参数校验、缓存空值和布隆过滤器。空值缓存可以避免相同无效请求反复查询数据库;布隆过滤器则可以提前过滤大量不存在的数据。
缓存击穿指某个热点数据突然失效,大量并发请求同时访问这个数据,导致请求集中落到数据库。比如秒杀商品的缓存刚好过期,瞬间有大量请求查询商品信息。常见方案是使用互斥锁控制缓存重建,让同一时刻只有一个请求查询数据库,其余请求等待;也可以使用逻辑过期,先返回旧数据,再异步更新缓存,减少请求阻塞。
缓存雪崩指大量缓存同时失效,或者 Redis 服务故障,导致大批请求直接访问数据库。对于集中失效,可以给缓存过期时间增加随机值,分散过期时刻;对于 Redis 故障,可以通过主从复制、故障自动切换、多级缓存提高可用性,同时配合限流、熔断和降级保护数据库。
实际应用中,缓存穿透重点解决无效请求反复访问数据库,缓存击穿重点解决单个热点数据的并发重建,缓存雪崩重点解决大范围缓存失效和系统整体可用性。
2. 核心原理与关键概念
2.1 核心原理
正常缓存查询流程(Cache Aside,旁路缓存模式):
请求到达 → 查询 Redis → 命中直接返回 → 未命中查询数据库 → 写入 Redis 并设置过期时间 → 返回结果。
三类问题分别发生在这个流程的不同环节。
(1)缓存穿透:数据本身不存在
请求不存在的 ID → Redis 未命中 → 数据库查询也不存在 → 无法缓存正常数据 → 后续请求继续访问数据库。
核心问题:无效请求持续造成数据库查询。
解决流程:
参数校验 → 布隆过滤器判断数据是否可能存在 → 查询 Redis → 未命中再查询数据库 → 数据库无记录则缓存短期空值。
其中,参数校验和布隆过滤器可以提前拦截请求,空值缓存用于减少重复查询。
(2)缓存击穿:热点 Key 突然失效
热点 Key 过期 → 大量并发请求同时未命中 → 同时查询数据库 → 数据库压力骤增。
使用互斥锁后的流程:
缓存未命中 → 尝试获取锁 → 获取成功后再次检查缓存 → 仍未命中则查询数据库并重建缓存 → 释放锁。
其他请求等待缓存重建完成,再重新查询 Redis,避免重复访问数据库。
(3)缓存雪崩:大范围缓存失效
大量 Key 同时过期或 Redis 故障 → 缓存命中率骤降 → 大量请求进入数据库 → 数据库连接和处理能力耗尽 → 可能出现级联故障。
防护主要分两部分:
-
预防集中失效: 随机过期时间、热点数据预热、分批重建缓存。
-
控制故障影响: Redis 高可用、多级缓存、数据库限流、熔断和业务降级。
2.2 关键概念
| 概念 | 含义与作用 |
|---|---|
| 缓存命中率 | 从缓存成功获取数据的比例,突然下降可能意味着缓存大量失效 |
| TTL(生存时间) | 缓存数据的有效时间,过期后重新查询时会视为未命中 |
| 热点 Key | 被大量请求集中访问的缓存键,失效时容易引发缓存击穿 |
| 空值缓存 | 将数据库查询不到的结果用特殊标记缓存,避免相同无效请求反复访问数据库 |
| 布隆过滤器 | 使用位数组和多个哈希函数进行集合成员判断,可以快速排除不存在的数据,但可能误判存在 |
| 互斥锁 | 限制同一个 Key 的并发重建数量,降低数据库瞬时压力 |
| 逻辑过期 | 将业务过期时间保存在缓存数据中,过期后可以暂时返回旧数据,并异步更新 |
| 缓存预热 | 在流量高峰或缓存失效前,主动加载热点数据,避免大量请求同时回源 |
3. 高频面试追问
3.1 缓存穿透为什么会导致数据库压力增大?怎么解决?
核心原因是无效请求无法形成正常的缓存命中,导致数据库被反复查询。
比如查询一个不存在的商品 ID,Redis 没有数据,数据库也没有数据。如果直接返回空结果,下次查询相同 ID 时,仍然需要访问数据库。
常见解决方案有三种:
-
参数校验: 在入口过滤格式错误、明显越界的 ID。
-
缓存空值: 数据库查询不到时,缓存一个特殊空值,并设置较短的 TTL。
-
布隆过滤器: 提前判断 ID 是否可能存在,过滤大量无效请求。
如果是普通业务,通常先采用参数校验和空值缓存;如果数据量很大、存在大量随机无效 ID 请求,可以增加布隆过滤器。
3.2 布隆过滤器的底层原理是什么?为什么会有误判?
布隆过滤器通过位数组和多个哈希函数判断元素是否可能存在,用较小的内存实现快速过滤。
它的工作流程是:
-
添加元素时,通过多个哈希函数计算位置,把位数组对应的位置设为 1。
-
查询元素时,再计算相同的哈希位置。
-
如果其中任意一位为 0,说明元素一定没有被添加过;如果全部为 1,说明元素可能存在。
出现误判的原因是,不同元素可能映射到相同的位置。所以可能把不存在的元素判断为存在,但对于已经正确添加、且未被删除的元素,标准布隆过滤器不会误判为不存在。
实际使用时,还需要保证数据库与过滤器的数据同步。比如新增商品后没有及时写入布隆过滤器,就可能错误拦截真实存在的商品。
继续追问:布隆过滤器能完全解决缓存穿透吗?
不能。布隆过滤器存在误判,仍然可能放行无效请求。另外,数据新增时要及时更新过滤器,普通布隆过滤器也不支持安全地直接删除单个元素,所以通常需要结合空值缓存、参数校验和限流使用。
3.3 缓存空值有什么问题?新增数据后怎么办?
空值缓存实现简单,但可能带来短暂的数据不一致,还会占用缓存空间。
比如商品 ID 原本不存在,查询后缓存了一个空值。随后数据库新增了这个商品,如果空值没有失效,用户仍然会查询到不存在。
解决方式是:新增数据成功后主动删除对应的空值缓存,同时为负缓存设置较短的 TTL,作为兜底。
另外,如果有人不断请求随机 ID,就可能生成大量空值缓存。因此还需要限制空值缓存的数量和生存时间,并配合请求校验与限流。
3.4 缓存击穿为什么要使用分布式锁?具体怎么实现?
分布式锁的作用是控制同一个热点 Key 的缓存重建并发,避免多个服务实例同时查询数据库。
如果系统部署了多个实例,单机互斥锁只能控制当前实例内的线程,无法协调不同实例,因此可以使用 Redis 分布式锁。
具体流程是:
-
请求发现缓存未命中,尝试获取当前 Key 对应的分布式锁。
-
获取锁成功后,再次检查缓存,避免重复重建。
-
如果缓存仍不存在,就查询数据库,写入缓存。
-
写入完成后释放锁。其他请求等待并重新查询缓存。
Redis 可以使用 SET key value NX PX 原子地获取带过期时间的锁,并通过唯一标识和 Lua 脚本安全释放,避免误删其他请求持有的锁。
还需要控制锁等待时间和重试次数。如果锁提前过期,仍可能发生多个请求同时重建,因此锁的有效期、任务超时和异常处理都需要考虑。
继续追问:为什么获取锁后还要查询一次缓存?
因为等待锁的过程中,其他请求可能已经完成了缓存重建。如果不进行二次检查,当前请求仍会再次查询数据库,产生重复操作。
3.5 互斥锁和逻辑过期有什么区别?怎么选择?
互斥锁优先保证获取新数据,逻辑过期优先保证请求响应速度。
互斥锁方案中,热点缓存过期后,由一个请求负责重建,其他请求等待。因此可以控制数据库并发,但等待期间响应时间可能增加。
逻辑过期方案会在缓存数据中保存一个业务过期时间。请求发现逻辑过期时,尝试触发后台重建,同时可以继续返回旧数据。
逻辑过期通常需要让 Redis 中的旧数据继续保留,并由后台任务完成更新。这样即使数据库查询较慢,也不会让所有请求阻塞。
选择时主要考虑业务对数据新鲜度的要求:
-
数据新鲜度要求较高: 优先考虑互斥锁或其他同步重建机制。
-
读取性能要求高、允许短暂旧数据: 可以使用逻辑过期,例如商品描述、店铺介绍等。
逻辑过期需要处理异步更新失败、重复重建和旧数据长期未刷新等问题。库存、余额等敏感数据不能直接依赖可能过期的缓存结果进行关键业务判断。
3.6 给缓存 TTL 增加随机值,能彻底解决缓存雪崩吗?
不能。随机 TTL 主要解决大量 Key 集中到期的问题,无法处理 Redis 整体故障或单个热点 Key 击穿。
比如批量缓存一万条商品数据,如果全部设置为 30 分钟,就可能在相近时间失效。
可以调整为:
TTL = 30 分钟 + 0~10 分钟随机值
这样缓存会在不同时间失效,数据库回源压力更加分散。
但如果 Redis 整体不可用,随机 TTL 就没有作用了,需要依靠高可用架构和限流降级。
另外,对于同一个热点 Key,随机 TTL 也不能避免它在失效瞬间被大量请求访问,这种情况仍然需要互斥锁、提前刷新或逻辑过期。
3.7 如果 Redis 突然宕机,怎么避免数据库被打垮?
需要同时考虑缓存服务恢复和数据库保护,不能让所有请求无限制地回源。
首先,Redis 层面可以采用主从复制配合 Sentinel,或者使用 Redis Cluster,提高故障自动切换能力。不过故障切换可能存在短暂不可用,无法保证零中断。
应用层需要设置合理的 Redis 访问超时,避免请求长时间阻塞。
如果 Redis 持续不可用,可以采取以下措施:
-
使用本地缓存或多级缓存,承接部分读请求。
-
对数据库回源进行限流,控制最大并发数。
-
对非核心业务进行熔断或降级,例如返回默认内容、暂时关闭推荐功能。
-
恢复后分批预热热点数据,避免大量请求同时重建缓存。
核心原则是优先保护数据库和核心业务,避免缓存故障进一步演变成整个系统的故障。
3.8 缓存重建期间数据库数据发生变化,怎么保证一致性?
常见方式是采用 Cache Aside 模式,更新数据库成功后删除缓存,并通过重试和过期机制降低不一致风险。
正常更新流程:
更新数据库 → 删除对应缓存 → 后续读请求重新查询数据库并写入缓存。
不过这种方式仍然存在并发问题。
例如,一个请求刚从数据库读到旧数据,还没写入缓存;另一个请求更新了数据库并删除缓存;随后第一个请求把旧数据写入缓存,就会产生短暂不一致。
对于一般业务,可以通过合理 TTL、缓存删除失败重试、消息队列补偿等方式降低影响。
对于一致性要求较高的业务,还可以通过版本号校验或其他并发控制机制防止旧数据覆盖新数据,关键操作以数据库或具备正确一致性保证的数据源为准。
3.9 怎么判断线上发生的是缓存穿透、击穿还是雪崩?
可以结合缓存命中率、异常请求比例、热点 Key 访问量以及数据库负载判断。
| 现象 | 优先排查方向 |
|---|---|
| 大量请求查询不存在的 ID,数据库无结果比例上升 | 缓存穿透 |
| 某个热点 Key 未命中量突然上升,数据库出现大量相同查询 | 缓存击穿 |
| 大量不同 Key 同时未命中,数据库整体查询量骤增 | 缓存雪崩 |
| Redis 大量超时或连接失败,数据库流量突然增加 | 缓存服务故障引发的雪崩风险 |
排查时还会检查 Key 的 TTL 分布、Redis 内存淘汰情况、数据库连接池和慢查询。
需要注意,缓存命中率下降只是一个信号,还要结合请求分布和数据库实际查询结果,才能区分具体原因。
3.10 在秒杀项目中,怎么同时处理这三类缓存问题?
可以根据请求类型和数据特征分层处理,并结合预热、并发控制与限流保护后端。
例如秒杀商品详情查询:
首先,在接口入口校验商品 ID。对于明显非法的 ID 直接拦截,必要时使用布隆过滤器过滤不存在的商品,避免缓存穿透。
其次,活动开始前预热热门商品信息。对于高并发读取的商品详情,可以采用逻辑过期和异步更新;对于需要同步获取新数据的场景,可以使用互斥锁控制重建,避免热点 Key 击穿。
然后,普通缓存设置分散的过期时间,降低批量失效导致雪崩的风险,同时配置 Redis 高可用、请求限流和业务降级。
最后,库存扣减和订单创建需要单独保证并发正确性,不能仅依赖缓存详情数据。
这样可以分别控制无效请求、热点重建和大范围故障带来的影响。
4. 易混淆概念对比
4.1 缓存穿透、缓存击穿、缓存雪崩
| 对比维度 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
|---|---|---|---|
| 核心原因 | 请求的数据不存在 | 单个热点数据失效 | 大量缓存失效或缓存服务故障 |
| 数据库是否存在数据 | 不存在 | 通常存在 | 通常存在 |
| 影响范围 | 一个或多个无效 Key | 某个热点 Key | 大量不同 Key |
| 主要风险 | 无效查询反复访问数据库 | 热点请求集中回源 | 数据库整体承受突增流量 |
| 主要方案 | 空值缓存、布隆过滤器 | 互斥锁、逻辑过期 | 随机 TTL、高可用、限流降级 |
三者最关键的区别是:穿透关注数据不存在,击穿关注热点 Key 失效,雪崩关注大范围缓存失效。
4.2 空值缓存与布隆过滤器
| 对比维度 | 空值缓存 | 布隆过滤器 |
|---|---|---|
| 工作机制 | 缓存数据库不存在的查询结果 | 根据哈希位判断数据是否可能存在 |
| 优势 | 实现简单,可以准确拦截已缓存的无效查询 | 内存利用率高,适合大量 ID 过滤 |
| 局限 | 随机无效 ID 可能占用大量缓存 | 存在误判,需要维护数据同步 |
| 适用场景 | 无效请求相对有限的普通业务 | 海量数据、大量随机无效请求 |
| 数据新增 | 需要删除对应空值缓存 | 需要及时添加到过滤器 |
两者可以组合使用,布隆过滤器负责提前过滤,空值缓存负责拦截已经确认不存在的数据。
4.3 互斥锁与逻辑过期
| 对比维度 | 互斥锁 | 逻辑过期 |
|---|---|---|
| 重建方式 | 一个请求同步重建 | 后台异步重建 |
| 其他请求 | 等待、重试或按策略降级 | 可以直接返回旧数据 |
| 响应速度 | 重建期间可能变慢 | 通常更稳定 |
| 数据新鲜度 | 重建成功后获得新数据 | 允许短暂读取旧数据 |
| 主要问题 | 锁竞争、等待、超时 | 旧数据、异步失败、更新管理 |
| 适用场景 | 需要较新数据的热点查询 | 允许短暂不一致的高并发读取 |
4.4 随机 TTL 与缓存预热
| 对比维度 | 随机 TTL | 缓存预热 |
|---|---|---|
| 解决的问题 | 大量缓存同时到期 | 冷启动或热点缓存缺失 |
| 实现方式 | 分散缓存失效时间 | 提前将热点数据加载到缓存 |
| 主要优势 | 实现简单,减少集中回源 | 提高活动开始时的缓存命中率 |
| 局限性 | 无法处理缓存整体故障 | 需要识别热点并维护预热任务 |
两种方案可以配合使用,预热时也需要合理设置 TTL,避免预热的数据再次集中失效。
4.5 Redis 高可用与限流降级
| 对比维度 | Redis 高可用 | 限流降级 |
|---|---|---|
| 核心目的 | 提高缓存服务可用性 | 保护数据库和核心业务 |
| 主要手段 | 主从复制、Sentinel、Cluster | 请求限流、熔断、返回兜底结果 |
| 处理重点 | 缓存节点故障及恢复 | 缓存不可用期间的流量控制 |
| 局限性 | 故障切换期间仍可能不可用 | 部分请求可能被拒绝或获得降级结果 |
两者解决的问题不同,需要共同使用,才能降低缓存故障造成的连锁影响。
5. 总结
缓存穿透、缓存击穿和缓存雪崩,本质上都是缓存未能有效拦截请求,导致数据库承受额外压力。穿透是请求的数据在缓存和数据库中都不存在,通常使用空值缓存和布隆过滤器解决;击穿是某个热点 Key 失效,引发大量并发请求回源,可以通过互斥锁或逻辑过期控制缓存重建;雪崩是大量缓存同时失效或 Redis 故障,需要使用随机 TTL、缓存预热、高可用和限流降级。三者的核心区别在于数据是否存在、失效范围以及并发请求的集中程度。实际系统通常组合多种方案,既要降低数据库压力,也要兼顾响应时间、数据一致性和系统可用性。