【2025最新Java面试八股】如何在Spring启动过程中做缓存预热?

在 Spring 应用启动过程中进行缓存预热(Cache Preloading)是一种优化手段,目的是在系统正式对外提供服务前,**提前加载高频访问的数据到缓存(如 Redis、Caffeine 等),避免用户首次请求时因缓存未命中(Cache Miss)导致性能下降。**以下是详细的实现方案和最佳实践:

1. 缓存预热的核心思路

  • 目标:在 Spring 容器初始化完成后,主动加载热点数据到缓存。

  • 适用场景

    • 高频访问的静态数据(如配置表、城市列表)。

    • 计算成本高的数据(如排行榜、聚合统计结果)。

  • 关键时机:确保预热在应用完全启动后执行,且不影响正常服务。


2. 实现方案

方案1:使用 @PostConstruct 注解或者实现 InitializingBean 接口,
实现 InitializingBean 接口,可以重写afterPropertiesSet 方法中执行缓存预热的逻辑。这样,Spring 在初始化 Bean 时会调用 afterPropertiesSet 方法。

在 Spring Bean 初始化完成后立即执行预热逻辑:

复制代码
@Service
public class CacheWarmUpService {

    @Autowired
    private UserService userService; // 依赖的业务服务
    @Autowired
    private CacheManager cacheManager; // Spring 缓存管理器

    @PostConstruct  // 在 Bean 初始化后执行
    public void warmUpCache() {
        List<User> hotUsers = userService.getTopActiveUsers(100); // 加载热点数据
        hotUsers.forEach(user -> 
            cacheManager.getCache("userCache").put(user.getId(), user)
        );
    }
}

import org.springframework.beans.factory.InitializingBean;

import org.springframework.stereotype.Component;

@Component

public class CachePreloader implements InitializingBean {

@Override

public void afterPropertiesSet() throws Exception {

// 执行缓存预热逻辑

// ...

}

}

优点 :简单直接,适合小规模预热。
缺点:可能阻塞 Spring 启动流程,需控制预热时间。


方案2:实现 ApplicationListener 监听上下文就绪事件

ApplicationReadyEvent 是 Spring Boot 框架中的一个事件类,它表示应用程序已经准备好接收请求,即应用程序已启动且上下文已刷新。这个事件是在 ApplicationContext 被初始化和刷新,并且应用程序已经准备好处理请求时触发的。

基于ApplicationReadyEvent,我们可以在应用程序完全启动并处于可用状态后执行一些初始化逻辑。使用 @EventListener 注解或实现 ApplicationListener 接口来监听这个事件。

在 Spring 上下文完全初始化后触发预热:

复制代码
@Component
public class CacheWarmUpListener implements  ApplicationListener<ContextRefreshedEvent> {
//通过实现ApplicationListener<ContextRefreshedEvent>接口
//也可以通过@EventListener实现
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (event.getApplicationContext().getParent() == null) { // 避免子容器重复执行
            // 执行预热逻辑
            warmUpRedisCache();
            warmUpLocalCache();
        }
    }

    private void warmUpRedisCache() {
        // 例如:预加载商品数据到 Redis
        // ...
    }
}

优点 :确保所有 Bean 已就绪,适合复杂预热逻辑。
缺点:需注意避免重复执行(如 Web 应用可能有父子容器)。


方案3: 使用 CommandLineRunner 或**ApplicationRunner**

Spring Boot 提供的启动后扩展点:

复制代码
@Component
@Order(1) // 控制执行顺序
public class CacheWarmUpRunner implements CommandLineRunner {

    @Autowired
    private ProductService productService;

    @Override
    public void run(String... args) {
        List<Product> hotProducts = productService.getHotProducts();
        // 写入缓存(如 Caffeine、Redis)
    }
}

优点 :与 Spring Boot 生命周期无缝集成,支持多任务顺序控制。
缺点:仅适用于 Spring Boot 项目。


方案4:异步预热(推荐)

避免阻塞主线程,使用 @Async 异步执行:

复制代码
@Service
public class AsyncCacheWarmUpService {

    @Async  // 需启用 @EnableAsync
    public void warmUpCacheAsync() {
        // 耗时预热逻辑(如全量加载数据库数据到缓存)
    }
}

// 通过监听器或 Runner 触发异步任务
@Component
public class CacheWarmUpTrigger implements ApplicationListener<ContextRefreshedEvent> {
    @Autowired
    private AsyncCacheWarmUpService asyncService;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        asyncService.warmUpCacheAsync();
    }
}

优点 :不阻塞应用启动,适合大数据量预热。
注意 :需配置线程池(通过 ThreadPoolTaskExecutor)。


3. 结合具体缓存框架

3.1 Redis 预热
复制代码
public void warmUpRedis() {
    StringRedisTemplate redisTemplate = ...;
    List<City> cities = cityRepository.findAll();
    cities.forEach(city -> 
        redisTemplate.opsForValue().set("city:" + city.getId(), city.getName())
    );
}
3.2 Caffeine 本地缓存预热
复制代码
@Bean
public Cache<String, Product> productCache() {
    return Caffeine.newBuilder()
        .maximumSize(1000)
        .build(cache -> {
            // 启动时自动加载数据
            return productService.getProductById(cache.key());
        });
}

4. 最佳实践

  1. 分批次加载:避免单次加载数据量过大导致 OOM 或超时。

    复制代码
    List<User> users = userService.getAllUsers();
    int batchSize = 100;
    for (int i = 0; i < users.size(); i += batchSize) {
        List<User> batch = users.subList(i, Math.min(i + batchSize, users.size()));
        batch.forEach(user -> cache.put(user.getId(), user));
    }
  2. 动态调整预热策略:通过配置文件控制是否启用预热或选择预热数据集。

    复制代码
    cache:
      warm-up:
        enabled: true
        data-sets: top_users,hot_products
  3. 监控与日志:记录预热耗时和数据量,便于优化。

    复制代码
    @Slf4j
    public class CacheWarmUpRunner implements CommandLineRunner {
        @Override
        public void run(String... args) {
            long start = System.currentTimeMillis();
            // 预热逻辑
     log.info("Cache warm-up completed in {} ms", System.currentTimeMillis() - start);
        }
    }

5. 避免的坑

  • 循环依赖:预热 Bean 不要依赖其他未初始化的 Bean。

  • 事务问题确保预热方法内数据库操作已提交(可加 @Transactional)。

  • 分布式环境在集群中仅由一个节点执行预热(通过分布式锁控制)。


总结

方案 适用场景 是否阻塞启动
@PostConstruct 简单、小数据量预热
ApplicationListener 需要完整上下文
CommandLineRunner Spring Boot 项目,需控制顺序
异步预热(推荐) 大数据量或耗时任务

选择合适方案后,结合具体缓存框架(Redis/Caffeine)和业务需求,可显著提升系统启动后的缓存命中率。

相关推荐
重生之后端学习26 分钟前
02-前端Web开发(JS+Vue+Ajax)
java·开发语言·前端·javascript·vue.js
字节源流6 小时前
关于maven的依赖下不下来的问题
java·maven
pjx9877 小时前
服务间的“握手”:OpenFeign声明式调用与客户端负载均衡
java·运维·spring·负载均衡
prinrf('千寻)7 小时前
MyBatis-Plus 的 updateById 方法不更新 null 值属性的问题
java·开发语言·mybatis
老华带你飞8 小时前
实习记录小程序|基于SSM+Vue的实习记录小程序设计与实现(源码+数据库+文档)
java·数据库·spring boot·小程序·论文·毕设·实习记录小程序
在未来等你8 小时前
互联网大厂Java求职面试:AI与大模型应用集成及云原生挑战
java·微服务·ai·kubernetes·大模型·embedding·spring ai
源码技术栈8 小时前
SaaS基于云计算、大数据的Java云HIS平台信息化系统源码
java·大数据·云计算·云his·his系统·云医院·区域his
编程、小哥哥8 小时前
互联网大厂Java面试:从Spring Boot到微服务架构的技术深挖
java·spring boot·redis·微服务·prometheus·面试技巧
揽你·入怀9 小时前
数据结构:ArrayList简单实现与常见操作实例详解
java·开发语言