第一次使用緩存,因?yàn)闆](méi)預(yù)熱,翻車(chē)了
預(yù)熱一般指緩存預(yù)熱,一般用在高并發(fā)系統(tǒng)中,為了提升系統(tǒng)在高并發(fā)情況下的穩(wěn)定性的一種手段。
緩存預(yù)熱是指在系統(tǒng)啟動(dòng)之前或系統(tǒng)達(dá)到高峰期之前,通過(guò)預(yù)先將常用數(shù)據(jù)加載到緩存中,以提高緩存命中率和系統(tǒng)性能的過(guò)程。緩存預(yù)熱的目的是盡可能地避免緩存擊穿和緩存雪崩,還可以減輕后端存儲(chǔ)系統(tǒng)的負(fù)載,提高系統(tǒng)的響應(yīng)速度和吞吐量。
預(yù)熱的必要性
緩存預(yù)熱的好處有很多,如:
- 減少冷啟動(dòng)影響:當(dāng)系統(tǒng)重啟或新啟動(dòng)時(shí),緩存是空的,這被稱(chēng)為冷啟動(dòng)。冷啟動(dòng)可能導(dǎo)致首次請(qǐng)求處理緩慢,因?yàn)閿?shù)據(jù)需要從慢速存儲(chǔ)(如數(shù)據(jù)庫(kù))檢索。
- 提高數(shù)據(jù)訪問(wèn)速度:通過(guò)預(yù)先加載常用數(shù)據(jù)到緩存中,可以確保數(shù)據(jù)快速可用,從而加快數(shù)據(jù)訪問(wèn)速度。
- 平滑流量峰值:在流量高峰期之前預(yù)熱緩存可以幫助系統(tǒng)更好地處理高流量,避免在流量激增時(shí)出現(xiàn)性能下降。
- 保證數(shù)據(jù)的時(shí)效性:定期預(yù)熱可以保證緩存中的數(shù)據(jù)是最新的,特別是對(duì)于高度依賴(lài)于實(shí)時(shí)數(shù)據(jù)的系統(tǒng)。
- 減少對(duì)后端系統(tǒng)的壓力:通過(guò)緩存預(yù)熱,可以減少對(duì)數(shù)據(jù)庫(kù)或其他后端服務(wù)的直接查詢(xún),從而減輕它們的負(fù)載。
預(yù)熱的方法
緩存預(yù)熱的一般做法是在系統(tǒng)啟動(dòng)或系統(tǒng)空閑期間,將常用的數(shù)據(jù)加載到緩存中,主要做法有以下幾種:
系統(tǒng)啟動(dòng)時(shí)加載:在系統(tǒng)啟動(dòng)時(shí),將常用的數(shù)據(jù)加載到緩存中,以便后續(xù)的訪問(wèn)可以直接從緩存中獲取。
定時(shí)任務(wù)加載:定時(shí)執(zhí)行任務(wù),將常用的數(shù)據(jù)加載到緩存中,以保持緩存中數(shù)據(jù)的實(shí)時(shí)性和準(zhǔn)確性。
手動(dòng)觸發(fā)加載:在系統(tǒng)達(dá)到高峰期之前,手動(dòng)觸發(fā)加載常用數(shù)據(jù)到緩存中,以提高緩存命中率和系統(tǒng)性能。
用時(shí)加載:在用戶(hù)請(qǐng)求到來(lái)時(shí),根據(jù)用戶(hù)的訪問(wèn)模式和業(yè)務(wù)需求,動(dòng)態(tài)地將數(shù)據(jù)加載到緩存中。
緩存加載器:一些緩存框架提供了緩存加載器的機(jī)制,可以在緩存中不存在數(shù)據(jù)時(shí),自動(dòng)調(diào)用加載器加載數(shù)據(jù)到緩存中。
Redis預(yù)熱
在分布式緩存中,我們通常都是使用Redis,針對(duì)Redis的預(yù)熱,有以下幾個(gè)工具可供使用,幫助我們實(shí)現(xiàn)緩存的預(yù)熱:
RedisBloom:RedisBloom是Redis的一個(gè)模塊,提供了多個(gè)數(shù)據(jù)結(jié)構(gòu),包括布隆過(guò)濾器、計(jì)數(shù)器、和TopK數(shù)據(jù)結(jié)構(gòu)等。其中,布隆過(guò)濾器可以用于Redis緩存預(yù)熱,通過(guò)將預(yù)熱數(shù)據(jù)添加到布隆過(guò)濾器中,可以快速判斷一個(gè)鍵是否存在于緩存中。
Redis Bulk loading:這是一個(gè)官方出的,基于Redis協(xié)議批量寫(xiě)入數(shù)據(jù)的工具
Redis Desktop Manager:Redis Desktop Manager是一個(gè)圖形化的Redis客戶(hù)端,可以用于管理Redis數(shù)據(jù)庫(kù)和進(jìn)行緩存預(yù)熱。通過(guò)Redis Desktop Manager,可以輕松地將預(yù)熱數(shù)據(jù)批量導(dǎo)入到Redis緩存中。
應(yīng)用啟動(dòng)時(shí)預(yù)熱
ApplicationReadyEvent
在應(yīng)用程序啟動(dòng)時(shí),可以通過(guò)監(jiān)聽(tīng)?wèi)?yīng)用啟動(dòng)事件,或者在應(yīng)用的初始化階段,將需要緩存的數(shù)據(jù)加載到緩存中。
ApplicationReadyEvent 是 Spring Boot 框架中的一個(gè)事件類(lèi),它表示應(yīng)用程序已經(jīng)準(zhǔn)備好接收請(qǐng)求,即應(yīng)用程序已啟動(dòng)且上下文已刷新。這個(gè)事件是在 ApplicationContext 被初始化和刷新,并且應(yīng)用程序已經(jīng)準(zhǔn)備好處理請(qǐng)求時(shí)觸發(fā)的。
基于ApplicationReadyEvent,我們可以在應(yīng)用程序完全啟動(dòng)并處于可用狀態(tài)后執(zhí)行一些初始化邏輯。使用 @EventListener 注解或?qū)崿F(xiàn) ApplicationListener 接口來(lái)監(jiān)聽(tīng)這個(gè)事件。例如,使用 @EventListener 注解:
@EventListener(ApplicationReadyEvent.class)
public void preloadCache() {
// 在應(yīng)用啟動(dòng)后執(zhí)行緩存預(yù)熱邏輯
// ...
}Runner
如果你不想直接監(jiān)聽(tīng)ApplicationReadyEvent,在SpringBoot中,也可以通過(guò)CommandLineRunner 和 ApplicationRunner 來(lái)實(shí)現(xiàn)這個(gè)功能。
CommandLineRunner 和 ApplicationRunner 是 Spring Boot 中用于在應(yīng)用程序啟動(dòng)后執(zhí)行特定邏輯的接口。這解釋聽(tīng)上去就像是專(zhuān)門(mén)干這個(gè)事兒的。
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// 在應(yīng)用啟動(dòng)后執(zhí)行緩存預(yù)熱邏輯
// ...
}
}import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// 在應(yīng)用啟動(dòng)后執(zhí)行緩存預(yù)熱邏輯
// ...
}
}CommandLineRunner 和 ApplicationRunner的調(diào)用,是在SpringApplication的run方法中
其實(shí)就是callRunners(context, applicationArguments);的實(shí)現(xiàn):
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}使用InitializingBean接口
實(shí)現(xiàn) InitializingBean 接口,并在 afterPropertiesSet 方法中執(zhí)行緩存預(yù)熱的邏輯。這樣,Spring 在初始化 Bean 時(shí)會(huì)調(diào)用 afterPropertiesSet 方法。
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
@Component
public class CachePreloader implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
// 執(zhí)行緩存預(yù)熱邏輯
// ...
}
}這個(gè)方法的調(diào)用我們?cè)赟pring的啟動(dòng)流程中也介紹過(guò),不再展開(kāi)了
使用@PostConstruct注解
類(lèi)似的,我們還可以使用 @PostConstruct 注解標(biāo)注一個(gè)方法,該方法將在 Bean 的構(gòu)造函數(shù)執(zhí)行完畢后立即被調(diào)用。在這個(gè)方法中執(zhí)行緩存預(yù)熱的邏輯。
import javax.annotation.PostConstruct;
import org.springframework.stereotype.Component;
@Component
public class CachePreloader {
@PostConstruct
public void preloadCache() {
// 執(zhí)行緩存預(yù)熱邏輯
// ...
}
}定時(shí)任務(wù)預(yù)熱
在啟動(dòng)過(guò)程中預(yù)熱有一個(gè)問(wèn)題,那就是一旦啟動(dòng)之后,如果需要預(yù)熱新的數(shù)據(jù),或者需要修改數(shù)據(jù),就不支持了,那么,在應(yīng)用的運(yùn)行過(guò)程中,我們也是可以通過(guò)定時(shí)任務(wù)來(lái)實(shí)現(xiàn)緩存的更新預(yù)熱的。
我們通常依賴(lài)這種方式來(lái)確保緩存中的數(shù)據(jù)是最新的,避免因?yàn)闃I(yè)務(wù)數(shù)據(jù)的變化而導(dǎo)致緩存數(shù)據(jù)過(guò)時(shí)。
在Spring中,想要實(shí)現(xiàn)一個(gè)定時(shí)任務(wù)也挺簡(jiǎn)單的,基于@Scheduled就可以輕易實(shí)現(xiàn).
@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1點(diǎn)執(zhí)行
public void scheduledCachePreload() {
// 執(zhí)行緩存預(yù)熱邏輯
// ...
}也可以依賴(lài)xxl-job等定時(shí)任務(wù)實(shí)現(xiàn)。
緩存器預(yù)熱
些緩存框架提供了緩存加載器的機(jī)制,可以在緩存中不存在數(shù)據(jù)時(shí),自動(dòng)調(diào)用加載器加載數(shù)據(jù)到緩存中。這樣可以簡(jiǎn)化緩存預(yù)熱的邏輯。如Caffeine中就有這樣的功能:
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class MyCacheService {
private final LoadingCache<String, String> cache;
public MyCacheService() {
this.cache = Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES) // 配置自動(dòng)刷新,1分鐘刷新一次
.build(key -> loadDataFromSource(key)); // 使用加載器加載數(shù)據(jù)
}
public String getValue(String key) {
return cache.get(key);
}
private String loadDataFromSource(String key) {
// 從數(shù)據(jù)源加載數(shù)據(jù)的邏輯
// 這里只是一個(gè)示例,實(shí)際應(yīng)用中可能是從數(shù)據(jù)庫(kù)、外部服務(wù)等獲取數(shù)據(jù)
System.out.println("Loading data for key: " + key);
return "Value for " + key;
}
}在上面的例子中,我們使用 Caffeine.newBuilder().refreshAfterWrite(1, TimeUnit.MINUTES) 配置了緩存的自動(dòng)刷新機(jī)制,即每個(gè)緩存項(xiàng)在寫(xiě)入后的1分鐘內(nèi),如果有讀請(qǐng)求,Caffeine 會(huì)自動(dòng)觸發(fā)數(shù)據(jù)的刷新。
loadDataFromSource 方法是用于加載數(shù)據(jù)的自定義方法。你可以在這個(gè)方法中實(shí)現(xiàn)從數(shù)據(jù)源(例如數(shù)據(jù)庫(kù)、外部服務(wù))加載數(shù)據(jù)的邏輯。


























