跳到主要内容

缓存预热机制

缓存预热(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应用刚开始启动日志初始化
ApplicationContextInitializedEventContext 初始化完成早期配置
ApplicationPreparedEventContext 准备完成注册 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小时

📚 延伸阅读