开发者

SpringBoot启动时将数据库数据预加载到Redis缓存的几种实现方案

目录
  • 引言
  • 方案一:使用 @PostConstruct 注解
  • 方案二:实现 ApplicationRunner 接口
  • 方案三:监听 ApplicationReadyEvent 事件
  • 方案四:创建专门的预加载服务
  • 配置文件控制
  • 最佳实践建议
    • 1. 异常处理
    • 2. 分批处理
    • 3. 监控和告警
  • 总结

    引言

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

    方案一:使用 @PostConstruct 注解

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

    @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);
            }
        }
    }
    

    方案二:实现 ApplicatioAIYrinRunner 接口

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

    @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 事件

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

    @Component
    @Slf4j
    public class CacheEventListener {
        
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        
        @Autowired
        private ProductService productService;js
        
        @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("商品分类缓存预加载完成");
        }
    }
    

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

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

    @Service
    @Slf4j
    public class DataPreloadService {
        
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        
        @Autowired
        private DictService dictService;
        
        @Autowiredhttp://www.devze.com
        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) -> {
                    S编程客栈tring key = "regions_level:" + level;
                    redisTemplate.opsForValue().set(key, levelRegions, Duration.ofDays(30));
                });
                
                log.info("地区数据缓存预加载完成,共 {} 条记录", regions.size());javascript
            } catch (Exception e) {
                log.error("地区数据缓存预加载失败", e);
            }
        }
    }
    

    配置文件控制

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

    app:
      cache:
        preload:
          enabled: true
          async: true
          timeout: 30000  # 超时时间(毫秒)
          BATch-size: 1000  # 批处理大小
    

    对应的配置类:

    @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. 异常处理

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

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

    2. 分批处理

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

    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. 监控和告警

    添加预加载状态监控:

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

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

    以上就是SpringBoot启动时将数据库数据预加载到Redis缓存的几种实现方案的详细内容,更多关于SpringBoot数据加载到Redis缓存的资料请关注编程客栈(www.devze.com)其它相关文章!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜