关于91大事件,我把缓存管理讲清楚后,很多问题都通了(越早知道越好)

前言 在一次名为“91大事件”的流量与数据波动考验中,我们团队遇到了各种看似无关但本质相通的问题:响应延迟飙升、后端数据库连接耗尽、用户看到过期数据、缓存穿透和雪崩……把缓存管理体系彻底理顺后,很多故障像多米诺骨牌一样被解决了。本文把实战中最有用的思路、策略和可落地的方法整理出来,目的是把复杂问题拆成几个清晰步骤,让你越早知道越少踩坑。
为什么在大事件时缓存会暴露问题
- 爆发性流量放大了设计缺陷:原本能扛的小漏掉,瞬间变成系统瓶颈。
- 缺乏分层与一致的失效策略:不同层缓存互相矛盾导致数据不一致或难以定位问题。
- 缺少防护机制:缓存穿透、击穿、雪崩在流量高峰时同时发生,后端承受不了瞬间请求涌入。
- 监控与可观测性不足:没有把关键指标串在一起,定位与恢复都变慢。
先说结论(节约时间)
- 把缓存当作系统的一等公民来设计:分层、可观测、可控、可回滚。
- 使用缓存版本化与幂等失效策略,避免直接依赖短 TTL 来解决一致性。
- 针对高并发读场景采用缓存预热、单点重建、随机化 TTL 与请求合并策略来防止雪崩与击穿。
- 配置必要的监控:命中率、延迟、后端流量、缓存大小与淘汰率、错误率和重建次数。
缓存分层与职责 将缓存分成多个层次并明确各自职责,会大幅简化故障排查:
- CDN / 边缘层:负责静态资源、热点页面的全球分发与带宽削峰。适合大时间粒度的缓存与快速失效策略(purge)。
- 代理缓存(如 Nginx、Varnish):做近端缓存与路由优化,减少后端 HTTP 请求。
- 应用层缓存(内存,比如 Redis / Memcached):各类实时数据的热点缓存,读写延迟低。
- 数据库缓存(查询结果缓存、物化视图):减少复杂查询对数据库的压力。 清楚每层的 TTL、失效触发点与一致性期望,是设计核心。
常见问题与可行策略 1) 缓存穿透(恶意或随机请求绕过缓存,直击后端)
- 对策:请求参数校验、布隆过滤器或布尔型防护(先查布隆,再查缓存,再查 DB)。对高成本接口可以增加限流或人机验证。
2) 缓存击穿(热点 key 在失效瞬间大量并发请求落到后端)
- 对策:
- 加锁/单飞(singleflight)机制:第一个请求去加载并重建缓存,其他请求等待或返回旧值;
- 互斥重建:在 Redis 使用 setnx 设置重建标志;
- 使用“永不过期”缓存加后台异步刷新(stale-while-revalidate)策略,先返回旧数据再异步刷新。
3) 缓存雪崩(大量缓存同一时间失效)
- 对策:
- 随机化 TTL:给同类 key 的 TTL 加上随机抖动,避免同步失效;
- 分批或延迟失效:将失效时间分散到更长窗口;
- 容错降级策略:当缓存不可用时,服务退化到降级页面、静态响应或限流,保证可用性。
4) 数据不一致(多层缓存与写操作不同步)
- 对策:
- 事件驱动失效:写操作后发布事件(消息队列、Redis pub/sub)通知各层清理;
- 版本化 key(例如 key:v2:…):写入新数据时使用新版本,读端兼容老版本;
- 写时双写、最终一致:对大多数场景采用缓存先更新或延时双写需谨慎设计,优先选择显式失效或版本化。
5) 缓存容量与淘汰(OOM 与频繁回源)
- 对策:
- 设定合理的容量与 eviction 策略(LRU、LFU、TTL 协同);
- 将不同热度的数据分桶(hot/warm/cold)并分别配置不同资源;
- 定期剔除大对象或采用压缩、分片策略。
实战模式与代码思路(伪代码) 缓存旁路:缓存击穿的单飞实现(思想)
- 首先尝试 cache.get(key)
- 命中直接返回
- 未命中尝试 setnx(lock_key),成功的请求去 DB 加载并写入 cache,失败的请求短等待重试或返回旧值
伪代码: lock = CACHE.setnx(lockkey, ttl=5s) if cache.has(key): return cache.get(key) if lock: val = db.query(key) cache.set(key, val, ttl=basettl + rand(0..jitter)) CACHE.del(lockkey) return val else: sleep(short) return cache.get(key) or fallbackresponse
stale-while-revalidate(先返回旧值再异步刷新)
- 缓存存储两个字段:value + expiry_real(真实过期时间)
- 当读取到 value 且 expiry_real < now 时,返回 value 给用户,同时异步触发后台刷新
监控指标(必须看的那几项)
- Cache hit rate(按接口/按 key 分类)
- Cache latency(平均与 P95、P99)
- 后端请求量(缓存断层前后对比)
- Cache evictions / memory usage(是否频繁驱逐)
- Cache rebuild次数与失败率(锁重试、超时) 收集这些指标,并在大事件前建立基线,会让你在风暴中冷静决策。
落地步骤清单(我在现场用的顺序,便于复现) 1) 列出所有缓存层与各自的职责与 owner(谁维护)。 2) 把热点接口或页面排出来,按 QPS 与后端成本优先级排序。 3) 为每个热点定义合理的 TTL 与抖动参数,列出失败回退方案。 4) 对写操作,决定是事件驱动失效还是版本化 key,并实现对应机制。 5) 在关键读取路径引入单飞/互斥重建或 stale-while-revalidate 模式。 6) 上线前做好流量回放或压测,验证命中率与后端负载。 7) 加入监控告警:命中率骤降、eviction 突增、重建错误触发告警。 8) 事件期间保持只读回滚路径与快速 purge/版本切换手段,避免盲目修改核心配置。
案例回顾(简要说明我们怎么解决) 在“91大事件”中,用户活动促使某些排行榜、热点页面的请求猛增。问题集中在:
- 热点 key 统一在凌晨失效,造成短时间内大量回源。
- 缓存重建逻辑无互斥,DB 被并发击垮。 我做了三件事: 1) 给热点 key 增加随机化 TTL 并把部分热点标记为永不过期,改为后台定时刷新。 2) 在应用层实现 singleflight,保证只有一个线程去刷新数据,其他请求等待或使用旧数据。 3) 增加监控并设置自动降级策略(超过阈值返回静态缓存页面)。 结果:数据库压力下降 70%+,页面响应 P99 降低明显,用户投诉率与错误率显著下降。
常见误区(短评)
- TTL 越短越新鲜:短 TTL 会导致频繁回源,反而更容易触发雪崩。用 TTL+抖动+版本化更稳。
- 所有数据都适合缓存:并非如此。高写低读的场景,缓存价值有限,反而增加一致性负担。
- 只靠 CDN 就能解决所有问题:CDN 对静态与可缓存页面有效,但对 API 层、用户个性化数据帮助有限。
结尾与行动建议 缓存不是“装饰”,而是系统架构里能显著改变可靠性与成本的部分。把缓存体系做成可观测、可控制并且有回滚方案的模块,会在每次流量大考中给你足够的余地。越早把这些原则和工具放到位,遇到“91”这样的事件时就越少手忙脚乱。

