5.4 缓存机制与淘汰策略


5.4 缓存机制与淘汰策略

缓存是 Redis 的第一大用途,也是生产事故的第一大来源。本节把缓存的读写模式与三大经典故障(穿透、击穿、雪崩)逐一拆解,每项给出防御实现;再讲内存满时的淘汰策略八档怎么选。5.1 演练过的"加缓存"只是入门,这一节才是缓存的生产说明书。

缓存的读写模式

**Cache-Aside(旁路缓存)**是最常用的模式:应用读缓存未命中则读库并回填缓存;写时先更库、后删缓存(而非更新缓存)。两个为什么:为什么删而不是更?因为更新后的缓存可能没人读,白写还引入并发写覆盖问题。为什么先库后删?反过来的话,并发下旧值回填缓存的窗口更大。即便如此,先更库后删缓存仍有微小不一致窗口(读请求恰在更库前读到旧缓存),彻底方案是延迟双删或订阅变更日志异步失效,按业务容忍度选。

三大故障与防御

穿透:查询不存在的数据,缓存永不命中、请求全部打到库——攻击者构造海量随机 ID 即可打穿。防御两招:缓存空值(查不到也把空结果缓存短时间,简单有效);布隆过滤器(所有合法 ID 预先登记,查询前过滤,"一定不存在"的判断零成本,代价是少量误判与不支持删除)。

击穿:某个热点键过期瞬间,并发请求同时未命中、同时回源建库,数据库瞬间被打出尖峰。防御三板斧:互斥重建(只放一个请求去查库回填,其余等待或返回旧值——用 5.6 的 SET NX 锁实现);逻辑过期(键永不过期、值里带过期字段,发现过期后异步刷新,期间返回旧值);热点不过期(运营级热点直接常驻)。

雪崩大量键同时过期(或 Redis 实例整体宕机),请求洪峰涌向数据库。防御:过期时间加随机偏移(如 30 分钟 ± 5 分钟,把到期打散);多级缓存(本地缓存挡第一波);限流降级预案(缓存全灭时按优先级放行);实例可用性交给主从与哨兵(5.7)。

图:缓存三大故障的请求路径对比

图:缓存三大故障的请求路径对比

淘汰策略:内存满了谁出局

maxmemory 设定上限后,写不进时怎么办由淘汰策略(maxmemory-policy)决定,八档按语义分组:noeviction(默认,拒绝写入,缓存场景慎用);allkeys-lru / volatile-lru(全键或仅带过期键中淘汰最近最少使用,常规缓存首选 allkeys-lru);allkeys-lfu / volatile-lfu(最少频率使用,适合偶发访问的"历史热键"不再霸占内存);allkeys-random / volatile-random(随机,少用);volatile-ttl(淘汰剩余寿命最短的)。判断口诀:缓存场景 allkeys-lru 或 lfu 起步;混合场景(既有缓存又有不能丢的键)用 volatile 系并给所有可丢键设过期——淘汰只发生带过期键的集合里。

演练:活动页雪崩事故的完整复盘

背景:运营活动凌晨整点上线,预热脚本把 2000 个活动键统一设置 2 小时过期;凌晨 2 点整 2000 个键同时过期,瞬间上万请求回源,数据库连接池打满,整个站点接口雪崩 5 分钟。

操作:复盘四步。第一步定位:监控显示数据库 QPS 尖峰与键过期时间点精确重合,雪崩实锤;第二步即时止血:紧急预热 + 回填,临时调大数据库连接池上限扛过残余流量;第三步根因修复:预热脚本改为"基础过期 2 小时 + 随机偏移 0 到 20 分钟",热点 Top 键改逻辑过期;第四步防御补强:接入"缓存命中率低于阈值自动限流"的降级开关,并演练一次 Redis 实例整体重启。

结果:下一场活动同类流量下数据库 QPS 平稳(回源请求按分钟级摊开);演练的实例重启中降级开关按预期生效,站点未雪崩。

解读:复盘链条值得记牢——止血(回填与限流)→ 根因(过期模式)→ 防御(打散与逻辑过期)→ 演练(整体失效预案)。四步缺一不可:只改过期模式不演练,下次实例宕机还是会雪崩;只演练不修根因,问题必然复发。

变式:若活动数据本质是只读商品快照,更省的方案是活动期直接静态化到 CDN,Redis 只留动态部分——缓存是分层体系的一层,不是唯一一层。

易错点

第一个是为省内存把过期时间设得过短:命中率崩塌,回源流量反噬数据库;过期时长要用命中率与回源量的监控数据校准,不靠拍脑袋。第二个是布隆过滤器当真理:它说"不存在"才可信,说"存在"可能是误判,业务判断逻辑要按这个语义写。第三个是互斥重建的锁忘记过期:持锁进程崩溃导致所有请求永久等待,锁必须带过期时间与重试上限。第四个是淘汰策略与业务错配:混合场景误用 allkeys 系,把不能丢的分布式锁键也淘汰了——锁类键要么不与缓存同实例,要么用 volatile 系隔离。

八大淘汰策略速查表

内存达到 maxmemory 上限时,Redis 按配置的策略淘汰数据。策略选错,命中率会断崖式下跌。

策略 淘汰范围 特点 适用
noeviction 不淘汰,写入报错 数据绝不丢 缓存与存储混用时(不推荐)
allkeys-lru 所有键 淘汰最久未访问的 纯缓存,最常用
allkeys-lfu 所有键 淘汰访问频次最低的 有明显冷热区分
allkeys-random 所有键 随机淘汰 访问分布均匀
volatile-lru 仅设了过期时间的键 只淘汰可过期的 缓存与持久数据混存
volatile-lfu 仅设了过期时间的键 按频次淘汰可过期的 同上,有冷热区分
volatile-ttl 仅设了过期时间的键 优先淘汰剩余寿命短的 明确知道哪些数据应先走
volatile-random 仅设了过期时间的键 随机淘汰可过期的 少用
# 配置:内存上限与淘汰策略 maxmemory 8gb maxmemory-policy allkeys-lru # 观察淘汰与命中情况 INFO stats | grep -E "keyspace_hits|keyspace_misses|evicted_keys" # 命中率 = hits / (hits + misses),持续低于 90% 就该查原因

三大缓存故障与防御清单

穿透:查询一个根本不存在的键,每次都打到数据库。防御两招:其一,缓存空值(查到不存在时把空结果也写进缓存,设短过期时间);其二,布隆过滤器(在缓存前加一层,判断键是否存在,不存在的直接返回)。

# 缓存空值:注意过期时间要短,防止恶意刷不同 Key 撑爆内存 SET user:profile:99999999 "{}" EX 60 # 布隆过滤器思路(Redis 4.0+ 可用 BF.ADD / BF.EXISTS 模块,或用 Bitmap 自建) BF.ADD user_filter 90001 BF.EXISTS user_filter 99999999 # 返回 0 表示一定不存在,直接返回,不打数据库

击穿:一个热点键过期的瞬间,大量请求同时打到数据库。防御手段是"互斥重建":缓存未命中时先抢分布式锁,只有抢到锁的线程去数据库加载并回填,其余线程等待后重读缓存。

# 互斥重建(伪命令示意) SET lock:rebuild:hot_key <token> NX PX 3000 # 抢到锁 → 查数据库 → SETEX hot_key 300 <value> → DEL lock # 未抢到 → 短暂 sleep 后重读缓存;超时则返回降级内容

雪崩:大批键在同一时刻集体过期,或缓存集群整体不可用,请求全部涌向数据库。防御三招:过期时间加随机抖动(避免同时过期)、缓存层做高可用(哨兵或集群,见 5.7、5.8)、以及数据库侧的限流与熔断兜底。

# 过期时间加抖动:基准 300 秒 ± 60 秒随机 EXPIRE cache:item:8801 $((300 + RANDOM % 120))

一条总原则:缓存层的设计目标不是"不出问题",而是"出问题时数据库不被打垮"。因此限流、熔断、降级这三样兜底措施,与缓存策略同等重要——它们决定了最坏情况下系统是"慢一点"还是"整体不可用"。

本节要点回顾

  • 读模式 Cache-Aside:读未命中回填,写先更库后删缓存。
  • 三故障三防御:穿透(空值缓存、布隆过滤器)、击穿(互斥重建、逻辑过期)、雪崩(过期打散、多级缓存、限流降级)。
  • 淘汰策略口诀:纯缓存 allkeys-lru/lfu,混合场景 volatile 系并保证可丢键都有过期。
  • 故障复盘链条:止血 → 根因 → 防御 → 演练,四步闭环。
  • 缓存分层看:CDN、本地缓存、Redis、数据库,各挡各的流量。

故障防住了,下一节看 Redis 的第二副面孔:消息传递。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U