SpringBoot 启动时实现缓存预热
Spring Boot 启动时预热缓存,核心思路就是在应用启动完成后、对外提供服务之前,把数据库里的热点数据或配置数据主动写入Redis。
主流方案有三种
- CommandLineRunner / ApplicationRunner
- @PostConstruct 注解
- 监听 ApplicationReadyEvent 事件
@PostConstruct 注解
在某个 Bean 初始化完成后执行预热逻辑。
执行时机较早(Bean 初始化阶段),此时可能其他依赖还未完全准备好(例如数据库连接池、Redis 连接等),不适合依赖外部资源的预热。
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Component public class CachePreloader {
@Autowired private SomeService someService;
@PostConstruct public void preload() { List<Data> list = someService.loadHotData(); list.forEach(data -> someService.cacheData(data)); } }
|
CommandLineRunner / ApplicationRunner
执行时机晚于 @PostConstruct,此时所有 Bean 已初始化完成,服务未启动完成,可以安全使用外部资源。
1 2 3 4 5 6 7 8 9 10 11 12
| @Component public class CachePreloader implements CommandLineRunner {
@Autowired private SomeService someService;
@Override public void run(String... args) throws Exception { someService.preloadCache(); } }
|
二者区别
| 特性 |
CommandLineRunner |
ApplicationRunner |
| 参数类型 |
String... args |
ApplicationArguments args |
| 参数访问方式 |
直接接收原始的应用启动参数数组 |
通过封装对象获取解析后的参数 |
| 是否支持选项解析 |
需要手动解析(如遍历判断 --key=value) |
内置支持解析选项参数(如 --foo=bar)和非选项参数 |
| 代码简洁性 |
适合只需简单参数列表的场景 |
适合需要区分选项参数和普通参数的场景 |
参数解析示例
假设通过命令行启动 Spring Boot 应用:
1
| java -jar myapp.jar --server.port=8080 arg1 arg2 --debug
|
使用 CommandLineRunner
1 2 3 4 5 6 7 8 9 10
| @Component public class MyCommandLineRunner implements CommandLineRunner { @Override public void run(String... args) { for (String arg : args) { System.out.println(arg); } } }
|
你需要自己解析哪些是选项参数(--xxx),哪些是普通参数。
使用 ApplicationRunner
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Component public class MyApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) { System.out.println("Option names: " + args.getOptionNames()); System.out.println("server.port = " + args.getOptionValues("server.port")); System.out.println("Non-option args: " + args.getNonOptionArgs()); System.out.println("Raw args: " + Arrays.toString(args.getSourceArgs())); } }
|
输出:
1 2 3 4
| Option names: [debug, server.port] server.port = [8080] Non-option args: [arg1, arg2] Raw args: [--server.port=8080, arg1, arg2, --debug]
|
可以看到,ApplicationArguments 已经帮你把参数分好类,直接调用方法即可,无需手动解析。
监听 ApplicationReadyEvent 事件
这是最稳妥的方案。ApplicationReadyEvent 在应用完全就绪、可以接收请求时才触发,比 CommandLineRunner 还要晚一点。如果你的缓存预热逻辑特别重要,不能有任何闪失,用这个最保险。
预热期间已有用户请求进入,可能短暂命中空白缓存,但通常可通过降级或重试缓解。
1 2 3 4 5 6 7 8 9 10 11 12
| @Component public class CachePreloader {
@Autowired private SomeService someService;
@EventListener(ApplicationReadyEvent.class) public void preload() { someService.preloadCache(); } }
|
三种方案的执行时机对比
| 方案 |
执行时机 |
适用场景 |
| @PostConstruct |
Bean 初始化完成后立即执行 |
单个服务自己负责的局部缓存 |
| CommandLineRunner/ ApplicationRunner |
所有 Bean 初始化完成后 |
全局缓存预热,需要多个 Bean 协作 |
| ApplicationReadyEvent |
应用完全就绪,可接收请求时 |
最稳妥,适合核心缓存 |
如果你有多个 Runner 或者 Listener,可以通过 @Order 注解来控制执行顺序,数字越小越先执行。
大数据量预热的优化策略
如果要预热的数据量很大,比如几十万条商品信息,一次性全捞出来再逐条写 Redis,启动时间会被拖得很长,内存也可能撑不住。
1)分批加载:用分页查询,每次查 1000 条,处理完再查下一批。避免一次性把数据库和内存都撑爆。
1 2 3 4 5 6 7 8 9
| int page = 0; int size = 1000; Page<Product> products; do { products = productRepository.findAll(PageRequest.of(page++, size)); for (Product p : products) { redisTemplate.opsForValue().set("product:" + p.getId(), JSON.toJSONString(p)); } } while (products.hasNext());
|
2)Pipeline 批量写入:Redis 的 pipeline 可以把多个命令打包一次性发过去,减少网络往返。1000 条数据用 pipeline 写,比单条写快 10 倍不止。
1 2 3 4 5 6 7 8 9
| redisTemplate.executePipelined((RedisCallback<Object>) connection -> { for (Product p : products) { connection.stringCommands().set( ("product:" + p.getId()).getBytes(), JSON.toJSONString(p).getBytes() ); } return null; });
|
3)异步预热:如果缓存预热不是强依赖,可以用 @Async 把预热逻辑丢到线程池里跑,不阻塞主启动流程。但要注意,这样应用启动后的前几秒缓存可能还没预热完,需要业务上能容忍短暂的缓存 miss。

@Cacheable 的懒加载方式
除了主动预热,还有一种懒加载思路:用 Spring Cache 的 @Cacheable 注解,第一次请求时自动把结果写入缓存。
1 2 3 4
| @Cacheable(value = "products", key = "#id") public Product getProductById(Long id) { return productRepository.findById(id).orElse(null); }
|
但这种方式有个问题:第一个请求还是会打到数据库。如果你的场景是高并发秒杀,第一波流量涌进来时缓存都是空的,效果就很差。所以 @Cacheable 更适合访问频率不高、对首次延迟不敏感的场景。
序列化踩坑提醒
往 Redis 写对象时,序列化方式一定要配对。默认的 JdkSerializationRedisSerializer 会往 key 和 value 里塞一堆乱码前缀,用 redis-cli 看起来很痛苦,而且跨语言读不了。
推荐配置 StringRedisSerializer 处理 key,Jackson2JsonRedisSerializer 或 GenericJackson2JsonRedisSerializer 处理 value:
1 2 3 4 5 6 7 8
| @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; }
|