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: 最安全的预加载时机
  • 专门的服务: 适合复杂的预加载需求

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

相关推荐
瀚高PG实验室15 分钟前
易智瑞GeoScene Pro连接瀚高安全版数据库 458
数据库·安全·瀚高数据库
551只玄猫18 分钟前
【数据库原理 实验报告3】索引的创建以及数据更新
数据库·sql·课程设计·实验报告·操作系统原理
加农炮手Jinx21 分钟前
Flutter for OpenHarmony:postgrest 直接访问 PostgreSQL 数据库的 RESTful 客户端(Supabase 核心驱动) 深度解析与鸿蒙适配指南
数据库·flutter·华为·postgresql·restful·harmonyos·鸿蒙
Augustine Electra29 分钟前
Flutter 三方库 memoize 的鸿蒙化实战 - 引入极简缓存引擎,避免重复计算,大幅提升鸿蒙应用渲染性能,让你的高刷体验更稳更丝滑。
flutter·缓存·harmonyos
xiaohe071 小时前
Spring Boot 各种事务操作实战(自动回滚、手动回滚、部分回滚)
java·数据库·spring boot
setmoon2141 小时前
使用Scikit-learn构建你的第一个机器学习模型
jvm·数据库·python
gechunlian881 小时前
Spring Boot中的404错误:原因、影响及处理策略
java·spring boot·后端
2401_833197732 小时前
为你的Python脚本添加图形界面(GUI)
jvm·数据库·python
执笔画情ora2 小时前
oracle数据库优化-表碎片优化性能。
数据库·oracle
givemeacar2 小时前
Spring Boot中集成MyBatis操作数据库详细教程
数据库·spring boot·mybatis