我把91网的缓存管理拆给你看:其实一点都不玄学(信息量有点大)

前言 最近在观察和分析几个大型中文网站的响应行为时,发现91网在缓存策略上做了很多常见却高效的工程化处理。下面把这些层次、思路和实现细节拆开来讲,既讲为什么这么做,也给出可直接落地的操作建议。声明:以下基于可观察到的外部行为和典型工程实践推断,便于复用到类似项目中。
整体缓存分层(从边缘到内核)
- CDN / Edge Cache:负责静态资源(js、css、图片)以及部分可缓存的 HTML 页面缓存。通过 Cache-Control、Expires、ETag、Surrogate-Control 控制边缘缓存。常见做法是把大文件长缓存(比如一年),HTML 采用短缓存或带 stale-while-revalidate 策略。
- 反向代理 / 缓存层(Nginx、Varnish):在 Origin 前做第二道缓存,处理动态页面的缓存或 API 响应的二级缓存,常用于降低回源压力并实现更细粒度的缓存策略(按路径或 Cookie 分流)。
- 应用层缓存(Redis / Memcached):存放业务数据、模板渲染结果、会话信息、限流计数器等。高频读、可容忍短期过期的数据放这里。
- 本地进程缓存(Caffeine、Guava、本地 LRU):避免短期内重复访问后端或 Redis,减少网络往返。一些热点数据使用本地缓存作为第一层。
关键设计点 1) 缓存键设计
- URL + Query String 精确控制,部分参数忽略或归一化(排序、删除无用参数)。
- 对用户相关数据以用户 ID、权限版本号等作为 key 的一部分,避免越权读取。
- 采用版本化 key(data:v2:xxx)来实现强制失效而不用逐条删除。
2) TTL 与分层策略
- 静态资源:长缓存(max-age=31536000)+ 文件名带 hash。
- 页面/接口:按稳定性分层。高度实时的接口短 TTL(几秒到几十秒),可松散一致的统计/榜单类数据可长 TTL(几分钟到几小时)。
- Redis 中对热点数据使用较短 TTL 并结合定期刷新(background refresh / prefetch)。
3) Cache-aside 模式(最常见)
- 应用先查缓存,未命中再访问后端并写回缓存。写操作同时更新数据源并删除/更新缓存键。
- 为避免并发穿透(cache stampede),常见做法有互斥锁(singleflight)、请求合并、以及概率过期。
4) 写入策略:写穿、写回与异步失效
- 以一致性优先的场景使用写穿(write-through),立刻更新缓存。
- 以性能优先的场景采用异步失效:先更新后端,再通过消息总线(Kafka/NSQ/RabbitMQ)通知各缓存节点清理或更新。消息驱动的失效是分布式系统常用的方案。
5) CDN 与回源控制
- 对于内容变更频繁但又需要 CDN 加速的资源,常用 Surrogate-Control + Cache-Control 配合边缘刷新(CDN API 清理或穿透白名单)。
- 采用长缓存+文件名 hash 是最简单可靠的方式。不能修改文件名时,使用 CDN 的快速 purge 接口并把更新时间放在响应头里以便追踪。
攻克常见难点
- Cache stampede(缓存雪崩):采用互斥锁、请求合并或概率性提前过期(比如把真实 TTL 减去一部分随机值),并把冷启动预热(warm-up)。
- Cache avalanche(缓存雨崩):避免大量 key 同时过期。给 TTL 加随机抖动(例如 TTL = base + rand(0, jitter))。
- 数据一致性:读多写少场景可以容忍短期不一致;写多场景建议同步失效或使用强一致方案。用事件驱动的失效并结合幂等写能降低复杂度。
- 热点 Key:对极热 Key 做本地缓存或单独热缓存分片;避免所有流量落到单个 Redis 实例。
监控与排查要点
- 必看指标:缓存命中率、命中/未命中率、后端 QPS、延迟分位(P50/P95/P99)、Redis 内存使用、eviction 率、命中热度分布(top keys)。
- 追踪链路:将缓存层纳入分布式追踪(Zipkin/Jaeger/OpenTelemetry),遇到高延迟可以看到是哪一层耗时。
- 日志与抽样:记录缓存 Key、命中结果、TTL 等,并对异常时段做抽样回放。
- 灰度与回滚:在修改缓存策略(比如改 TTL 或引入本地缓存)时,先做小流量灰度并持续观察命中率和后端压力。
实操级建议(落地清单)
- 静态资源:确保文件名带 hash + 长缓存 + CDN 持久化。
- API:先定义数据可容忍的延迟与一致性要求,再为不同接口设置合适 TTL。
- 避免 query 参数盲目作为 key,对不影响结果的参数做 Normalize。
- 给 TTL 加随机抖动以防齐刷刷过期。
- 热点 Key 放本地缓存并限流到 Redis,必要时做本地异步刷新。
- 引入缓存命中率告警(低于阈值触发)和 Redis eviction 告警。
- 对更新频繁的表使用消息总线驱动缓存失效,而不是逐条删除所有缓存(更稳定、可伸缩)。
调优案例(简单演示思路)
- 现象:某接口在 18:00 QPS 突增导致后端数据库崩溃。
- 排查:观察到缓存命中率在高峰时下降 → 大量 miss 打到 DB。
- 解决思路: 1) 给该接口设本地缓存 200ms,减少短期重复请求。 2) 将缓存策略改为 cache-aside + singleflight(合并同一时间窗口内对同 key 的回源请求)。 3) 针对榜单类数据改为定时刷新(CRON)写入缓存而不是依赖请求驱动回源。 4) 增加监控告警,观察效果并逐步放开。
常见工具链
- 缓存层:Redis、Memcached、Varnish、Nginx proxy_cache、CDN(Cloudflare/Cloudfront/Fastly)
- 本地缓存:Caffeine、Guava(Java),groupcache(Go)
- 消息与同步:Kafka、RabbitMQ、NSQ
- 监控与追踪:Prometheus + Grafana、Zipkin/Jaeger、ELK/EFK
结语(精简版) 缓存并不玄学:分层、键设计、TTL 策略、失效机制和监控四块做好了,系统就稳了。把问题拆成“哪些数据需要高速读”,“哪些数据允许短时不一致”,以及“如何在失效时平滑过渡回源”,这三点搞清楚,绝大多数缓存问题都能被工程化解决。对91网的观察显示:结构清晰、分层明确、用消息驱动失效并结合本地缓存,是他们在高并发下保持稳定的关键思路。希望这篇拆解能给你在自家系统的缓存设计上省下一堆折腾时间。