一、前言:为什么需要 Redis 缓存预热?
想象这样一个场景:
- 系统刚上线或重启
- 所有缓存为空(冷启动)
- 大量用户同时访问商品详情、用户信息等接口
- 请求全部穿透到数据库 → DB 瞬间被打垮
这就是典型的"缓存雪崩"前置问题------缓存未就绪!
缓存预热(Cache Warm-up) 就是在系统启动前或低峰期,主动将热点数据加载到 Redis 中,避免冷启动带来的性能灾难。
本文将从策略设计、实现方案、避坑指南三个维度,带你掌握 Redis 缓存预热的完整实践。
二、什么数据值得预热?
不是所有数据都需要预热!聚焦高访问频次 + 低变更频率的数据:
| 数据类型 | 是否适合预热 | 示例 |
|---|---|---|
| ✅ 热点商品信息 | 是 | 双11主会场商品 |
| ✅ 用户基础资料 | 是 | VIP 用户 profile |
| ✅ 配置字典表 | 是 | 国家列表、状态码映射 |
| ❌ 用户订单列表 | 否 | 每人不同,且实时变化 |
| ❌ 实时库存 | 否 | 高频更新,预热即过期 |
💡 经验法则:
- 访问量 Top 1000 的 key
- 更新频率 < 1 次/小时
- 数据量可控(避免 OOM)
三、四大预热策略对比
| 策略 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 启动时预热 | 应用启动时加载 | 简单直接 | 阻塞启动、单点压力大 | 小型系统 |
| 定时任务预热 | 定时从 DB 同步 | 不阻塞主流程 | 有延迟 | 中低频更新数据 |
| 双写+渐进预热 | 写 DB 时同步写缓存 | 实时性强 | 逻辑复杂 | 核心热点数据 |
| 离线计算+批量导入 | 大数据平台生成快照 | 海量数据支持 | 架构复杂 | 超大型系统 |
✅ 推荐组合 :启动预热(核心) + 定时任务(兜底)
四、实战 1:Spring Boot 启动时预热(最常用)
场景
- 系统启动时加载 1000 个热门商品到 Redis
代码实现
java
@Component
public class CacheWarmUpRunner implements CommandLineRunner {
@Autowired
private ProductMapper productMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void run(String... args) {
log.info("开始缓存预热...");
// 1. 查询热点商品(按访问量排序)
List<Product> hotProducts = productMapper.selectHotProducts(1000);
// 2. 批量写入 Redis(使用 Pipeline 提升性能)
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
hotProducts.forEach(product -> {
String key = "product:" + product.getId();
String value = JSON.toJSONString(product);
connection.set(key.getBytes(), value.getBytes());
connection.expire(key.getBytes(), Duration.ofHours(2).getSeconds());
});
return null;
});
log.info("缓存预热完成,共加载 {} 个商品", hotProducts.size());
}
}
✅ 关键优化:
- 使用
executePipelined减少网络往返- 设置合理 TTL(如 2 小时),避免脏数据长期滞留
五、实战 2:OpenResty + Lua 预热(网关层兜底)
即使后端做了预热,仍可能因扩容、故障导致部分实例缓存缺失。
在 OpenResty 层实现"懒加载 + 自动回源预热":
Lua
location /api/product {
content_by_lua_block {
local id = ngx.var.arg_id
local key = "product:" .. id
-- 1. 先查 Redis
local redis = require "resty.redis"
local red = redis:new()
red:connect("127.0.0.1", 6379)
local cache = red:get(key)
if cache and cache ~= ngx.null then
ngx.say(cache)
return
end
-- 2. 缓存未命中:回源查询 DB(通过内部 API)
local res = ngx.location.capture("/internal/product?id=" .. id)
if res.status == 200 then
-- 3. 写入 Redis(设置较短 TTL,如 60s)
red:set(key, res.body)
red:expire(key, 60)
ngx.print(res.body)
else
ngx.status = res.status
ngx.print(res.body)
end
red:close()
}
}
🔁 效果:
- 首次请求稍慢,但后续请求极速响应
- 自动重建缓存,无需人工干预
六、高级技巧:分片预热 + 限流保护
问题
- 一次性加载 100 万条数据 → Redis CPU 打满
- 网络带宽耗尽
解决方案:分批次 + 限速
java
// 分页预热
int pageSize = 1000;
int total = productMapper.countHotProducts();
int pages = (total + pageSize - 1) / pageSize;
for (int i = 0; i < pages; i++) {
List<Product> batch = productMapper.selectHotProductsPage(i, pageSize);
// 写入 Redis...
// 限速:每批间隔 100ms
Thread.sleep(100);
}
📊 建议指标:
- 单次批量 ≤ 5000 条
- QPS 控制在 Redis 承载能力的 50% 以内
七、避坑指南:常见错误与解决方案
| 坑点 | 后果 | 解决方案 |
|---|---|---|
| 未设 TTL | 脏数据永久滞留 | 所有预热 key 必须设置 TTL |
| 全量预热 | Redis OOM | 只预热 Top N 热点数据 |
| 同步阻塞启动 | 应用启动超时 | 改为异步线程 + 健康检查延迟就绪 |
| 忽略数据一致性 | 缓存与 DB 不一致 | 预热后监听 binlog 增量更新 |
💡 健康检查示例(K8s):
readinessProbe: exec: command: ["sh", "-c", "redis-cli EXISTS product:1"] initialDelaySeconds: 30 # 等待预热完成
八、监控与验证
如何确认预热成功?
-
Redis 监控 :
bashredis-cli INFO memory # 查看 used_memory redis-cli DBSIZE # 查看 key 数量 -
应用日志:记录预热数量与耗时
-
压测对比 :
- 冷启动 QPS:500
- 预热后 QPS:15,000+
九、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!