本文作者:V5IfhMOK8g

我把91大事件的缓存管理拆给你看:其实一点都不玄学

V5IfhMOK8g 昨天 124
我把91大事件的缓存管理拆给你看:其实一点都不玄学摘要: 我把91大事件的缓存管理拆给你看:其实一点都不玄学引子 91大事件是典型的高并发读多写少场景:用户刷事件列表、查看详情、筛选排序……每一次都可能触发后端和数据库的压力。一...

我把91大事件的缓存管理拆给你看:其实一点都不玄学

我把91大事件的缓存管理拆给你看:其实一点都不玄学

引子 91大事件是典型的高并发读多写少场景:用户刷事件列表、查看详情、筛选排序……每一次都可能触发后端和数据库的压力。一个稳健的缓存策略能把延迟拉低数倍、把数据库压力削到可控范围。下面把缓存设计拆成可执行的模块,既讲原理,也给出实践要点,方便直接上手。

整体思路(一句话版) 把缓存当成快速可失效的副本:以 cache-aside 为主线,配合版本化 key、分层缓存(本地+分布式)、防击穿与热点保护、事件驱动的精确失效,以及完整的可观测与回退链路。

缓存策略选型(场景驱动)

  • 读多写少(事件详情、排行、标签统计):cache-aside(客户端/中间层读先查缓存,未命中再读DB并写缓存)。
  • 需要强一致(付费改动、计数器):走后端事务/乐观锁,并把缓存作为只读层,或用写穿(write-through)谨慎处理。
  • 高吞吐实时汇聚(实时榜单):考虑流式计算 + 最终一致的缓存,避免同步写大量缓存。

Key 设计与版本化

  • 规范化 key:event:91:detail:{id}:v{ver}。把业务维度和版本放在 key 里,便于整体失效(版本号++)或逐条失效。
  • 版本化替代批量删除:当结构变化或需要全量刷新时,增加全局版本号比遍历删除更快、更安全。
  • 限长与编码:避免超长 key,避免使用直接 JSON 序列化 entire object 作 key。对复杂查询(多条件)用哈希摘要 + 可读前缀。

失效与更新策略

  • 精确失效:写事件(新增/修改/删除)后,通过消息队列(Kafka/Redis PubSub)广播变更事件,消费者只失效相关 key。
  • 延迟一致性与弱一致场景:对非关键展示数据允许短 TTL(30s-5min),用事件驱动补偿不一致。
  • 灰度回滚:对影响面大的缓存逻辑改动先在小流量试验,确保回退机制(版本回退或禁用开关)顺畅。

防止缓存雪崩、击穿和穿透

  • 雪崩(大量 key 同时过期):采用TTL抖动(随机化TTL),且对关键 key 做主动预热或滑动过期。
  • 击穿(热点 key 突发失效造成DB打爆):使用请求合并(singleflight)、分布式锁或互斥队列,只让一个请求去DB回填,其余等待或返回旧数据。
  • 穿透(查询不存在数据频繁打DB):对空结果设置短 TTL(例如 30s-1min),并可用布隆过滤器预先过滤掉明显不存在的 id。
  • 热点隔离:对热 key 单独限流并引入本地缓存(近源缓存)+限速队列。

本地缓存 vs 分布式缓存

  • 本地缓存(进程内 LRU,如 Caffeine、guava)用于极低延迟返回,适合用户会话或短期热点;缺点是失效广播复杂。
  • 分布式缓存(Redis/Memcached)用于共享状态和一致性。推荐:本地作为第一层、Redis 作为第二层,并用发布/订阅或 version 机制同步本地失效。
  • 容量预算:把内存容量按业务维度拆分(详情、列表、聚合),避免一个大对象挤爆整个缓存池。

序列化与内存优化

  • 选择高效二进制序列化(MessagePack、Protobuf)能显著降低内存和网络开销,尤其是对象重复率高时。
  • 避免在缓存里放大对象(比如把数据库里的 JSON 字段再放一遍)。若对象大,可拆分为 meta+payload,短 TTL 的 payload 另存。
  • 压缩与压缩门槛:对 >1KB 对象考虑 gzip/snappy,但注意 CPU 开销与编码延迟。

Eviction 策略与容量管理

  • LRU 是常用默认策略,但在频繁访问模式改变时可能抖动。LFU 或 W-TinyLFU 在热点稳定时更高效。
  • 监控 evictions、usedmemory、hitrate,按业务维度设置 maxmemory-policy 或分片限额。
  • 容量预留与动态扩容:生产环境中预留 10-20% 内存避免频繁 OOM 触发全量回收。

指标与监控(必须有的那些)

  • 命中率(hit/miss)、请求延迟分布、后端 DB QPS、缓存回填率、evictions、内存占用。
  • 设置告警阈值:命中率突降、evictions 激增、延迟 SLO 违背。
  • 请求采样与 tracing:把缓存命中路径埋点,能在回归时直接看到热点与瓶颈来源。

灰度与回滚策略

  • Canary 发布:先在 1%-5% 流量验证缓存策略改动(新 key 前缀或开关)。
  • 迭代回退:通过版本控制或 feature flag 立刻回滚到稳定行为。
  • 回流量限控:在回填流量突增时,用速率限制器保护数据库。

实践代码片段(伪代码,思路示例)

  • Cache-aside(伪代码) value = cache.get(key) if value != nil: return value lock = singleflight.acquire(key) // 合并并发回填 if lock.isLeader: value = db.query(key) cache.set(key, value, ttl) singleflight.release(key, value) else: value = singleflight.wait(key) return value

  • 精确失效(消息驱动) on eventupdate(eid): cache.del("event:91:detail:" + eid + ":v" + CURVER) publish("event:invalidate", {key:…, ver:…})

测试与故障排查清单

  • 压测:模拟真实访问模式(热点+长尾),验证命中率与 DB QPS。
  • 故障注入:关掉 Redis 节点,观察降级路径是否平滑。
  • 验证 TTL 抖动与预热效果:是否避免批量过期引发雪崩。
  • 热点回填测试:并发击穿时是否只有一个回填并保护 DB。

落地清单(30 天可以完成的步骤) 1) 梳理现有 key 与对象大小,列出 Top 100 热点 key。 2) 对热点实现版本化 key + 精确失效通道(消息队列)。 3) 加入 singleflight/互斥回填,避免并发回填。 4) 在关键展示处加本地缓存一层并实现失效广播。 5) 切换到高效序列化并压测内存占用。 6) 打通监控、告警与 tracing,设置 SLO。

结语 把缓存问题拆开来做,既有工程意识也有可执行的技术细节。对 91 大事件这种读密集型系统,采取分层缓存、版本化 key、事件驱动失效与防击穿机制,能把延迟和后端压力同时降下来。你可以先从热 key 清单、版本化和 singleflight 三步开始,逐步扩展到全链路监控与灰度发布——整个过程可以逐步推进,不需要一次性改完全部。需要我把其中某一步(比如消息驱动失效或 singleflight 的具体实现)做成详细实现方案和代码示例吗?