缓存三兄弟
缓存穿透
是什么:大量请求查询数据库不存在的数据。缓存没有,持续击打数据库 防范方法:
- 缓存空值
- 数据库查不到也往缓存放一个空标记(TTL短,2分钟,该TTL过期了才能进行再次数据库查询)
- 下一次同样的id来了直接被挡住
- 优点:有效拦截大量穿透请求
- 缺点:
- 恶意攻击造成大量不同的不存在的key,缓存堆满无效数据,浪费内存
- 延迟了数据一致性,新数据必须等缓存过期
- 布隆过滤器(推荐)
- 启动时把所有合法id放入布隆过滤器;
- 布隆过滤器不给进就真的没有;
- 优点:
- 没有高内存占用风险
- 没有数据一致性风险
- 缺点:
- 有极小误判率
- 增删要维护。数据更新时,要同步更新布隆过滤器
- 接口层限流和校验
- API网关或Controller层做拦截
- 参数校验、限流降级
- 互斥锁重建
- 互斥锁主要解决缓存击穿(热点key过期),但是在缓存空对象场景下,如果有大量并发请求一个不存在的key,可以使用锁
- 每次访问让一个线程去查DB,其他线程等待
大厂的实践
- 网关层 校验参数格式,IP限流(挡掉脚本攻击)
- 过滤器层 布隆过滤器
- 缓存层
缓存击穿
是什么:某个被疯狂访问的热点key,因为TTL一到,失效的一瞬间海量并发请求同时miss,同时到DB重建同一个key
防范方法:
- 互斥锁
- 只让第1个miss的线程去查DB
- 其他线程等一下再读缓存
- 高一致性:线程傻傻等待
- 低一致性:先给旧数据,下一次来查就是新的
- 逻辑过期
- key永不物理过期,把过期时间存在value里面
- 如果发现逻辑上过期了,异步开个线程重建
- 当前请求先返回旧值
互斥锁的实现
如何搭建出正确的锁?
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同一时刻集体过期。 场景:
- 系统启动时一次性灌满了一堆缓存,TTL都一样
- Redis整个挂了,海量请求同时打到DB
和击穿的区别:击穿是一个热点key;雪崩是一大片key
防范方法:
- TTL加随机值
- Redis高可用
- 主从
- 哨兵
- 集群
- 不让redis单点挂掉引发雪崩
- 多级缓存+限流降级
- 本地缓存先扛一层
- 最后用熔断限流保护
Cache-Aside 旁路缓存
适用的场景:所有读多写少的情况 实际业务:用户的资料信息、商品详情页等 决策口诀:“如果这个数据,用户刷新一次页面就要查一次,但用户一天只会改一次,那么闭眼用 Cache-Aside。” 作用过程:读时填充,写时失效
案例1:聊天室在线列表。 很多人同时查询在线列表。不能都去数据库读。 将在线列表缓存到 Redis 中。查询直接查 Redis。
聊天室人数增加/减少 -> 触发写DB -> 删除缓存 与此同时, 查询聊天室在线列表 -> 缓存失效 -> 到DB查 -> 将DB写入缓存