冷启动是一道算术题
设想缓存命中率 95% 的详情接口:一万 QPS 里只有五百个会打到 DB,数据库气定神闲。现在发布新版本,缓存里是空的——一万 QPS 原封不动全量砸向 DB,二十倍的流量冲击,平时够用的容量瞬间不够。Redis 重启后、大促开闸前、新缓存架构切换时,都是同一道算术题。冷启动的缓存等于没有缓存,而穿透、击穿、雪崩三篇讲的所有防线,都要在这个空窗期里独木支撑。
预热的本质,是把这道算术题提前算掉:上线之前,把该有的数据先搬进缓存,让第一波真实流量进来时命中的是热缓存而不是空仓库。
预热什么:抓热点,别全量
第一反应是「把库里数据全灌进去」,劝住——全量预热既慢又浪费,绝大多数数据根本等不到被访问,就先在缓存里占着内存排队过气。预热的对象永远是热点数据:销量榜 Top N 的商品、首页与推荐位要展示的内容、核心配置与白名单、上一周期统计出的高频访问集合。热点数据的来源现成的:埋点统计、访问日志聚合、前一个版本缓存的访问频率,都指着同一批 key。
怎么预热:限速回源,打散 TTL
预热脚本的写法不难,难的是两个纪律。第一是控制回源速率:预热本身也是一次对 DB 的批量扫描,十万条热点数据一口气并发查库,预热脚本就成了压测工具——批量分页拉取、每批之间留间隔、QPS 压在 DB 闲时的安全水位以下(限流系列第 6 篇的 Redis+Lua 计数器可以直接拿来给预热脚本限速):
List<Long> hotIds = statService.topN("item_view", 50_000);
for (List<Long> batch : Lists.partition(hotIds, 200)) {
Map<Long, Item> items = itemDao.loadBatch(batch); // 批量查,别一条条来
items.forEach((id, it) ->
redis.setex(keyOf(id), ttlWithJitter(it), toJson(it)));
Thread.sleep(100); // 限速:每批之间歇口气
}第二是预热也要打散 TTL——雪崩篇的教训在这里原样生效:统一设 24 小时,明天凌晨同一秒集体失效,预热脚本亲手埋下下一场雪崩。批量加载用 MGET、pipeline、批量查询 走批,别让预热脚本自己变成击穿源。
什么时机:三个触发点
预热挂在三个时机上。发布前:新版本灰度前跑一遍预热,灰度实例从一开始就命中热缓存;大促前:活动数据、爆款商品在开闸前 T-1 就位,配合压测验证命中率;定时兜底:每天凌晨低峰跑一轮热点刷新,把过气 key 换成新的热点。三个时机共用同一套预热脚本,参数不同而已。
预热的反例也值得记:只预热了冷门数据浪费内存;预热脚本没限速把 DB 打挂在大促前一晚;预热完不验证命中率就上线,空窗期照样发生。预热不是仪式,是带验收的工程动作——上线前看一眼命中率报表,比十篇方案文档都踏实。
到这里,「缓存空」的问题讲完了。但缓存还有一类更隐蔽的失衡:流量不是平均落在所有 key 上的——个别 key 扛下海量 QPS,把一个 Redis 分片打满。热的极限形态,下一篇讲热 Key。
评论 (0)