SpringBoot啟動時將數(shù)據(jù)庫數(shù)據(jù)預(yù)加載到Redis緩存的幾種實現(xiàn)方案
引言
在實際項目開發(fā)中,我們經(jīng)常需要在應(yīng)用啟動時將一些固定的、頻繁訪問的數(shù)據(jù)從數(shù)據(jù)庫預(yù)加載到 Redis 緩存中,以提高系統(tǒng)性能。本文將介紹幾種實現(xiàn)方案。
方案一:使用 @PostConstruct 注解
這是最簡單直接的方式,在 Spring Bean 初始化完成后執(zhí)行預(yù)加載邏輯。
@Component
@Slf4j
public class CachePreloader {
@Autowired
private UserService userService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@PostConstruct
public void preloadCache() {
log.info("開始預(yù)加載緩存數(shù)據(jù)...");
try {
// 加載用戶基礎(chǔ)信息
List<User> users = userService.getAllActiveUsers();
for (User user : users) {
String key = "user:" + user.getId();
redisTemplate.opsForValue().set(key, user, Duration.ofHours(24));
}
// 加載系統(tǒng)配置
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("緩存預(yù)加載完成,共加載 {} 條用戶數(shù)據(jù),{} 條配置數(shù)據(jù)",
users.size(), configs.size());
} catch (Exception e) {
log.error("緩存預(yù)加載失敗", e);
}
}
}
方案二:實現(xiàn) ApplicationRunner 接口
ApplicationRunner 在應(yīng)用啟動完成后執(zhí)行,可以獲取命令行參數(shù),更適合復(fù)雜的初始化邏輯。
@Component
@Order(1) // 設(shè)置執(zhí)行順序
@Slf4j
public class CacheApplicationRunner implements ApplicationRunner {
@Autowired
private DataPreloadService dataPreloadService;
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("ApplicationRunner 開始執(zhí)行緩存預(yù)加載...");
// 檢查是否需要預(yù)加載
if (args.containsOption("skip-cache-preload")) {
log.info("跳過緩存預(yù)加載");
return;
}
dataPreloadService.preloadAllCache();
log.info("ApplicationRunner 緩存預(yù)加載完成");
}
}
方案三:監(jiān)聽 ApplicationReadyEvent 事件
這種方式在應(yīng)用完全啟動就緒后執(zhí)行,是最安全的預(yù)加載時機(jī)。
@Component
@Slf4j
public class CacheEventListener {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductService productService;
@EventListener
public void handleApplicationReady(ApplicationReadyEvent event) {
log.info("應(yīng)用啟動完成,開始預(yù)加載緩存...");
// 異步執(zhí)行預(yù)加載,避免阻塞啟動
CompletableFuture.runAsync(() -> {
try {
preloadProductCache();
preloadCategoryCache();
} catch (Exception e) {
log.error("異步預(yù)加載緩存失敗", 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("熱門商品緩存預(yù)加載完成,共 {} 條", hotProducts.size());
}
private void preloadCategoryCache() {
List<Category> categories = productService.getAllCategories();
redisTemplate.opsForValue().set("all_categories", categories, Duration.ofDays(1));
log.info("商品分類緩存預(yù)加載完成");
}
}
方案四:創(chuàng)建專門的預(yù)加載服務(wù)
將預(yù)加載邏輯封裝成獨立的服務(wù),便于管理和測試。
@Service
@Slf4j
public class DataPreloadService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private DictService dictService;
@Autowired
private RegionService regionService;
/**
* 預(yù)加載所有緩存數(shù)據(jù)
*/
public void preloadAllCache() {
long startTime = System.currentTimeMillis();
try {
// 并行加載多種數(shù)據(jù)
CompletableFuture<Void> dictFuture = CompletableFuture.runAsync(this::preloadDictCache);
CompletableFuture<Void> regionFuture = CompletableFuture.runAsync(this::preloadRegionCache);
// 等待所有任務(wù)完成
CompletableFuture.allOf(dictFuture, regionFuture).join();
long endTime = System.currentTimeMillis();
log.info("所有緩存預(yù)加載完成,耗時: {} ms", endTime - startTime);
} catch (Exception e) {
log.error("緩存預(yù)加載過程中發(fā)生異常", e);
}
}
/**
* 預(yù)加載數(shù)據(jù)字典
*/
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("數(shù)據(jù)字典緩存預(yù)加載完成,共 {} 種類型", dictMap.size());
} catch (Exception e) {
log.error("數(shù)據(jù)字典緩存預(yù)加載失敗", e);
}
}
/**
* 預(yù)加載地區(qū)數(shù)據(jù)
*/
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("地區(qū)數(shù)據(jù)緩存預(yù)加載完成,共 {} 條記錄", regions.size());
} catch (Exception e) {
log.error("地區(qū)數(shù)據(jù)緩存預(yù)加載失敗", e);
}
}
}
配置文件控制
在 application.yml 中添加配置項,控制預(yù)加載行為:
app:
cache:
preload:
enabled: true
async: true
timeout: 30000 # 超時時間(毫秒)
batch-size: 1000 # 批處理大小
對應(yīng)的配置類:
@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. 異常處理
預(yù)加載過程中的異常不應(yīng)該影響應(yīng)用啟動:
@PostConstruct
public void preloadCache() {
try {
// 預(yù)加載邏輯
} catch (Exception e) {
log.error("緩存預(yù)加載失敗,但不影響應(yīng)用啟動", e);
// 可以發(fā)送告警通知
}
}
2. 分批處理
對于大量數(shù)據(jù),應(yīng)該分批處理避免內(nèi)存溢出:
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("已預(yù)加載第 {} 批數(shù)據(jù),本批 {} 條", pageNum, dataList.size());
}
}
3. 監(jiān)控和告警
添加預(yù)加載狀態(tài)監(jiān)控:
@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();
}
}
總結(jié)
選擇合適的預(yù)加載方案需要考慮以下因素:
- @PostConstruct: 適合簡單的預(yù)加載邏輯
- ApplicationRunner: 適合需要命令行參數(shù)的場景
- ApplicationReadyEvent: 最安全的預(yù)加載時機(jī)
- 專門的服務(wù): 適合復(fù)雜的預(yù)加載需求
無論選擇哪種方案,都要注意異常處理、性能優(yōu)化和監(jiān)控告警,確保預(yù)加載過程不會影響應(yīng)用的正常啟動和運行。
以上就是SpringBoot啟動時將數(shù)據(jù)庫數(shù)據(jù)預(yù)加載到Redis緩存的幾種實現(xiàn)方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot數(shù)據(jù)加載到Redis緩存的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring Boot使用yml格式進(jìn)行配置的方法
很多springboot項目使用的是yml格式,主要目的是方便對讀懂其他人的項目,下面小編通過本文給大家分享Spring Boot使用yml格式進(jìn)行配置的方法,需要的朋友參考下吧2018-04-04
基于@RequestParam注解之Spring MVC參數(shù)綁定的利器
這篇文章主要介紹了基于@RequestParam注解之Spring MVC參數(shù)綁定的利器,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-03-03
JAVA統(tǒng)計字符串中某個字符出現(xiàn)次數(shù)的方法實現(xiàn)
本文主要介紹了JAVA統(tǒng)計字符串中某個字符出現(xiàn)次數(shù)的方法實現(xiàn),可以循環(huán)使用String的charAt(int index)函數(shù),具有一定的參考價值,感興趣的可以了解一下2023-11-11
Java ConcurrentModificationException異常解決案例詳解
這篇文章主要介紹了Java ConcurrentModificationException異常解決案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09
java 多線程Thread與runnable的區(qū)別
這篇文章主要介紹了java 多線程Thread與runnable的區(qū)別的相關(guān)資料,java線程有兩種方法繼承thread類與實現(xiàn)runnable接口,下面就提供實例幫助大家理解,需要的朋友可以參考下2017-08-08
springBoot集成Ollama大模型及流式傳輸?shù)膯栴}小結(jié)
Ollama是一個用于部署和運行各種開源大模型的工具,它支持多種平臺和設(shè)備,包括Windows、Mac和Linux,甚至可以在樹莓派等輕量級設(shè)備上運行,這篇文章主要介紹了springBoot集成Ollama大模型及流式傳輸?shù)膯栴}小結(jié),需要的朋友可以參考下2025-04-04

