缓存预热机制
缓存预热(Cache Warmup) 是指在应用启动时,提前将热点数据加载到缓存中,避免冷启动时大量请求直接打到数据库。
🎯 为什么需要缓存预热?
问题场景
应用重启后:
用户请求 → 缓存未命中 → 查询数据库 → 写入缓存
用户请求 → 缓存未命中 → 查询数据库 → 写入缓存
用户请求 → 缓存未命中 → 查询数据库 → 写入缓存
...
结果:数据库瞬间承受大量请求(缓存击穿)
解决方案
应用启动时:
预热程序 → 批量查询热点数据 → 写入缓存
用户请求 → 缓存命中 ✅ → 直接返回
用户请求 → 缓存命中 ✅ → 直接返回
结果:数据库压力平稳
🏗️ 项目实现
CacheWarmup 组件
位置: blog-application/src/main/java/com/blog/config/CacheWarmup.java
@Slf4j
@Component
@RequiredArgsConstructor
public class CacheWarmup {
private final RoleMapper roleMapper;
private final RedisTemplate<String, Object> redisTemplate;
private final CacheManager cacheManager;
/**
* 应用启动完成后执行缓存预热
* 监听 ApplicationReadyEvent 事件,确保所有 Bean 已初始化完成
*/
@EventListener(ApplicationReadyEvent.class)
public void warmupCache() {
long startTime = System.currentTimeMillis();
log.info("🔥 开始缓存预热...");
try {
// 1. 预加载所有启用的角色
warmupRoles();
// 2. 可扩展:预加载其他热点数据
// warmupUsers();
// warmupArticles();
long duration = System.currentTimeMillis() - startTime;
log.info("✅ 缓存预热完成,耗时: {}ms", duration);
} catch (Exception e) {
log.error("❌ 缓存预热失败: {}", e.getMessage(), e);
// 不抛出异常,避免影响应用启动
}
}
/**
* 预热角色数据
*/
private void warmupRoles() {
List<SysRole> roles = roleMapper.selectAllActive();
if (CollectionUtils.isEmpty(roles)) {
log.warn("⚠️ 没有找到启用的角色数据,跳过角色缓存预热");
return;
}
for (SysRole role : roles) {
String key = "role:detail:" + role.getId();
redisTemplate.opsForValue().set(key, role, 1, TimeUnit.HOURS);
}
log.info("✅ 角色缓存预热完成: 预加载 {} 个角色", roles.size());
}
}
🔧 核心机制
1. ApplicationReadyEvent 事件
@EventListener(ApplicationReadyEvent.class)
public void warmupCache() { }
| 事件 | 触发时机 | 适用场景 |
|---|---|---|
ApplicationStartingEvent | 应用刚开始启动 | 日志初始化 |
ApplicationContextInitializedEvent | Context 初始化完成 | 早期配置 |
ApplicationPreparedEvent | Context 准备完成 | 注册 Bean |
ApplicationReadyEvent | 所有 Bean 已就绪 | 缓存预热 ✅ |
ApplicationFailedEvent | 启动失败 | 异常处理 |
为什么选择 ApplicationReadyEvent?
- ✅ 所有 Bean 已初始化完成
- ✅ 数据库连接池已就绪
- ✅ Redis 连接已建立
- ✅ 不会阻塞应用启动
2. 异步执行(可选优化)
如果预热数据量较大,可以改为异步执行:
@Async
@EventListener(ApplicationReadyEvent.class)
public void warmupCacheAsync() {
// 异步执行,不阻塞应用启动
warmupCache();
}
需要在启动类添加 @EnableAsync 注解。
3. 容错处理
try {
warmupRoles();
} catch (Exception e) {
log.error("❌ 缓存预热失败: {}", e.getMessage(), e);
// 不抛出异常,避免影响应用启动
}
设计原则:预热失败不应该阻止应用启动,缓存可以在运行时按需加载。
📋 预热策略
预热什么数据?
| 数据类型 | 优先级 | 原因 |
|---|---|---|
| 角色列表 | ⭐⭐⭐⭐⭐ | 每次认证都会查询 |
| 系统配置 | ⭐⭐⭐⭐⭐ | 全局共享,访问频繁 |
| 热门文章 | ⭐⭐⭐⭐ | 首页展示 |
| 用户信息 | ⭐⭐⭐ | 按需加载即可 |
预热多少数据?
// ✅ 推荐:只预热最热数据
List<SysRole> roles = roleMapper.selectAllActive(); // 通常 < 10 条
// ❌ 不推荐:预热所有数据
List<User> users = userMapper.selectAll(); // 可能数万条
🔄 扩展示例
预热用户热点数据
private void warmupHotUsers() {
// 只预热活跃用户(最近登录的 TOP 100)
List<Long> hotUserIds = userMapper.selectRecentActiveUserIds(100);
if (CollectionUtils.isEmpty(hotUserIds)) {
return;
}
List<SysUser> users = userMapper.selectBatchIds(hotUserIds);
for (SysUser user : users) {
String key = CacheKeys.userDetailKey(user.getId());
redisUtils.set(key, user, 30, TimeUnit.MINUTES);
}
log.info("✅ 热门用户缓存预热完成: {} 个", users.size());
}
预热文章列表
private void warmupArticleList() {
// 预热首页文章列表(第1页)
Page<Article> page = articleMapper.selectPage(
new Page<>(1, 10),
new QueryWrapper<Article>().eq("status", 1)
);
String key = "article:list:page:1";
redisUtils.set(key, page.getRecords(), 10, TimeUnit.MINUTES);
log.info("✅ 文章列表缓存预热完成");
}
📊 监控与管理
手动触发预热
通过 CacheManagementController 暴露的接口:
curl -X POST http://localhost:8080/actuator/cache/warmup \
-H "Authorization: Bearer {token}"
查看预热日志
2025-12-12 08:00:01 INFO 🔥 开始缓存预热...
2025-12-12 08:00:01 INFO ✅ 角色缓存预热完成: 预加载 3 个角色
2025-12-12 08:00:01 INFO ✅ 缓存预热完成,耗时: 156ms
⚠️ 注意事项
1. 预热顺序
有依赖关系的数据要按正确顺序预热:
// ✅ 正确顺序
warmupRoles(); // 先预热角色
warmupUsers(); // 再预热用户(用户可能需要角色信息)
2. 避免预热过多
// ❌ 不要这样做
for (int i = 1; i <= 1000; i++) {
Article article = articleMapper.selectById(i);
redisUtils.set("article:" + i, article, 1, TimeUnit.HOURS);
}
// 问题:启动时间过长,Redis 内存占用过大
3. 设置合理的 TTL
// 预热的缓存 TTL 可以比普通缓存稍长
redisTemplate.opsForValue().set(key, role, 1, TimeUnit.HOURS); // 1小时