缓存三兄弟

缓存穿透

是什么:大量请求查询数据库不存在的数据。缓存没有,持续击打数据库 防范方法:

  1. 缓存空值
    1. 数据库查不到也往缓存放一个空标记(TTL短,2分钟,该TTL过期了才能进行再次数据库查询)
    2. 下一次同样的id来了直接被挡住
    3. 优点:有效拦截大量穿透请求
    4. 缺点:
      1. 恶意攻击造成大量不同的不存在的key,缓存堆满无效数据,浪费内存
      2. 延迟了数据一致性,新数据必须等缓存过期
  2. 布隆过滤器(推荐)
    1. 启动时把所有合法id放入布隆过滤器;
    2. 布隆过滤器不给进就真的没有;
    3. 优点:
      1. 没有高内存占用风险
      2. 没有数据一致性风险
    4. 缺点:
      1. 有极小误判率
      2. 增删要维护。数据更新时,要同步更新布隆过滤器
  3. 接口层限流和校验
    1. API网关或Controller层做拦截
    2. 参数校验、限流降级
  4. 互斥锁重建
    1. 互斥锁主要解决缓存击穿(热点key过期),但是在缓存空对象场景下,如果有大量并发请求一个不存在的key,可以使用锁
    2. 每次访问让一个线程去查DB,其他线程等待

大厂的实践

  1. 网关层 校验参数格式,IP限流(挡掉脚本攻击)
  2. 过滤器层 布隆过滤器
  3. 缓存层

缓存击穿

是什么:某个被疯狂访问的热点key,因为TTL一到,失效的一瞬间海量并发请求同时miss,同时到DB重建同一个key

防范方法:

  1. 互斥锁
    1. 只让第1个miss的线程去查DB
    2. 其他线程等一下再读缓存
      1. 高一致性:线程傻傻等待
      2. 低一致性:先给旧数据,下一次来查就是新的
  2. 逻辑过期
    1. key永不物理过期,把过期时间存在value里面
    2. 如果发现逻辑上过期了,异步开个线程重建
    3. 当前请求先返回旧值

互斥锁的实现

如何搭建出正确的锁?

v1:SETNX key 1 抢锁,DEL key 释放

SETNX=“不存在才设置成功”,天然互斥。但问题:抢到锁的线程崩了、DEL 没执行 → 锁永远不释放 → 死锁。

v2:给锁加过期间

崩了也能自动过期。但关键:必须用一条原子命令 SET key value NX EX 30。

陷阱:别写成 SETNX + 再 EXPIRE 两条——如果刚 SETNX 成功、还没 EXPIRE 就崩了,又变回 v1 的死锁。“抢锁"和"设过期"必须一步做完。

v3 陷阱:误删别人的锁

场景:A 抢到锁、TTL 30s,但 A 干活干了 35s。第 30s 锁自动过期 → B 抢到了同一把锁 → 第 35s,A 干完了、DEL key → 把 B 的锁删了。于是 B、C
可能同时持锁,互斥失效。

修法:抢锁时写一个唯一标识(比如 UUID)当 value,释放时先检查 value 是不是自己的,是自己的才删。

v4 陷阱:检查和删除不是原子的

GET key 确认是自己 → DEL key,这是两步。万一 GET 之后、DEL 之前,锁恰好过期、被 C 抢走,你的 DEL 又把 C 的删了。

修法:用 Lua 脚本把"比对 value + 删除"打包成一条原子操作(Redis 单线程执行lua,中间插不进别的命令)。

v5 及以上(了解)

  • 看门狗续期:任务可能比 TTL 长,怎么办?后台起个线程定期给锁续期,快过期就延长——Redisson
    的看门狗就是干这个的。
  • Redisson:生产里基本不手写,直接用 Redisson 的 RLock(封装了上面所有坑 + 可重入 + 看门狗)。
  • 红锁 RedLock:单点 Redis 主从切换会丢锁,红锁想用多个独立节点解决,但业界有争议(Martin Kleppmann 那场著名辩论)。面试能说出"知道有红锁、也知道它有争议、生产我会用Redisson"就到位了。

缓存雪崩

是什么:大批key同一时刻集体过期。 场景:

  1. 系统启动时一次性灌满了一堆缓存,TTL都一样
  2. Redis整个挂了,海量请求同时打到DB

和击穿的区别:击穿是一个热点key;雪崩是一大片key

防范方法:

  1. TTL加随机值
  2. Redis高可用
    1. 主从
    2. 哨兵
    3. 集群
    4. 不让redis单点挂掉引发雪崩
  3. 多级缓存+限流降级
    1. 本地缓存先扛一层
    2. 最后用熔断限流保护

Cache-Aside 旁路缓存

适用的场景:所有读多写少的情况 实际业务:用户的资料信息、商品详情页等 决策口诀:“如果这个数据,用户刷新一次页面就要查一次,但用户一天只会改一次,那么闭眼用 Cache-Aside。” 作用过程:读时填充,写时失效

案例1:聊天室在线列表。 很多人同时查询在线列表。不能都去数据库读。 将在线列表缓存到 Redis 中。查询直接查 Redis。

聊天室人数增加/减少 -> 触发写DB -> 删除缓存 与此同时, 查询聊天室在线列表 -> 缓存失效 -> 到DB查 -> 将DB写入缓存