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