Spring Boot 启动时将数据库数据预加载到 Redis 缓存

Spring Boot 启动时将数据库数据预加载到 Redis 缓存

在实际项目开发中,我们经常需要在应用启动时将一些固定的、频繁访问的数据从数据库预加载到 Redis 缓存中,以提高系统性能。本文将介绍几种实现方案。

方案一:使用 @PostConstruct 注解

这是最简单直接的方式,在 Spring Bean 初始化完成后执行预加载逻辑。

java 复制代码
@Component
@Slf4j
public class CachePreloader {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @PostConstruct
    public void preloadCache() {
        log.info("开始预加载缓存数据...");
        
        try {
            // 加载用户基础信息
            List<User> users = userService.getAllActiveUsers();
            for (User user : users) {
                String key = "user:" + user.getId();
                redisTemplate.opsForValue().set(key, user, Duration.ofHours(24));
            }
            
            // 加载系统配置
            List<SystemConfig> configs = systemConfigService.getAllConfigs();
            for (SystemConfig config : configs) {
                String key = "config:" + config.getKey();
                redisTemplate.opsForValue().set(key, config.getValue(), Duration.ofDays(7));
            }
            
            log.info("缓存预加载完成,共加载 {} 条用户数据,{} 条配置数据", 
                    users.size(), configs.size());
                    
        } catch (Exception e) {
            log.error("缓存预加载失败", e);
        }
    }
}

方案二:实现 ApplicationRunner 接口

ApplicationRunner 在应用启动完成后执行,可以获取命令行参数,更适合复杂的初始化逻辑。

java 复制代码
@Component
@Order(1) // 设置执行顺序
@Slf4j
public class CacheApplicationRunner implements ApplicationRunner {
    
    @Autowired
    private DataPreloadService dataPreloadService;
    
    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("ApplicationRunner 开始执行缓存预加载...");
        
        // 检查是否需要预加载
        if (args.containsOption("skip-cache-preload")) {
            log.info("跳过缓存预加载");
            return;
        }
        
        dataPreloadService.preloadAllCache();
        log.info("ApplicationRunner 缓存预加载完成");
    }
}

方案三:监听 ApplicationReadyEvent 事件

这种方式在应用完全启动就绪后执行,是最安全的预加载时机。

java 复制代码
@Component
@Slf4j
public class CacheEventListener {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private ProductService productService;
    
    @EventListener
    public void handleApplicationReady(ApplicationReadyEvent event) {
        log.info("应用启动完成,开始预加载缓存...");
        
        // 异步执行预加载,避免阻塞启动
        CompletableFuture.runAsync(() -> {
            try {
                preloadProductCache();
                preloadCategoryCache();
            } catch (Exception e) {
                log.error("异步预加载缓存失败", e);
            }
        });
    }
    
    private void preloadProductCache() {
        List<Product> hotProducts = productService.getHotProducts();
        hotProducts.forEach(product -> {
            String key = "hot_product:" + product.getId();
            redisTemplate.opsForValue().set(key, product, Duration.ofHours(12));
        });
        log.info("热门商品缓存预加载完成,共 {} 条", hotProducts.size());
    }
    
    private void preloadCategoryCache() {
        List<Category> categories = productService.getAllCategories();
        redisTemplate.opsForValue().set("all_categories", categories, Duration.ofDays(1));
        log.info("商品分类缓存预加载完成");
    }
}

方案四:创建专门的预加载服务

将预加载逻辑封装成独立的服务,便于管理和测试。

java 复制代码
@Service
@Slf4j
public class DataPreloadService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private DictService dictService;
    
    @Autowired
    private RegionService regionService;
    
    /**
     * 预加载所有缓存数据
     */
    public void preloadAllCache() {
        long startTime = System.currentTimeMillis();
        
        try {
            // 并行加载多种数据
            CompletableFuture<Void> dictFuture = CompletableFuture.runAsync(this::preloadDictCache);
            CompletableFuture<Void> regionFuture = CompletableFuture.runAsync(this::preloadRegionCache);
            
            // 等待所有任务完成
            CompletableFuture.allOf(dictFuture, regionFuture).join();
            
            long endTime = System.currentTimeMillis();
            log.info("所有缓存预加载完成,耗时: {} ms", endTime - startTime);
            
        } catch (Exception e) {
            log.error("缓存预加载过程中发生异常", e);
        }
    }
    
    /**
     * 预加载数据字典
     */
    private void preloadDictCache() {
        try {
            Map<String, List<DictItem>> dictMap = dictService.getAllDictItems();
            dictMap.forEach((dictType, items) -> {
                String key = "dict:" + dictType;
                redisTemplate.opsForValue().set(key, items, Duration.ofDays(30));
            });
            log.info("数据字典缓存预加载完成,共 {} 种类型", dictMap.size());
        } catch (Exception e) {
            log.error("数据字典缓存预加载失败", e);
        }
    }
    
    /**
     * 预加载地区数据
     */
    private void preloadRegionCache() {
        try {
            List<Region> regions = regionService.getAllRegions();
            redisTemplate.opsForValue().set("all_regions", regions, Duration.ofDays(30));
            
            // 按层级缓存
            Map<Integer, List<Region>> regionsByLevel = regions.stream()
                .collect(Collectors.groupingBy(Region::getLevel));
            
            regionsByLevel.forEach((level, levelRegions) -> {
                String key = "regions_level:" + level;
                redisTemplate.opsForValue().set(key, levelRegions, Duration.ofDays(30));
            });
            
            log.info("地区数据缓存预加载完成,共 {} 条记录", regions.size());
        } catch (Exception e) {
            log.error("地区数据缓存预加载失败", e);
        }
    }
}

配置文件控制

application.yml 中添加配置项,控制预加载行为:

yaml 复制代码
app:
  cache:
    preload:
      enabled: true
      async: true
      timeout: 30000  # 超时时间(毫秒)
      batch-size: 1000  # 批处理大小

对应的配置类:

java 复制代码
@ConfigurationProperties(prefix = "app.cache.preload")
@Data
public class CachePreloadProperties {
    private boolean enabled = true;
    private boolean async = true;
    private long timeout = 30000;
    private int batchSize = 1000;
}

最佳实践建议

1. 异常处理

预加载过程中的异常不应该影响应用启动:

java 复制代码
@PostConstruct
public void preloadCache() {
    try {
        // 预加载逻辑
    } catch (Exception e) {
        log.error("缓存预加载失败,但不影响应用启动", e);
        // 可以发送告警通知
    }
}

2. 分批处理

对于大量数据,应该分批处理避免内存溢出:

java 复制代码
private void preloadLargeDataset() {
    int pageSize = cachePreloadProperties.getBatchSize();
    int pageNum = 0;
    
    while (true) {
        List<DataEntity> dataList = dataService.getDataByPage(pageNum, pageSize);
        if (dataList.isEmpty()) {
            break;
        }
        
        // 批量写入 Redis
        dataList.forEach(data -> {
            String key = "data:" + data.getId();
            redisTemplate.opsForValue().set(key, data, Duration.ofHours(6));
        });
        
        pageNum++;
        log.info("已预加载第 {} 批数据,本批 {} 条", pageNum, dataList.size());
    }
}

3. 监控和告警

添加预加载状态监控:

java 复制代码
@Component
public class CachePreloadMonitor {
    
    private final MeterRegistry meterRegistry;
    private final Counter preloadSuccessCounter;
    private final Counter preloadFailureCounter;
    
    public CachePreloadMonitor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.preloadSuccessCounter = Counter.builder("cache.preload.success").register(meterRegistry);
        this.preloadFailureCounter = Counter.builder("cache.preload.failure").register(meterRegistry);
    }
    
    public void recordSuccess(String cacheType, long count) {
        preloadSuccessCounter.increment();
        Gauge.builder("cache.preload.count")
            .tag("type", cacheType)
            .register(meterRegistry, count, Number::doubleValue);
    }
    
    public void recordFailure(String cacheType) {
        preloadFailureCounter.increment();
    }
}

总结

选择合适的预加载方案需要考虑以下因素:

  • @PostConstruct: 适合简单的预加载逻辑
  • ApplicationRunner: 适合需要命令行参数的场景
  • ApplicationReadyEvent: 最安全的预加载时机
  • 专门的服务: 适合复杂的预加载需求

无论选择哪种方案,都要注意异常处理、性能优化和监控告警,确保预加载过程不会影响应用的正常启动和运行。

相关推荐
bksczm1 分钟前
MySQL基础篇之事务
linux·数据库·sql·mysql
李白客2 分钟前
数据库管理工具怎么选?Navicat、DataGrip、DBeaver 六维横向对比与选型建议
运维·数据库
马立杰3 分钟前
mysql中Illegal mix of collations for operation “UNION”错误的解决方法
数据库·sql·网络安全
蓝速科技5 分钟前
蓝速 K10 三防平板在工业恶劣工况下的选型与应用指南
大数据·运维·数据库·人工智能·科技
bksczm7 分钟前
MySQL基础篇之视图与用户管理
linux·数据库·sql·mysql
huaweichenai12 分钟前
spring boot实现任务异步处理(队列)
java·spring boot
香菜TTT13 分钟前
怎么实现MySQL的主从同步机制
数据库·mysql
MZA66616 分钟前
实战:基于 XXL-JOB + 策略模式 + Nacos 配置化的 CSV 导出任务系统设计
java·spring boot·mybatis
Bs_MoneyMagnet4 小时前
基于springboot+vue的滑雪场票务与装备租赁系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring·毕业设计·计算机毕业设计
2501_933670799 小时前
2026秋招数据分析岗备考路线:SQL、BI、项目与面试题拆解
数据库