SpringBoot + 缓存预热 + 启动时加载:服务启动即预热热点数据,避免冷启动抖动
前言
想象一下这个场景:凌晨 2 点,你正在值班,突然收到告警——核心服务重启了。紧接着,大量用户反馈系统卡顿、响应超时。这就是典型的冷启动问题:服务刚启动时缓存为空,大量请求直接打到数据库,导致系统性能急剧下降。
缓存预热(Cache Warmup)就是解决这个问题的利器。通过在服务启动时提前将热点数据加载到缓存中,我们可以避免冷启动带来的性能抖动,确保服务从第一秒起就能提供稳定、高效的响应。
本文将介绍一套完整的 SpringBoot 缓存预热方案,包括:
- 多级缓存预热:本地缓存 + 分布式缓存协同预热
- 异步加载:不阻塞服务启动流程
- 智能调度:按优先级和依赖关系加载数据
- 监控告警:实时掌握预热进度和状态
一、冷启动问题分析
1. 什么是冷启动问题
┌─────────────────────────────────────────────────────────────┐
│ 冷启动问题示意 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 服务启动前 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 缓存:空 │ │
│ │ 数据库:正常 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 服务启动后(0-60秒) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 请求1 ──► 缓存未命中 ──► 查询数据库 ──► 写入缓存 │ │
│ │ 请求2 ──► 缓存未命中 ──► 查询数据库 ──► 写入缓存 │ │
│ │ 请求3 ──► 缓存未命中 ──► 查询数据库 ──► 写入缓存 │ │
│ │ ... │ │
│ │ 请求N ──► 缓存未命中 ──► 查询数据库 ──► 写入缓存 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 结果: │
│ - 数据库压力激增(缓存击穿) │
│ - 响应时间从 10ms 上升到 500ms+ │
│ - 部分请求超时失败 │
│ │
└─────────────────────────────────────────────────────────────┘
2. 冷启动的影响
| 场景 | 影响 | 后果 |
|---|---|---|
| 电商大促 | 秒杀开始时服务重启 | 大量用户无法下单,直接损失 |
| 金融交易 | 交易高峰服务扩容 | 交易延迟,用户体验差 |
| 社交应用 | 热点事件时服务重启 | 信息流加载慢,用户流失 |
| 游戏服务 | 开服时服务启动 | 玩家登录失败,口碑受损 |
3. 缓存预热的价值
┌─────────────────────────────────────────────────────────────┐
│ 缓存预热的价值 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 无预热方案 有预热方案 │
│ ─────────── ─────────── │
│ │
│ 响应时间(ms) 响应时间(ms) │
│ ▲ ▲ │
│ 500 │ ╱╲ │ ╭─╮ │
│ 400 │ ╱ ╲ │ │ │ │
│ 300 │ ╱ ╲ │ │ │ │
│ 200 │ ╱ ╲ │ │ │ │
│ 100 │╱ ╲____ │__│ │________ │
│ 0 └──────────────► └──────────────► │
│ 启动 30s 60s 启动 30s 60s │
│ │
│ 特点: 特点: │
│ - 启动后响应时间剧烈波动 - 启动后响应时间稳定 │
│ - 数据库压力大 - 数据库压力平稳 │
│ - 用户体验差 - 用户体验一致 │
│ │
└─────────────────────────────────────────────────────────────┘
二、整体架构设计
1. 系统架构
┌─────────────────────────────────────────────────────────────┐
│ 缓存预热系统架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 配置管理层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ 预热数据配置 │ │ 加载策略配置 │ │ │
│ │ │ (YAML/DB) │ │ (优先级/并发)│ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 预热调度层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ 依赖分析器 │ │ 任务调度器 │ │ │
│ │ │ (DAG排序) │ │ (异步执行) │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 数据加载层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ 本地缓存加载 │ │ 分布式缓存 │ │ │
│ │ │ (Caffeine) │ │ 加载(Redis) │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 监控告警层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ 预热进度监控 │ │ 异常告警 │ │ │
│ │ │ (Metrics) │ │ (通知服务) │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
2. 核心组件
| 组件 | 职责 | 技术选型 |
|---|---|---|
| 预热配置中心 | 管理预热数据类型、加载策略 | YAML / 数据库 |
| 依赖分析器 | 分析数据加载依赖关系,生成 DAG | JGraphT |
| 任务调度器 | 异步调度预热任务,控制并发 | Spring @Async |
| 数据加载器 | 从数据源加载数据到缓存 | Repository + Cache |
| 进度监控器 | 监控预热进度和状态 | Micrometer |
| 健康检查器 | 检查预热完成状态 | Spring Boot Actuator |
3. 预热流程
┌─────────────────────────────────────────────────────────────┐
│ 缓存预热流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 服务启动 │
│ │ │
│ ▼ │
│ 2. 读取预热配置 │
│ │ │
│ ▼ │
│ 3. 分析依赖关系 │
│ │ │
│ ▼ │
│ 4. 创建预热任务 │
│ │ │
│ ▼ │
│ 5. 异步执行预热 │
│ │ │
│ ├──► 加载商品数据 ──► 写入本地缓存 ──► 写入 Redis │
│ ├──► 加载用户数据 ──► 写入本地缓存 ──► 写入 Redis │
│ ├──► 加载配置数据 ──► 写入本地缓存 │
│ └──► ... │
│ │ │
│ ▼ │
│ 6. 监控预热进度 │
│ │ │
│ ▼ │
│ 7. 预热完成 / 告警 │
│ │
└─────────────────────────────────────────────────────────────┘
三、代码实现
1. 项目结构
SpringBoot-CacheWarmup-Demo/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── cachewarmup/
│ │ ├── CacheWarmupApplication.java
│ │ ├── config/
│ │ │ ├── CacheConfig.java
│ │ │ └── WarmupConfig.java
│ │ ├── warmup/
│ │ │ ├── CacheWarmupManager.java
│ │ │ ├── WarmupTask.java
│ │ │ ├── WarmupTaskExecutor.java
│ │ │ ├── WarmupProgressTracker.java
│ │ │ └── loader/
│ │ │ ├── DataLoader.java
│ │ │ ├── ProductDataLoader.java
│ │ │ ├── UserDataLoader.java
│ │ │ └── ConfigDataLoader.java
│ │ ├── entity/
│ │ │ ├── Product.java
│ │ │ ├── User.java
│ │ │ └── SystemConfig.java
│ │ ├── repository/
│ │ │ ├── ProductRepository.java
│ │ │ ├── UserRepository.java
│ │ │ └── ConfigRepository.java
│ │ ├── service/
│ │ │ ├── ProductService.java
│ │ │ ├── UserService.java
│ │ │ └── ConfigService.java
│ │ ├── controller/
│ │ │ ├── WarmupController.java
│ │ │ └── DataController.java
│ │ └── monitor/
│ │ ├── WarmupMetrics.java
│ │ └── WarmupHealthIndicator.java
│ └── resources/
│ ├── application.yml
│ └── warmup-config.yml
├── pom.xml
└── README.md
2. 预热配置
# application.yml
spring:
application:
name: cache-warmup-demo
cache:
type: redis
redis:
time-to-live: 3600000
data:
redis:
host: localhost
port: 6379
datasource:
url: jdbc:h2:mem:testdb
username: sa
password:
driver-class-name: org.h2.Driver
# 缓存预热配置
cache:
warmup:
enabled: true
async: true
timeout-seconds: 300
max-concurrent: 5
tasks:
- name: product-cache
loader: productDataLoader
priority: 1
dependencies: []
local-cache: true
redis-cache: true
batch-size: 1000
- name: user-cache
loader: userDataLoader
priority: 2
dependencies: []
local-cache: true
redis-cache: true
batch-size: 500
- name: config-cache
loader: configDataLoader
priority: 3
dependencies: []
local-cache: true
redis-cache: false
batch-size: 100
logging:
level:
com.example.cachewarmup: DEBUG
3. 预热任务定义
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WarmupTask {
private String name;
private String loader;
private int priority;
private List<String> dependencies;
private boolean localCache;
private boolean redisCache;
private int batchSize;
@Builder.Default
private AtomicInteger loadedCount = new AtomicInteger(0);
@Builder.Default
private AtomicInteger totalCount = new AtomicInteger(0);
@Builder.Default
private volatile WarmupStatus status = WarmupStatus.PENDING;
private long startTime;
private long endTime;
private String errorMessage;
public enum WarmupStatus {
PENDING, RUNNING, COMPLETED, FAILED
}
public double getProgress() {
int total = totalCount.get();
if (total == 0) {
return 0.0;
}
return (double) loadedCount.get() / total * 100;
}
public long getDuration() {
if (startTime == 0) {
return 0;
}
long end = endTime > 0 ? endTime : System.currentTimeMillis();
return end - startTime;
}
}
4. 数据加载器接口
public interface DataLoader<T> {
String getName();
List<T> loadAll();
int getTotalCount();
default List<T> loadBatch(int offset, int limit) {
List<T> all = loadAll();
int fromIndex = Math.min(offset, all.size());
int toIndex = Math.min(offset + limit, all.size());
return all.subList(fromIndex, toIndex);
}
String getCacheKey(T item);
default long getCacheTtl() {
return 3600; // 默认1小时
}
}
5. 商品数据加载器
@Component
@Slf4j
public class ProductDataLoader implements DataLoader<Product> {
@Autowired
private ProductRepository productRepository;
@Autowired
private CacheManager cacheManager;
@Autowired
private StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public String getName() {
return "productDataLoader";
}
@Override
public List<Product> loadAll() {
log.info("开始加载所有商品数据");
return productRepository.findAll();
}
@Override
public int getTotalCount() {
return (int) productRepository.count();
}
@Override
public String getCacheKey(Product product) {
return "product:" + product.getId();
}
public void loadToLocalCache(List<Product> products) {
Cache cache = cacheManager.getCache("products");
if (cache == null) {
log.warn("本地缓存 'products' 不存在");
return;
}
for (Product product : products) {
cache.put(getCacheKey(product), product);
}
log.info("已加载 {} 条商品到本地缓存", products.size());
}
public void loadToRedis(List<Product> products) {
for (Product product : products) {
try {
String key = getCacheKey(product);
String value = objectMapper.writeValueAsString(product);
redisTemplate.opsForValue().set(key, value, getCacheTtl(), TimeUnit.SECONDS);
} catch (Exception e) {
log.error("写入 Redis 失败: productId={}", product.getId(), e);
}
}
log.info("已加载 {} 条商品到 Redis", products.size());
}
}
6. 缓存预热管理器
@Component
@Slf4j
public class CacheWarmupManager implements ApplicationRunner, ApplicationListener<ContextClosedEvent> {
@Autowired
private WarmupConfig warmupConfig;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private WarmupTaskExecutor taskExecutor;
@Autowired
private WarmupProgressTracker progressTracker;
private final Map<String, WarmupTask> tasks = new ConcurrentHashMap<>();
private final AtomicBoolean warmupCompleted = new AtomicBoolean(false);
@Override
public void run(ApplicationArguments args) {
if (!warmupConfig.isEnabled()) {
log.info("缓存预热已禁用");
return;
}
log.info("开始执行缓存预热...");
long startTime = System.currentTimeMillis();
try {
// 初始化任务
initializeTasks();
// 分析依赖关系并排序
List<WarmupTask> sortedTasks = analyzeDependencies();
// 执行预热
if (warmupConfig.isAsync()) {
executeAsync(sortedTasks);
} else {
executeSync(sortedTasks);
}
long duration = System.currentTimeMillis() - startTime;
log.info("缓存预热任务启动完成,耗时: {}ms", duration);
} catch (Exception e) {
log.error("缓存预热启动失败", e);
if (!warmupConfig.isContinueOnError()) {
throw new RuntimeException("缓存预热失败,服务启动中止", e);
}
}
}
private void initializeTasks() {
for (WarmupConfig.TaskConfig config : warmupConfig.getTasks()) {
WarmupTask task = WarmupTask.builder()
.name(config.getName())
.loader(config.getLoader())
.priority(config.getPriority())
.dependencies(config.getDependencies())
.localCache(config.isLocalCache())
.redisCache(config.isRedisCache())
.batchSize(config.getBatchSize())
.build();
tasks.put(task.getName(), task);
progressTracker.registerTask(task);
}
log.info("已初始化 {} 个预热任务", tasks.size());
}
private List<WarmupTask> analyzeDependencies() {
// 使用拓扑排序处理依赖关系
Map<String, Integer> inDegree = new HashMap<>();
Map<String, List<String>> graph = new HashMap<>();
// 初始化
for (String name : tasks.keySet()) {
inDegree.put(name, 0);
graph.put(name, new ArrayList<>());
}
// 构建图
for (WarmupTask task : tasks.values()) {
for (String dep : task.getDependencies()) {
if (tasks.containsKey(dep)) {
graph.get(dep).add(task.getName());
inDegree.put(task.getName(), inDegree.get(task.getName()) + 1);
}
}
}
// 拓扑排序
Queue<WarmupTask> queue = new LinkedList<>();
for (Map.Entry<String, Integer> entry : inDegree.entrySet()) {
if (entry.getValue() == 0) {
queue.offer(tasks.get(entry.getKey()));
}
}
List<WarmupTask> sorted = new ArrayList<>();
while (!queue.isEmpty()) {
WarmupTask task = queue.poll();
sorted.add(task);
for (String nextName : graph.get(task.getName())) {
int degree = inDegree.get(nextName) - 1;
inDegree.put(nextName, degree);
if (degree == 0) {
queue.offer(tasks.get(nextName));
}
}
}
// 按优先级排序
sorted.sort(Comparator.comparingInt(WarmupTask::getPriority));
return sorted;
}
private void executeAsync(List<WarmupTask> sortedTasks) {
CompletableFuture<Void> future = CompletableFuture.allOf(
sortedTasks.stream()
.map(taskExecutor::execute)
.toArray(CompletableFuture[]::new)
);
future.whenComplete((result, ex) -> {
if (ex != null) {
log.error("缓存预热执行失败", ex);
} else {
warmupCompleted.set(true);
log.info("所有缓存预热任务已完成");
}
});
}
private void executeSync(List<WarmupTask> sortedTasks) {
for (WarmupTask task : sortedTasks) {
try {
taskExecutor.executeSync(task);
} catch (Exception e) {
log.error("预热任务执行失败: {}", task.getName(), e);
if (!warmupConfig.isContinueOnError()) {
throw e;
}
}
}
warmupCompleted.set(true);
log.info("所有缓存预热任务已完成");
}
public boolean isWarmupCompleted() {
return warmupCompleted.get();
}
public Map<String, WarmupTask> getTasks() {
return new HashMap<>(tasks);
}
@Override
public void onApplicationEvent(ContextClosedEvent event) {
log.info("应用关闭,清理缓存预热资源");
taskExecutor.shutdown();
}
}
7. 预热任务执行器
@Component
@Slf4j
public class WarmupTaskExecutor {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private WarmupProgressTracker progressTracker;
@Autowired
private WarmupMetrics warmupMetrics;
private ExecutorService executorService;
@PostConstruct
public void init() {
int maxConcurrent = Runtime.getRuntime().availableProcessors();
executorService = Executors.newFixedThreadPool(maxConcurrent,
new ThreadFactoryBuilder().setNameFormat("warmup-pool-%d").build());
}
public CompletableFuture<Void> execute(WarmupTask task) {
return CompletableFuture.runAsync(() -> executeSync(task), executorService);
}
public void executeSync(WarmupTask task) {
log.info("开始执行预热任务: {}", task.getName());
task.setStatus(WarmupTask.WarmupStatus.RUNNING);
task.setStartTime(System.currentTimeMillis());
try {
DataLoader<?> loader = getDataLoader(task.getLoader());
if (loader == null) {
throw new IllegalArgumentException("找不到数据加载器: " + task.getLoader());
}
// 获取总数量
int totalCount = loader.getTotalCount();
task.getTotalCount().set(totalCount);
log.info("任务 [{}] 需要加载 {} 条数据", task.getName(), totalCount);
// 批量加载
int batchSize = task.getBatchSize();
int offset = 0;
while (offset < totalCount) {
List<?> batch = loader.loadBatch(offset, batchSize);
if (batch.isEmpty()) {
break;
}
// 写入本地缓存
if (task.isLocalCache()) {
loadToLocalCache(loader, batch);
}
// 写入 Redis
if (task.isRedisCache()) {
loadToRedis(loader, batch);
}
task.getLoadedCount().addAndGet(batch.size());
offset += batch.size();
// 更新进度
progressTracker.updateProgress(task);
// 记录指标
warmupMetrics.recordBatchLoaded(task.getName(), batch.size());
}
task.setStatus(WarmupTask.WarmupStatus.COMPLETED);
task.setEndTime(System.currentTimeMillis());
log.info("预热任务 [{}] 完成,加载 {} 条数据,耗时: {}ms",
task.getName(), task.getLoadedCount().get(), task.getDuration());
warmupMetrics.recordTaskCompleted(task);
} catch (Exception e) {
task.setStatus(WarmupTask.WarmupStatus.FAILED);
task.setErrorMessage(e.getMessage());
task.setEndTime(System.currentTimeMillis());
log.error("预热任务 [{}] 失败", task.getName(), e);
warmupMetrics.recordTaskFailed(task);
throw new RuntimeException("预热任务执行失败: " + task.getName(), e);
}
}
@SuppressWarnings("unchecked")
private DataLoader<Object> getDataLoader(String name) {
try {
return (DataLoader<Object>) applicationContext.getBean(name);
} catch (Exception e) {
log.error("获取数据加载器失败: {}", name, e);
return null;
}
}
private void loadToLocalCache(DataLoader<Object> loader, List<Object> batch) {
// 具体实现由各个 Loader 提供
if (loader instanceof ProductDataLoader) {
((ProductDataLoader) loader).loadToLocalCache(
batch.stream().map(o -> (Product) o).collect(Collectors.toList()));
} else if (loader instanceof UserDataLoader) {
((UserDataLoader) loader).loadToLocalCache(
batch.stream().map(o -> (User) o).collect(Collectors.toList()));
}
}
private void loadToRedis(DataLoader<Object> loader, List<Object> batch) {
if (loader instanceof ProductDataLoader) {
((ProductDataLoader) loader).loadToRedis(
batch.stream().map(o -> (Product) o).collect(Collectors.toList()));
} else if (loader instanceof UserDataLoader) {
((UserDataLoader) loader).loadToRedis(
batch.stream().map(o -> (User) o).collect(Collectors.toList()));
}
}
public void shutdown() {
if (executorService != null) {
executorService.shutdown();
try {
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}
}
}
}
8. 预热进度追踪器
@Component
@Slf4j
public class WarmupProgressTracker {
private final Map<String, WarmupTask> tasks = new ConcurrentHashMap<>();
public void registerTask(WarmupTask task) {
tasks.put(task.getName(), task);
}
public void updateProgress(WarmupTask task) {
double progress = task.getProgress();
log.debug("任务 [{}] 进度: {:.2f}% ({}/{})",
task.getName(), progress,
task.getLoadedCount().get(),
task.getTotalCount().get());
}
public WarmupProgress getOverallProgress() {
int totalTasks = tasks.size();
int completedTasks = 0;
int failedTasks = 0;
double totalProgress = 0;
for (WarmupTask task : tasks.values()) {
if (task.getStatus() == WarmupTask.WarmupStatus.COMPLETED) {
completedTasks++;
} else if (task.getStatus() == WarmupTask.WarmupStatus.FAILED) {
failedTasks++;
}
totalProgress += task.getProgress();
}
return WarmupProgress.builder()
.totalTasks(totalTasks)
.completedTasks(completedTasks)
.failedTasks(failedTasks)
.overallProgress(totalTasks > 0 ? totalProgress / totalTasks : 0)
.tasks(new ArrayList<>(tasks.values()))
.build();
}
public WarmupTask getTask(String name) {
return tasks.get(name);
}
@Data
@Builder
public static class WarmupProgress {
private int totalTasks;
private int completedTasks;
private int failedTasks;
private double overallProgress;
private List<WarmupTask> tasks;
public boolean isAllCompleted() {
return completedTasks == totalTasks;
}
public boolean hasFailed() {
return failedTasks > 0;
}
}
}
9. 预热指标监控
@Component
@Slf4j
public class WarmupMetrics {
private final MeterRegistry meterRegistry;
private final Counter taskCompletedCounter;
private final Counter taskFailedCounter;
private final Counter dataLoadedCounter;
private final DistributionSummary taskDurationSummary;
public WarmupMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.taskCompletedCounter = Counter.builder("cache.warmup.task.completed")
.description("已完成的预热任务数")
.register(meterRegistry);
this.taskFailedCounter = Counter.builder("cache.warmup.task.failed")
.description("失败的预热任务数")
.register(meterRegistry);
this.dataLoadedCounter = Counter.builder("cache.warmup.data.loaded")
.description("已加载的数据条数")
.register(meterRegistry);
this.taskDurationSummary = DistributionSummary.builder("cache.warmup.task.duration")
.description("预热任务执行时间")
.baseUnit("milliseconds")
.register(meterRegistry);
}
public void recordTaskCompleted(WarmupTask task) {
taskCompletedCounter.increment();
taskDurationSummary.record(task.getDuration());
meterRegistry.gauge("cache.warmup.task.progress",
Tags.of("task", task.getName()),
task, t -> t.getProgress());
}
public void recordTaskFailed(WarmupTask task) {
taskFailedCounter.increment();
}
public void recordBatchLoaded(String taskName, int count) {
dataLoadedCounter.increment(count);
meterRegistry.counter("cache.warmup.batch.loaded",
"task", taskName).increment(count);
}
}
10. 健康检查指示器
@Component
public class WarmupHealthIndicator implements HealthIndicator {
@Autowired
private CacheWarmupManager warmupManager;
@Autowired
private WarmupProgressTracker progressTracker;
@Override
public Health health() {
WarmupProgressTracker.WarmupProgress progress = progressTracker.getOverallProgress();
if (progress.getTotalTasks() == 0) {
return Health.up()
.withDetail("message", "缓存预热未配置")
.build();
}
if (progress.hasFailed()) {
return Health.down()
.withDetail("failedTasks", progress.getFailedTasks())
.withDetail("message", "部分预热任务失败")
.build();
}
if (progress.isAllCompleted()) {
return Health.up()
.withDetail("completedTasks", progress.getCompletedTasks())
.withDetail("progress", String.format("%.2f%%", progress.getOverallProgress()))
.withDetail("message", "缓存预热完成")
.build();
}
return Health.status("WARMING_UP")
.withDetail("completedTasks", progress.getCompletedTasks())
.withDetail("totalTasks", progress.getTotalTasks())
.withDetail("progress", String.format("%.2f%%", progress.getOverallProgress()))
.withDetail("message", "缓存预热进行中")
.build();
}
}
11. 预热管理接口
@RestController
@RequestMapping("/api/warmup")
@Slf4j
public class WarmupController {
@Autowired
private CacheWarmupManager warmupManager;
@Autowired
private WarmupProgressTracker progressTracker;
@GetMapping("/status")
public ApiResponse<Map<String, Object>> getStatus() {
Map<String, Object> status = new HashMap<>();
status.put("warmupCompleted", warmupManager.isWarmupCompleted());
status.put("progress", progressTracker.getOverallProgress());
return ApiResponse.success(status);
}
@GetMapping("/tasks")
public ApiResponse<List<WarmupTask>> getTasks() {
return ApiResponse.success(new ArrayList<>(warmupManager.getTasks().values()));
}
@GetMapping("/tasks/{name}")
public ApiResponse<WarmupTask> getTask(@PathVariable String name) {
WarmupTask task = progressTracker.getTask(name);
if (task == null) {
return ApiResponse.error("任务不存在: " + name);
}
return ApiResponse.success(task);
}
@PostMapping("/retry/{name}")
public ApiResponse<String> retryTask(@PathVariable String name) {
// 实现重试逻辑
return ApiResponse.success("任务重试已提交: " + name);
}
}
四、高级功能
1. 增量预热
@Component
@Slf4j
public class IncrementalWarmupService {
@Autowired
private CacheWarmupManager warmupManager;
@Scheduled(cron = "0 0 */6 * * ?") // 每6小时执行一次
public void incrementalWarmup() {
log.info("开始执行增量预热");
// 获取最近更新的数据
LocalDateTime lastWarmupTime = getLastWarmupTime();
// 增量加载商品数据
warmupProducts(lastWarmupTime);
// 增量加载用户数据
warmupUsers(lastWarmupTime);
updateLastWarmupTime();
log.info("增量预热完成");
}
private void warmupProducts(LocalDateTime since) {
// 加载最近更新的商品
// productRepository.findByUpdateTimeAfter(since);
}
private void warmupUsers(LocalDateTime since) {
// 加载最近更新的用户
// userRepository.findByUpdateTimeAfter(since);
}
private LocalDateTime getLastWarmupTime() {
// 从缓存或数据库获取上次预热时间
return LocalDateTime.now().minusHours(6);
}
private void updateLastWarmupTime() {
// 更新上次预热时间
}
}
2. 预热熔断保护
@Component
@Slf4j
public class WarmupCircuitBreaker {
private final Map<String, CircuitBreaker> breakers = new ConcurrentHashMap<>();
public <T> T executeWithBreaker(String taskName, Supplier<T> supplier) {
CircuitBreaker breaker = breakers.computeIfAbsent(taskName,
k -> new CircuitBreaker(5, 60000)); // 5次失败,60秒冷却
if (breaker.isOpen()) {
log.warn("预热任务 [{}] 熔断器已打开,跳过执行", taskName);
return null;
}
try {
T result = supplier.get();
breaker.recordSuccess();
return result;
} catch (Exception e) {
breaker.recordFailure();
throw e;
}
}
private static class CircuitBreaker {
private final int failureThreshold;
private final long cooldownMillis;
private final AtomicInteger failureCount = new AtomicInteger(0);
private volatile long lastFailureTime = 0;
public CircuitBreaker(int failureThreshold, long cooldownMillis) {
this.failureThreshold = failureThreshold;
this.cooldownMillis = cooldownMillis;
}
public boolean isOpen() {
if (failureCount.get() >= failureThreshold) {
long elapsed = System.currentTimeMillis() - lastFailureTime;
if (elapsed < cooldownMillis) {
return true;
} else {
failureCount.set(0);
return false;
}
}
return false;
}
public void recordSuccess() {
failureCount.set(0);
}
public void recordFailure() {
failureCount.incrementAndGet();
lastFailureTime = System.currentTimeMillis();
}
}
}
五、最佳实践
1. 预热数据选择
┌─────────────────────────────────────────────────────────────┐
│ 预热数据选择策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 热点数据识别 │
│ - 基于历史访问频率 │
│ - 基于业务重要性(如:热销商品) │
│ - 基于访问模式(如:首页数据) │
│ │
│ 2. 数据分级策略 │
│ ┌──────────────┬──────────────┬──────────────┐ │
│ │ P0-核心 │ P1-重要 │ P2-普通 │ │
│ ├──────────────┼──────────────┼──────────────┤ │
│ │ 必须预热 │ 建议预热 │ 按需预热 │ │
│ │ 同步加载 │ 异步加载 │ 延迟加载 │ │
│ │ 双缓存 │ 分布式缓存 │ 本地缓存 │ │
│ └──────────────┴──────────────┴──────────────┘ │
│ │
│ 3. 数据量控制 │
│ - 本地缓存:不超过内存的 20% │
│ - Redis:根据内存大小合理设置 │
│ - 使用 LRU/LFU 淘汰策略 │
│ │
└─────────────────────────────────────────────────────────────┘
2. 预热时机选择
| 场景 | 预热时机 | 说明 |
|---|---|---|
| 日常发布 | 启动时预热 | 标准流程 |
| 大促前 | 提前 1 小时手动触发 | 确保缓存充足 |
| 故障恢复 | 启动时 + 增量预热 | 快速恢复服务 |
| 扩容场景 | 启动时预热 | 新节点快速融入 |
3. 监控指标
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| 预热完成时间 | 从开始到全部完成 | > 5 分钟 |
| 预热成功率 | 成功任务 / 总任务 | < 95% |
| 数据加载速率 | 条/秒 | < 1000 |
| 缓存命中率 | 预热后的命中率 | < 80% |
六、常见问题
Q1: 预热太慢影响启动时间怎么办?
A:
- 使用异步预热,不阻塞启动流程
- 调整并发度,增加并行加载
- 优化数据加载 SQL,添加合适的索引
- 考虑分级预热,优先加载核心数据
Q2: 预热失败如何处理?
A:
- 设置
continue-on-error: true允许部分失败 - 实现重试机制,自动重试失败任务
- 记录失败日志,人工介入处理
- 服务启动后执行增量预热补偿
Q3: 如何验证预热效果?
A:
- 查看
/actuator/health端点确认预热状态 - 调用
/api/warmup/status查看详细进度 - 监控缓存命中率指标
- 进行压力测试对比预热前后性能
更多技术文章,欢迎关注公众号"服务端技术精选",及时获取最新动态。
标题:SpringBoot + 缓存预热 + 启动时加载:服务启动即预热热点数据,避免冷启动抖动
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/29/1774589996031.html
公众号:服务端技术精选
- 前言
- 一、冷启动问题分析
- 1. 什么是冷启动问题
- 2. 冷启动的影响
- 3. 缓存预热的价值
- 二、整体架构设计
- 1. 系统架构
- 2. 核心组件
- 3. 预热流程
- 三、代码实现
- 1. 项目结构
- 2. 预热配置
- 3. 预热任务定义
- 4. 数据加载器接口
- 5. 商品数据加载器
- 6. 缓存预热管理器
- 7. 预热任务执行器
- 8. 预热进度追踪器
- 9. 预热指标监控
- 10. 健康检查指示器
- 11. 预热管理接口
- 四、高级功能
- 1. 增量预热
- 2. 预热熔断保护
- 五、最佳实践
- 1. 预热数据选择
- 2. 预热时机选择
- 3. 监控指标
- 六、常见问题
- Q1: 预热太慢影响启动时间怎么办?
- Q2: 预热失败如何处理?
- Q3: 如何验证预热效果?
评论
0 评论