写点什么

面试官:SpringBoot 工程启动以后,希望将数据库中已有的固定内容提前加载到 Redis 缓存中,应该如何处理

  • 2025-04-21
    福建
  • 本文字数:4246 字

    阅读完需:约 14 分钟

这个问题说白了就是希望通过预加载数据,达到提升系统性能和响应速度的效果。像目前在很多场景中都有使用:


  • 电商平台的商品分类信息、用户基础资料:避免高并发时数据库被重复查询,降低响应延迟。

  • 系统参数配置(如地区编码、权限规则)、国际化资源:减少对配置中心或数据库的依赖,提升配置读取速度。

  • 促销活动的商品库存信息、新闻头条内容:通过预加载防止缓存击穿,应对突发流量。


题目说的是提前加载的 redis 缓存中,像配置类信息等这种变更频率低、实时性要求低的数据,还会加载到本地缓存中(如 GuavaCache,Caffeine 等),进一步减轻 redis 的压力,提升访问速度


重点


重点其实就是利用 Spring 或 SpringBoot 的扩展点来完成这部分功能


初始化数据加载触发机制


1、使用 CommandLineRunner 或 ApplicationRunner 在应用启动时自动执行数据加载逻辑。这是最常见的实现方式。


@Componentpublic class CacheWarmupRunner implements ApplicationRunner {    @Override    public void run(ApplicationArguments args) {        // 分页加载数据到缓存        PageHelper.startPage(1, 1000);        List<Product> products = productMapper.selectAll();        products.forEach(p -> redisTemplate.opsForHash().put("products", p.getId(), p));    }}
复制代码


2、使用 @PostConstruct 注解


在服务类中通过 @Postconstruct 注解标记等初始化方法,在 Bean 创建后立即执行数据加载


@Servicepublic class CachePreloader {    @Autowired    private UserService userService;    @Autowired    private RedisTemplate<String, Object> redisTemplate;
@PostConstruct public void init() { List<User> users = userService.getAllFixedData(); // 从数据库获取数据 users.forEach(user -> redisTemplate.opsForValue().set("user:" + user.getId(), user) ); }}
复制代码


结合缓存注解主动触发


使用 @Cacheable 注解:在首次调用方法时触发缓存写入,强制触发缓存写入。但需手动触发首次调用才能完成预加载


@Servicepublic class UserService {    @Cacheable(value = "users", key = "#root.methodName")    public List<User> getAllFixedData() {        return userRepository.findAll(); // 首次调用会写入缓存    }}
复制代码


注意


  • 推荐方案:使用 CommandlineRunner 或 @PostConstruct 在启动时主动加载数据到 Redis,确保缓存立即可用。

  • 注解补充:@Cacheable 适用于懒加载场景,但需结合首次调用触发。

  • 注意事项:确保实体类实现 Serializable 接口,并正确配置 RedisTemplate 的序列化方式.


扩展知识


关于 Spring 和 SpringBoot 的扩展点我已经写过一篇文章详细介绍,可以点击查看了解



CommandLineRunner 和 ApplicationRunner


org.springframework.boot.CommandLineRunner


介绍


这两个是 Springboot 中新增的扩展点,之所以将这两个扩展点放在一起,是因为它两个功能特性高度相似,不同的只是名字、扩展方法形参数类型、执行先后的一些小的不同。


这两个接口触发时机为整个项目启动完毕后,自动执行。如果有多个CommandLineRunner,可以利用@Order来进行排序。


注意:


  • CommandLineRunner 和 ApplicationRunner 都有一个扩展方法 run(),但是 run()形参数类型不同;

  • CommandLineRunner.run()方法的形参数类型是 String... args,ApplicationRunner.run()的形参数类型是 ApplicationArguments args;

  • CommandLineRunner.run()的执行时机要晚于 ApplicationRunner.run()一点;

  • CommandLineRunner 和 ApplicationRunner 触发执行时机是在 Spring 容器、Tomcat 容器正式启动完成后,可以正式处理业务请求前,即项目启动的最后一步;

  • CommandLineRunner 和 ApplicationRunner 可以应用的场景:项目启动前,热点数据的预加载、清除临时文件、读取自定义配置信息等;


使用场景


1、初始化数据:使用 CommandLineRunner 可以在应用启动后初始化一些必要的数据,例如从数据库加载某些配置或插入初始数据。


@Componentpublic class DataInitializer implements CommandLineRunner {
@Override public void run(String... args) { System.out.println("初始化数据:插入初始数据"); // 模拟插入初始数据 insertInitialData(); }
private void insertInitialData() { System.out.println("插入数据:用户表初始数据"); }}
复制代码


2、启动后执行任务:使用 CommandLineRunner 可以在应用启动后执行一些特定的任务,比如发送一个通知或启动一些背景任务。


@Componentpublic class TaskExecutor implements CommandLineRunner {
@Override public void run(String... args) { System.out.println("启动后执行任务:发送启动通知"); // 模拟发送启动通知 sendStartupNotification(); }
private void sendStartupNotification() { System.out.println("通知:应用已启动"); }}
复制代码


3、读取命令行参数:使用 CommandLineRunner 可以获取并处理命令行参数,这对于需要根据启动参数动态配置应用的场景非常有用。


@Componentpublic class CommandLineArgsProcessor implements CommandLineRunner {
@Override public void run(String... args) { System.out.println("处理命令行参数:"); for (String arg : args) { System.out.println("参数:" + arg); } }}
@SpringBootApplicationpublic class AppConfig { public static void main(String[] args) { SpringApplication.run(AppConfig.class, new String[]{"参数1", "参数2", "参数3"}); }}
复制代码


@PostConstruct


javax.annotation.PostConstruct


介绍


可以看出来其本身不是 Spring 定义的注解,但是 Spring 提供了具体的实现。这个并不算一个扩展点,其实就是一个标注。其作用是在 bean 的初始化阶段,如果对一个方法标注了@PostConstruct,会先调用这个方法。这里重点是要关注下这个标准的触发点,这个触发点是在postProcessBeforeInitialization之后,InitializingBean.afterPropertiesSet之前。


注意:


  • 使用 @PostConstruct 注解标记的方法不能有参数,除非是拦截器,可以采用拦截器规范定义的 InvocationContext 对象。

  • 使用 @PostConstruct 注解标记的方法不能有返回值,实际上如果有返回值,也不会报错,但是会忽略掉;

  • 使用 @PostConstruct 注解标记的方法不能被 static 修饰,但是 final 是可以的;


使用场景


使用场景与 InitializingBean 类似,具体看下文


InitializingBean


org.springframework.beans.factory.InitializingBean


介绍


这个类,顾名思义,也是用来初始化 bean 的。InitializingBean接口为 bean 提供了初始化方法的方式,它只在 bean 实例化、属性注入后的提供了一个扩展点afterPropertiesSet方法,凡是继承该接口的类,在初始前、属性赋值后,都会执行该方法。这个扩展点的触发时机postProcessAfterInitialization之前。



注意:

  • 与 InitializingBean#afterPropertiesSet()类似效果的是init-method,但是需要注意的是 InitializingBean#afterPropertiesSet()执行时机要略早于 init-method;

  • InitializingBean#afterPropertiesSet()的调用方式是在 bean 初始化过程中真接调用 bean 的 afterPropertiesSet();

  • bean 自定义属性 init-method 是通过 java 反射的方式进行调用 ;


使用场景


1、初始化资源:可以在 Bean 初始化后自动启动一些资源,如数据库连接、文件读取等。


import org.springframework.beans.factory.InitializingBean;import org.springframework.stereotype.Component;
@Componentpublic class ResourceInitializer implements InitializingBean {
@Override public void afterPropertiesSet() { // 模拟资源初始化 System.out.println("资源初始化:建立数据库连接"); }
public void performAction() { System.out.println("资源使用:执行数据库操作"); }} @Configuration@ComponentScan(basePackages = "com.seven")public class AppConfig { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); ResourceInitializer initializer = context.getBean(ResourceInitializer.class); initializer.performAction(); }}
复制代码


2、设置初始值


@Componentpublic class InitialValueSetter implements InitializingBean {
private String initialValue;
@Override public void afterPropertiesSet() { initialValue = "默认值"; System.out.println("设置初始值:" + initialValue); }
public void printValue() { System.out.println("当前值:" + initialValue); }}
@Configuration@ComponentScan(basePackages = "com.seven")public class AppConfig { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); InitialValueSetter valueSetter = context.getBean(InitialValueSetter.class); valueSetter.printValue(); }}
复制代码


3、加载配置:可以在 Bean 初始化后加载必要的配置,如从文件或数据库中读取配置。


@Componentpublic class ConfigLoader implements InitializingBean {
private String configValue;
@Override public void afterPropertiesSet() { // 模拟配置加载 configValue = "配置值"; System.out.println("加载配置:" + configValue); }
public void printConfig() { System.out.println("当前配置:" + configValue); }}
@Configuration@ComponentScan(basePackages = "com.seven")public class AppConfig { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); ConfigLoader configLoader = context.getBean(ConfigLoader.class); configLoader.printConfig(); }}
复制代码


文章转载自:seven97_top

原文链接:https://www.cnblogs.com/seven97-top/p/18823241

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2025-04-01 加入

还未添加个人简介

评论

发布
暂无评论
面试官:SpringBoot 工程启动以后,希望将数据库中已有的固定内容提前加载到 Redis 缓存中,应该如何处理_数据库_电子尖叫食人鱼_InfoQ写作社区