在绝大多数的后端架构中,引入 Redis 作为缓存层几乎是提升系统并发能力的“银弹”。然而,缓存并非万能药。当你的业务流量真正开始飙升时,隐藏在平静水面下的暗礁就会显现。

今天,结合我之前的生产环境踩坑经验,咱们来聊聊 Redis 缓存层最令人头疼的“三座大山”:缓存穿透、缓存击穿与缓存雪崩,以及它们对应的实战解决方案。

一、缓存穿透 (Cache Penetration)

场景还原
黑客或者恶意用户疯狂请求一个在数据库中根本不存在的数据(例如 ID 为 -1 的商品)。
此时,请求每次都会先去查 Redis,发现没有,然后直接打到 MySQL。因为数据库里也没有,所以无法将空结果回写到 Redis。最终的结果就是:Redis 形同虚设,海量请求直接把 MySQL 打挂。

如何破局?

  1. 缓存空对象 (Cache Null)
    最简单粗暴的方法。哪怕数据库查出来是空的,我也把这个“空值”存进 Redis,设置一个较短的过期时间(比如 5 分钟)。这样后续的恶意请求就会被 Redis 挡住。
    缺点:会浪费 Redis 的内存空间,且可能存在短期的数据不一致。
  2. 布隆过滤器 (Bloom Filter)
    这是目前最优雅的高阶方案。在请求到达 Redis 之前,先过一遍布隆过滤器。布隆过滤器能以极小的内存开销,快速判断一个 Key “绝对不存在”或“可能存在”。如果判定不存在,直接拒绝请求,根本不给它访问数据库的机会。

[在这里插入一张布隆过滤器的拦截原理架构图]

二、缓存击穿 (Cache Breakdown)

场景还原
这个通常发生在“热点数据”上。假设某个超级爆款商品的缓存刚好在秒杀进行到一半时过期了
就在这失效的短短一瞬间,成千上万的并发请求发现缓存没数据,于是如同猛兽出笼般,全部涌向 MySQL 去重建缓存,瞬间造成数据库瘫痪。

如何破局?

  1. 互斥锁 (Mutex Lock / SETNX)
    发现缓存失效后,不要让所有线程都去查数据库。而是让大家先去 Redis 里“抢锁”(利用 SETNX 命令)。抢到锁的那个幸运儿,去查数据库并重建缓存;没抢到的线程则休眠几毫秒后重试。
    1
    2
    3
    4
    5
    6
    7
    8
    // 伪代码示例
    if (redis.setnx("lock_key", "1", 10s)) {
    // 1. 查询 MySQL
    // 2. 写入 Redis
    // 3. 释放锁 redis.del("lock_key")
    } else {
    // 休眠 50ms 后重试获取缓存
    }
  2. 逻辑过期 (永不过期)
    物理上给这个热点 Key 设置不失效。而是把过期时间写在业务 Value 里面。每次取出数据时,判断一下是否到达了“逻辑过期时间”。如果过期了,直接返回旧数据,同时偷偷开启一个后台异步线程去数据库里拉取最新数据并更新。这种方案用户体验极佳,做到了平滑过渡。

三、缓存雪崩 (Cache Avalanche)

场景还原
比击穿更恐怖的是雪崩。通常发生在这两种情况:

  1. 大量的 Key 被设置了相同的过期时间,导致在同一时刻集体失效。
  2. Redis 节点意外宕机。
    此时,所有的流量会像雪崩一样压向后端数据库。

如何破局?

  1. 打散过期时间:在设置基础过期时间(如 1 小时)的基础上,再加上一个 1~5 分钟的随机浮动值。这样就能避免海量 Key 在同一秒集体失效。
  2. 高可用集群:不要让单节点 Redis 裸奔。上生产环境至少要配置主从复制 + 哨兵模式 (Sentinel) 或者直接上 Redis Cluster 集群,保证缓存层的高可用。
  3. 熔断与限流:这是最后一道防线。当发现 MySQL 的负载飙升到危险阈值时,直接通过 Sentinel 或 Hystrix 等组件进行限流,牺牲部分用户的体验,来保全整个系统的存活。

总结

引入缓存,就是在用系统的复杂性去换取性能。架构设计永远是一门“妥协与折中”的艺术,没有最好的方案,只有最适合你当前业务体量的方案。