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 / 数据库
依赖分析器分析数据加载依赖关系,生成 DAGJGraphT
任务调度器异步调度预热任务,控制并发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
公众号:服务端技术精选
    评论
    0 评论
avatar

取消