缓存预热有哪些方案?

一道经典面试题:缓存预热有哪些方案?

在系统业务高峰期到来之前,我们提前将一些热点数据加载到缓存中,进而提高系统的响应速度,这就是所谓的缓存预热。

那么怎么实现缓存预热呢?

一般来说,我们主要有三种思路:

  • 系统启动时加载缓存。
  • 定时任务加载缓存。
  • 使用缓存加载器。

每种里边往往又对应了不同的具体方案,我们逐一来看。

一 系统启动时加载缓存

这个就是利用系统启动时候的一些钩子函数,或者如事件监听机制或者是框架专为系统启动预留的方法,在这些方法中去加载缓存。

这里松哥给四个常见的思路。

1.1 启动监听事件

使用 ApplicationListener 监听 Spring Boot 启动完成的事件,如 ContextRefreshedEventApplicationReadyEvent,在事件触发后加载数据到缓存。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

@Component
public class CacheWarmer implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 假设有一个缓存管理器cacheManager
cacheManager.put("key1", "松哥");
cacheManager.put("key2", "江南一点雨");
// ... 加载更多数据到缓存
}
}

1.2 @PostConstruct 注解

使用 @PostConstruct 注解在 Spring Bean 初始化后立即执行缓存预热逻辑。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import javax.annotation.PostConstruct;
import org.springframework.stereotype.Component;

@Component
public class CachePreloader {
private final CacheManager cacheManager;

public CachePreloader(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}

@PostConstruct
public void preloadCache() {
cacheManager.put("key1", "江南一点雨");
cacheManager.put("key2", "松哥");
// ... 加载更多数据到缓存
}
}

1.3 CommandLineRunner 或 ApplicationRunner

实现 CommandLineRunnerApplicationRunner 接口,在 Spring Boot 启动后执行缓存预热。

CommandLineRunner 案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class MyCommandLineRunner implements CommandLineRunner {
private final CacheManager cacheManager;

public MyCommandLineRunner(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}

@Override
public void run(String... args) throws Exception {
cacheManager.put("key1", "江南一点雨");
cacheManager.put("key2", "松哥");
// ... 加载更多数据到缓存
}
}

ApplicationRunner 案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class MyApplicationRunner implements ApplicationRunner {
private final CacheManager cacheManager;

public MyApplicationRunner(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}

@Override
public void run(ApplicationArguments args) throws Exception {
cacheManager.put("key1", "江南一点雨");
cacheManager.put("key2", "松哥");
// ... 加载更多数据到缓存
}
}

1.4 InitializingBean接口

实现 InitializingBean 接口,并在 afterPropertiesSet 方法中执行缓存预热。

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

@Component
public class CachePreloader implements InitializingBean {
private final CacheManager cacheManager;

public CachePreloader(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}

@Override
public void afterPropertiesSet() throws Exception {
cacheManager.put("key1", "松哥");
cacheManager.put("key2", "江南一点雨");
// ... 加载更多数据到缓存
}
}

在这些案例中,CacheManager是一个假设的缓存管理器,你需要根据实际使用的缓存技术(如 Redis、EhCache 等)来实现或注入相应的缓存管理器。这些代码片段展示了如何在 Spring Boot 应用启动后,通过不同的方式加载数据到缓存中,以减少应用启动后的首次加载延迟。

在系统启动时加载缓存往往有一个问题,就是这些缓存加载之后,不会主动更新。如果我们需要对缓存进行定期更新,那么就可以考虑使用定时任务去加载缓存。

二 定时任务加载缓存

一般来说,如果我们面对以下需求时,就可以考虑使用定时任务加载缓存。

  1. 数据依赖性:当缓存的数据依赖于外部系统或数据库的定期更新,且这些更新不是实时触发的,而是按照一定的时间间隔发生时,可以通过定时任务来预热缓存。
  2. 数据量大:如果预热的数据量非常大,一次性加载可能会对系统性能产生影响,可以通过定时任务分批次逐步加载数据到缓存中。
  3. 依赖多个数据源:当缓存的数据需要从多个数据源聚合时,可以通过定时任务来协调这些数据源的更新,确保缓存的数据一致性和准确性。
  4. 业务逻辑复杂:如果缓存预热的业务逻辑比较复杂,涉及到多步骤处理或者需要等待某些异步操作完成,定时任务可以在特定时间点触发这些复杂的业务逻辑。
  5. 资源分配:在资源受限的环境中,为了避免在应用启动时占用过多资源,可以通过定时任务在系统资源较为空闲的时候进行缓存预热。

举个栗子:假设有一个电商网站,商品信息每天凌晨由供应商更新,并且更新操作不是实时的。在这种情况下,可以设置一个定时任务,在每天凌晨更新完成后,将最新的商品信息加载到缓存中,以便用户在白天访问时能够快速获取到最新的商品数据。

在 Spring 框架中,我们可以使用 @Scheduled 注解来实现定时任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class CacheWarmUpTask {

private final CacheManager cacheManager;

public CacheWarmUpTask(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}

@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行
public void warmUpCache() {
// 执行缓存预热逻辑
cacheManager.put("key1", "江南一点雨");
cacheManager.put("key2", "松哥");
// ... 加载更多数据到缓存
}
}

@Scheduled 注解用于指定定时任务的执行计划,cron 表达式定义了任务的执行时间。这样,每天凌晨 1 点,定时任务就会执行,将数据加载到缓存中。

三 使用缓存加载器

缓存加载器(Cache Loader)用于在缓存中自动加载数据。

当缓存中缺少请求的数据时,缓存加载器会被触发,以便从原始数据源(如数据库、文件系统或其他服务)中加载数据并将其放入缓存中。

缓存加载器的主要目的是减少直接从原始数据源加载数据的延迟,提高数据访问的速度。

缓存加载器通常与缓存库或框架一起使用,如 Guava Cache、EhCache、Caffeine 等。在 Spring Cache abstract 中,也可以通过自定义的缓存管理器来实现缓存加载器的功能。

3.1 使用缓存加载器步骤

  1. 定义缓存加载器逻辑:首先,我们需要定义一个加载数据的逻辑,这个逻辑会在缓存中缺失数据时被调用。
  2. 配置缓存:在缓存配置中指定缓存加载器。
  3. 预加载数据:在应用启动时或在特定时间点,通过调用缓存的获取方法来触发缓存加载器,从而预先加载数据到缓存中。
  4. 定时任务:如果需要定期更新缓存,可以结合定时任务(如 Spring 的 @Scheduled 注解)来定期触发缓存加载器。

3.2 举个栗子

Caffeine 是一个高性能的 Java 缓存库,它提供了丰富的缓存策略和灵活的配置选项。Caffeine 并没有直接的缓存预热 API,但是可以通过在应用启动时预先加载数据到缓存中来实现缓存预热的效果。

下面是使用 Caffeine 进行缓存预热的步骤:

  1. 定义缓存:首先,你需要定义一个 Caffeine 缓存实例,并配置相应的缓存策略。
  2. 预加载数据:在应用启动时,通过显式调用缓存的 get 方法或使用 getAll 方法来加载数据到缓存中。
  3. 监听器:如果需要在缓存加载时执行额外的操作,可以配置 CacheLoader 的监听器。
  4. 异步加载:如果数据加载是异步的,可以使用 AsyncLoadingCache 接口来处理异步加载的情况。

代码案例

定义缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;

import java.util.concurrent.TimeUnit;

public class CacheInitializer {
public static LoadingCache<String, String> createCache() {
CacheLoader<String, String> loader = new CacheLoader<String, String>() {
@Override
public String load(String key) {
// 模拟从数据库或其他数据源加载数据
return "Value for " + key;
}
};

return Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES) // 设置写入后过期时间
.maximumSize(1000) // 设置最大缓存项数
.build(loader);
}
}

预加载数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.github.benmanes.caffeine.cache.LoadingCache;

import java.util.Arrays;
import java.util.concurrent.ExecutionException;

public class CacheWarmUp {
private final LoadingCache<String, String> cache;

public CacheWarmUp(LoadingCache<String, String> cache) {
this.cache = cache;
}

public void warmUp() throws ExecutionException {
// 预加载数据到缓存
Arrays.asList("key1", "key2", "key3").forEach(key -> {
try {
cache.get(key); // 这将触发加载器加载数据
} catch (ExecutionException e) {
e.printStackTrace();
}
});
}
}

在 Spring Boot 应用中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class CacheWarmUpRunner implements CommandLineRunner {
private final LoadingCache<String, String> cache;

public CacheWarmUpRunner(LoadingCache<String, String> cache) {
this.cache = cache;
}

@Override
public void run(String... args) {
try {
new CacheWarmUp(cache).warmUp();
} catch (Exception e) {
e.printStackTrace();
}
}
}

在这个示例中,CacheInitializer 类定义了一个 Caffeine 缓存实例,CacheWarmUp 类包含了预加载数据到缓存的逻辑。CacheWarmUpRunner 是一个 Spring Boot 的 CommandLineRunner,它在应用启动时调用 CacheWarmUp 类来预加载数据。

通过这种方式,你可以在应用启动时预先加载数据到 Caffeine 缓存中,从而实现缓存预热。这对于提高应用的响应速度和减少首次加载延迟非常有帮助。

上面的案例本质上还是利用了 CommandLineRunner,不过,LoadingCache 在配置的时候,其实也可以设置一个自动刷新的时间,这样就不需要 CommandLineRunner 了,系统会自动执行。

好啦,缓存预热的三种思路,小伙伴们平时都是怎么做的?欢迎留言讨论。