Redis 虽然拥有极高的读写性能,但缓存并不是万能的。在高并发场景下,如果缓存设计不合理,就可能导致大量请求直接访问数据库,甚至引发数据库过载。
本文从 产生原因、解决方案、代码示例、注意事项 四个方面,梳理 Redis 最经典的三大缓存问题。
文章目录
- 一、缓存穿透:查询不存在的数据
- [1. 什么是缓存穿透?](#1. 什么是缓存穿透?)
- [2. 解决方案](#2. 解决方案)
- [3. 适用场景](#3. 适用场景)
- [二、缓存击穿:热点 Key 突然失效](#二、缓存击穿:热点 Key 突然失效)
- [1. 什么是缓存击穿?](#1. 什么是缓存击穿?)
- [2. 解决方案](#2. 解决方案)
- [3. 适用场景](#3. 适用场景)
- 三、缓存雪崩:大量缓存同时失效
- [1. 什么是缓存雪崩?](#1. 什么是缓存雪崩?)
- [2. 解决方案](#2. 解决方案)
- [3. 适用场景](#3. 适用场景)
- 四、三大缓存问题对比与实战注意事项
- [1. 三大问题对比](#1. 三大问题对比)
- [2. 实际开发中的注意事项](#2. 实际开发中的注意事项)
- 总结
一、缓存穿透:查询不存在的数据
1. 什么是缓存穿透?
缓存穿透是指用户查询的数据 在 Redis 和数据库中都不存在。由于缓存无法命中,请求每次都会访问数据库,导致大量无效查询消耗数据库资源。
例如,用户不断请求 user:999999,但这个用户实际上并不存在。

⚠️ 核心问题: 缓存和数据库都没有数据,请求无法被缓存拦截。
2. 解决方案
方案一:缓存空值
当数据库查询不到数据时,将一个特殊的空值标记写入 Redis,并设置较短的过期时间。
go
// 将查询不存在的结果缓存 60 秒
err := rdb.Set(ctx, "user:999999", "__NULL__", 60*time.Second).Err()
后续请求命中这个标记后,就可以直接返回数据不存在的结果,无须重复查询数据库。
优点: 实现简单,能够有效减少重复的无效查询。
缺点: 会占用一定的缓存空间;如果大量随机 Key 都被缓存,也可能浪费内存。
💡 注意: 必须区分"缓存未命中"和"缓存中存储了空值标记"。如果数据库新增了原本不存在的数据,还要考虑空值缓存导致的短暂数据不一致问题。
方案二:布隆过滤器(Bloom Filter)
布隆过滤器是一种节省空间的数据结构,用于快速判断某个数据是否可能存在。
- ✅ 判断为不存在:直接拦截请求,不访问数据库。
- ⚠️ 判断为可能存在:继续查询 Redis 或数据库。
- ❗ 可能出现误判:把不存在的数据判断为可能存在,但在正确实现和维护的前提下,不会把实际存在的数据判断为不存在。
text
用户请求
↓
布隆过滤器判断
├── 一定不存在 → 直接返回
└── 可能存在 → 查询 Redis → 必要时查询 MySQL
优点: 能够在大量无效请求到达数据库之前进行拦截。
缺点: 存在一定的误判率;数据新增、删除时需要设计合理的维护策略。
方案三:参数校验
在请求进入缓存和数据库之前,先检查参数是否合法。例如,用户 ID 必须为正整数,明显非法的参数直接拒绝。
3. 适用场景
- 缓存空值: 适合不存在的数据可能被反复查询的业务。
- 布隆过滤器: 适合数据规模较大、无效查询较多的业务。
- 参数校验: 适合能够提前识别非法请求的场景。
二、缓存击穿:热点 Key 突然失效
1. 什么是缓存击穿?
缓存击穿是指某个被大量请求访问的热点 Key 突然过期或被删除,导致大量并发请求同时访问数据库。
例如,某件商品正在参加秒杀活动,其缓存恰好过期,大量用户同时查询商品详情,数据库就可能瞬间承受巨大的压力。

⚠️ 核心问题: 单个热点 Key 失效,引发大量并发请求同时回源。
2. 解决方案
方案一:互斥锁(Mutex Lock)
当缓存失效时,只允许一个请求获得锁并查询数据库、重建缓存,其他请求等待、重试或返回降级结果。
text
缓存未命中
↓
尝试获取互斥锁
├── 获取成功 → 查询数据库 → 重建缓存 → 释放锁
└── 获取失败 → 等待、重试或返回降级结果
优点: 能够控制并发回源的数量,避免大量请求同时查询数据库。
缺点: 可能增加请求延迟;锁的使用不当还可能造成死锁、重复回源等问题。
💡 注意: 实际开发中需要考虑锁的过期时间、异常释放、等待超时等问题。分布式场景下,不能简单地认为执行一次
SETNX就解决了所有锁安全问题。
方案二:逻辑过期
不直接依靠 Redis Key 的 TTL 删除数据,而是在缓存数据中保存一个逻辑过期时间。
当数据超过逻辑过期时间时:
- 请求仍然读取旧缓存,避免大量请求同时访问数据库。
- 只有一个请求负责异步刷新缓存。
- 刷新成功后,用新数据替换旧数据。
text
请求到达
↓
读取缓存
↓
判断逻辑过期时间
├── 未过期 → 返回缓存数据
└── 已过期 → 返回旧数据,并触发异步刷新
优点: 能够降低请求等待时间,适合高并发读取场景。
缺点: 可能返回旧数据,不适合所有业务。
❗ 注意: 逻辑过期不是让数据永远不更新。必须配合后台刷新机制,并考虑刷新失败、重复刷新和数据一致性问题。
方案三:热点数据主动刷新
对于访问量极高的数据,可以通过定时任务、消息通知或业务更新事件提前刷新缓存,降低热点 Key 过期时的并发冲击。
3. 适用场景
- 互斥锁: 适合希望严格控制数据库回源并发量的场景。
- 逻辑过期: 适合允许短时间读取旧数据的高并发业务。
- 主动刷新: 适合商品详情、热门文章等访问频率较高的数据。
三、缓存雪崩:大量缓存同时失效
1. 什么是缓存雪崩?
缓存雪崩是指大量缓存 Key 在短时间内集中失效,或者 Redis 服务整体不可用,导致大量请求涌向数据库。
常见原因主要有两种:
- 大量 Key 设置了相同或相近的过期时间,导致缓存集中失效。
- Redis 出现故障,导致大量缓存请求无法正常处理。
例如,系统批量缓存了大量商品数据,并统一设置为 30 分钟后过期。到期后,大量请求同时访问数据库,可能引发数据库过载。

⚠️ 核心问题: 受到影响的不再只是一个热点 Key,而可能是大量缓存数据甚至整个缓存服务。
2. 解决方案
方案一:过期时间增加随机值
为每个 Key 的 TTL 增加随机偏移量,避免大量缓存同时过期。
go
// 在 30 分钟基础上增加 0~299 秒的随机时间
ttl := 30*time.Minute +
time.Duration(rand.Intn(300))*time.Second
err := rdb.Set(ctx, key, value, ttl).Err()
优点: 实现简单,能够分散缓存过期时间。
缺点: 只能缓解集中失效,无法解决 Redis 服务整体故障。
💡 注意: 随机值的范围应根据业务流量和缓存规模确定。Go 中使用
math/rand时,也要结合所用 Go 版本及并发场景选择合适的随机数使用方式。
方案二:构建 Redis 高可用架构
通过 Redis Sentinel(哨兵)或 Redis Cluster(集群)等方式,提高缓存服务的可用性。
- 哨兵模式: 监控主从节点,在主节点发生故障时执行故障转移。
- 集群模式: 通过数据分片和节点冗余等机制,提升扩展能力与故障处理能力。
优点: 降低单点故障对业务的影响。
缺点: 架构和运维复杂度增加;故障转移期间仍可能出现短暂的请求失败。
❗ 注意: 高可用不等于绝对不会发生故障。还需要做好容量规划、故障演练、监控告警和客户端重试控制。
方案三:限流、熔断与降级
当 Redis 故障或数据库压力过大时,对请求进行限流,限制进入数据库的请求数量。
- 限流: 控制单位时间内允许处理的请求数量。
- 熔断: 当下游服务持续异常时,暂时停止调用,避免故障扩散。
- 降级: 非核心业务可以返回默认值、旧数据或提示稍后重试。
优点: 能够保护数据库和核心业务,避免故障进一步扩大。
注意: 降级策略必须结合业务设计。支付、库存等核心操作不能简单地返回成功或使用过期数据完成写入。
方案四:多级缓存
在 Redis 前增加本地缓存,例如 Go 项目中的进程内缓存,用于保存部分热点数据。
text
用户请求
↓
本地缓存
├── 命中 → 直接返回
└── 未命中
↓
Redis
├── 命中 → 返回数据
└── 未命中 → 查询 MySQL
优点: Redis 出现短暂故障时,本地缓存仍可能承担一部分读取请求。
缺点: 多实例之间可能出现数据不一致,还需要考虑本地缓存的容量和更新策略。
3. 适用场景
- 随机 TTL: 适合批量缓存、定时刷新等场景。
- Redis 高可用: 适合对缓存服务可用性要求较高的系统。
- 限流降级: 适合需要保护数据库和核心业务的系统。
- 多级缓存: 适合热点数据明显、读取频率较高的业务。
四、三大缓存问题对比与实战注意事项
1. 三大问题对比
| 对比维度 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
|---|---|---|---|
| 核心原因 | 查询的数据不存在 | 热点 Key 失效 | 大量 Key 集中失效或 Redis 故障 |
| 影响范围 | 无效查询反复访问数据库 | 单个或少数热点数据 | 大量缓存请求受到影响 |
| 典型表现 | 数据库反复执行无效查询 | 大量并发请求同时回源 | 数据库请求量突然暴增 |
| 主要方案 | 缓存空值、布隆过滤器 | 互斥锁、逻辑过期 | 随机 TTL、高可用、限流降级 |
| 核心目标 | 拦截无效查询 | 控制热点数据回源 | 分散失效并保护系统 |
2. 实际开发中的注意事项
① 缓存问题需要组合治理
不要认为使用某一种方案就能解决所有问题。例如,随机 TTL 可以缓解缓存雪崩,但不能阻止非法请求造成的缓存穿透。
② 缓存一致性不能忽略
数据库更新后,缓存可能仍然保存旧数据。需要结合业务场景选择合适的缓存更新、删除与重建策略。
③ 不要让缓存故障演变为数据库故障
当 Redis 不可用时,如果所有请求都直接访问 MySQL,缓存故障就可能进一步演变为数据库过载。应结合限流、超时、熔断和降级保护数据库。
④ 热点数据需要重点监控
可以关注缓存命中率、Redis 延迟、数据库 QPS、连接池使用情况、缓存 Key 过期情况以及异常请求比例,及时发现潜在风险。
⑤ 核心业务必须保证数据正确性
对于支付、库存扣减等业务,不能只依赖缓存。需要结合数据库事务、原子操作、幂等机制等手段,保证业务数据的正确性。
总结
Redis 三大缓存问题可以用三个关键词记忆:
- 🔍 缓存穿透:查不到。 缓存和数据库中都没有数据,使用缓存空值、布隆过滤器等方式拦截。
- 🔥 缓存击穿:热点失效。 热门 Key 突然过期,使用互斥锁、逻辑过期或主动刷新控制并发回源。
- 🌊 缓存雪崩:大面积失效。 大量缓存同时过期或 Redis 故障,使用随机 TTL、高可用架构、限流降级等方式降低影响。
真正可靠的缓存设计,不是只解决某一种问题,而是从请求拦截、缓存更新、并发控制、服务可用性和数据库保护等多个层面建立防护机制。