后端 · 缓存
高并发场景下 Redis 缓存三大经典问题与解决方案
缓存是提升系统吞吐、降低数据库压力的标配。但在高并发场景下,如果缓存设计不当, 它反而会成为系统的"阿喀琉斯之踵"。本文聚焦最经典的三个问题——缓存穿透、缓存击穿、缓存雪崩, 从成因到解法系统梳理,并给出可落地的代码思路(以 Java + Redis 为例)。
1缓存穿透(Cache Penetration)
是什么:查询一个根本不存在的数据,缓存和数据库都没有, 请求每次都"穿透"缓存、直击数据库。攻击者用不存在的 id(如负数、超大数据、随机串)高频请求, 就能用极低成本把数据库打垮。
解决方案
① 缓存空值(Cache Null):对查不到的数据也缓存一个空标记(如 NULL / ""),并设置较短 TTL。
这样同一 key 的后续请求直接命中缓存返回空,不再穿透到数据库。
// 缓存空值:防止不存在的 key 反复穿透到数据库 public String getWithCacheNull(String key) { String val = redis.get(key); if (val != null) { if (NULL_PLACEHOLDER.equals(val)) return null; // 命中空值标记 return val; } val = db.query(key); if (val == null) { redis.setex(key, 60, NULL_PLACEHOLDER); // 空值缓存,短 TTL return null; } redis.setex(key, 3600, val); return val; }
② 布隆过滤器(Bloom Filter):在缓存前加一层 BloomFilter,把所有可能存在的 key 预热进去。 请求先过 BF,不存在的 key 直接拦截,100% 挡掉穿透。这是拦截穿透最彻底的方案。
// 伪代码:布隆过滤器前置拦截 if (!bloomFilter.mightContain(key)) { return null; // 一定不存在,直接返回,不查缓存/库 } // 可能存在,再走缓存 → 数据库流程
③ 接口层校验 + 限流:参数合法性校验(如 id > 0)、网关/Redis 维度限流,封掉恶意扫描与异常流量。
2缓存击穿(Cache Breakdown)
是什么:某个热点 key 在过期的瞬间,大量并发请求同时涌入,全部未命中缓存, 瞬间一起打到数据库,导致数据库被单点打挂。
解决方案
① 互斥锁重建(分布式锁):只有一个线程去查库并重建缓存,其余线程等待重试或返回旧值。
用 Redis SET NX 实现锁;注意锁超时与误删(用 UUID + Lua 原子释放)。
// 互斥锁重建:同一时刻只有一个线程回源数据库 public String getWithMutex(String key) { String val = redis.get(key); if (val != null) return val; String lockKey = "lock:" + key; String uuid = UUID.randomUUID().toString(); try { boolean locked = redis.set(lockKey, uuid, "NX", "EX", 30); if (locked) { val = db.query(key); // 只有拿到锁的线程查库 redis.setex(key, 3600, val); redis.eval(UNLOCK_LUA, lockKey, uuid); // Lua 原子释放 return val; } else { Thread.sleep(50); // 未拿到锁,短暂等待后重试 return getWithMutex(key); } } catch (Exception e) { redis.eval(UNLOCK_LUA, lockKey, uuid); throw e; } }
② 逻辑过期(Logical Expire):缓存中不设物理 TTL,而是存一个"逻辑过期时间"字段; 发现逻辑过期时,另起线程异步重建,当前请求先返回旧值。优点是不阻塞请求,缺点是返回的可能不是最新值(最终一致)。
③ 热点 key 永不过期 + 后台刷新:对极热点 key 不设过期,由后台定时任务或订阅数据变更主动更新,从根本上消除"过期瞬间"问题。
3缓存雪崩(Cache Avalanche)
是什么:大量缓存 key 在同一时间集中失效,或 Redis 实例宕机, 导致海量请求直击数据库,数据库瞬时压力过大甚至崩溃。
解决方案
① 过期时间加随机抖动:TTL = base + random(0, jitter),把失效时间点打散,避免集体失效。
// 过期时间加随机抖动,避免批量 key 同时失效 int base = 3600; int jitter = new Random().nextInt(600); // 0 ~ 600s 随机 redis.setex(key, base + jitter, value);
② 多级缓存:本地缓存(Caffeine / Guava)+ 分布式缓存(Redis)+ 回源,降低对 Redis 的单点依赖。
③ Redis 高可用:哨兵 / Cluster 部署,主从切换,避免单点故障引发整体雪崩。
④ 限流、降级、熔断:用 Sentinel / Hystrix 保护数据库,超时快速失败,返回默认或降级数据。
⑤ 缓存预热:大促前把热点数据提前加载进缓存,避免冷启动时的集中回源。
4三者对比一览
| 维度 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
|---|---|---|---|
| key 是否存在 | 不存在 | 存在(热点) | 存在(批量) |
| 触发点 | 查询不存在的数据 | 单热点 key 过期瞬间 | 大量 key 同时过期 / Redis 宕机 |
| 主要影响 | 无效请求压垮数据库 | 单点数据库被打挂 | 数据库瞬时过载 / 崩溃 |
| 核心解法 | 缓存空值、布隆过滤器、校验限流 | 互斥锁、逻辑过期、热点不过期 | 过期抖动、多级缓存、高可用、限流降级、预热 |
5最佳实践小结
- 所有缓存 key 设置合理 TTL,并加随机抖动;
- 热点 key 单独治理(不过期 / 逻辑过期 / 互斥重建);
- 不存在的 key 用空值 + 布隆过滤器兜底;
- Redis 集群化 + 多级缓存 + 限流降级,多层防护;
- 监控缓存命中率与数据库 QPS,命中率骤降即告警。
缓存不是为了"加了就快",而是要在一致性、可用性、成本之间做权衡。 把穿透、击穿、雪崩这三座大山各自的解法落到架构里,缓存层才算真正"高可用"。
© 2026 JokerChou's Blog · 用 ❤️ 与 ☕ 制作 · 返回博客首页