
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: 最安全的预加载时机
- 专门的服务: 适合复杂的预加载需求
无论选择哪种方案,都要注意异常处理、性能优化和监控告警,确保预加载过程不会影响应用的正常启动和运行。