SpringBoot3 缓存抽象深度实践:Caffeine+Redis多级缓存,穿透/雪崩/击穿防御全方案

在高并发后端系统中,缓存是提升性能、降低数据库压力的核心手段。但单一缓存方案往往难以兼顾性能与可靠性:本地缓存(如Caffeine)速度快但无法跨节点共享,分布式缓存(如Redis)可共享但存在网络开销,且若缓存使用不当,还会引发穿透、雪崩、击穿等致命问题。

SpringBoot3 对缓存抽象进行了进一步优化,简化了多缓存整合的复杂度,本文将基于 SpringBoot3 缓存抽象,实现 Caffeine(本地)+ Redis(分布式)多级缓存架构,同时提供缓存穿透、雪崩、击穿的全维度防御方案,结合实际业务场景拆解实操细节,助力开发者搭建高性能、高可靠的缓存体系。

一、核心认知:SpringBoot3 缓存抽象与多级缓存设计理念

1.1 SpringBoot3 缓存抽象核心优势

SpringBoot3 基于 Spring Framework 6,其缓存抽象(Spring Cache)的核心价值在于"解耦"------开发者无需关注具体缓存实现(Caffeine、Redis、Ehcache 等),只需通过注解(如 @Cacheable、@CachePut、@CacheEvict)即可完成缓存操作,底层缓存实现可灵活切换。

相较于 SpringBoot2,SpringBoot3 的缓存抽象有两个关键优化,更适配高并发场景:

  • 支持 JDK 17+,结合虚拟线程特性,缓存操作的并发性能进一步提升;

  • 优化了缓存管理器的初始化逻辑,支持多缓存管理器联动,完美适配多级缓存架构;

  • 增强了缓存注解的灵活性,支持自定义缓存 key 生成策略、缓存条件、过期时间等,减少重复编码。

1.2 多级缓存设计:Caffeine + Redis 选型逻辑

多级缓存的核心思路是"就近原则",优先从速度更快的缓存中获取数据,降低网络开销和响应时间,同时通过分布式缓存保证集群环境下的缓存一致性。选择 Caffeine + Redis 组合,核心原因如下:

  1. Caffeine(本地缓存):基于 Google Guava Cache 优化而来,是目前性能最优的本地缓存框架之一,支持自动过期、内存淘汰(LRU 变体算法),并发性能极强(每秒可处理百万级请求),适合存储热点数据,减少 Redis 访问压力;

  2. Redis(分布式缓存):高性能、高可用的分布式缓存中间件,支持多种数据结构,可跨服务、跨节点共享缓存,适合存储非热点但需共享的数据,同时可通过持久化(RDB/AOF)避免缓存丢失;

  3. 协同逻辑:查询数据时,先查 Caffeine 本地缓存,命中则直接返回;未命中则查 Redis 分布式缓存,命中后将数据同步到 Caffeine 本地缓存,再返回;更新/删除数据时,先操作数据库,再同步删除/更新 Redis 和 Caffeine 缓存,避免缓存脏读。

二、环境搭建:SpringBoot3 整合 Caffeine + Redis

本节将从依赖引入、配置文件、缓存管理器配置三个层面,完成 SpringBoot3 与 Caffeine、Redis 的整合,搭建基础的多级缓存环境。

2.1 引入核心依赖

在 pom.xml 中引入 SpringBoot3 缓存 starter、Caffeine、Redis 相关依赖(基于 Maven),确保版本兼容(SpringBoot3 建议搭配 Redis 6.2+、Caffeine 3.0+):

复制代码
<!-- SpringBoot3 缓存抽象 starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Redis 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Caffeine 依赖 -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>
<!-- SpringBoot Web(模拟业务接口) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok(简化代码) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

2.2 配置文件编写(application.yml)

配置 Redis 连接信息、Caffeine 本地缓存参数,以及 Spring Cache 相关配置,明确缓存类型和过期时间:

复制代码
spring:
  # 缓存核心配置
  cache:
    # 启用缓存
    type: CAFFEINE,REDIS
    # 缓存名称(可自定义,对应不同缓存场景)
    cache-names: userCache,productCache
    # Caffeine 本地缓存配置
    caffeine:
      spec: initialCapacity=100,maximumSize=1000,expireAfterWrite=5m
      # initialCapacity:初始缓存容量
      # maximumSize:最大缓存容量(超过则触发 LRU 淘汰)
      # expireAfterWrite:写入后过期时间(5分钟,可根据业务调整)
    # Redis 分布式缓存配置
    redis:
      # Redis 连接地址(集群环境可配置 cluster.nodes)
      host: localhost
      port: 6379
      password: 123456
      database: 0
      # 缓存过期时间(全局配置,可被具体缓存覆盖)
      time-to-live: 30m
      # 缓存 key 前缀(避免与其他系统冲突)
      key-prefix: springboot3:cache:
      # 是否缓存空值(用于防御缓存穿透,下文详解)
      cache-null-values: true
      # Redis 序列化方式(避免默认序列化乱码)
      serialization:
        key:
          type: string
        value:
          type: generic
# 日志配置(便于调试缓存流程)
logging:
  level:
    org.springframework.cache: DEBUG
    com.example.cache: DEBUG

2.3 缓存管理器配置(核心)

SpringBoot3 默认会自动配置 CaffeineCacheManager 和 RedisCacheManager,但要实现"多级缓存联动"(先查 Caffeine,再查 Redis),需要自定义缓存管理器,整合两者为多级缓存管理器。

核心思路:自定义 CacheManager,将 CaffeineCacheManager 作为一级缓存,RedisCacheManager 作为二级缓存,重写 getCache 方法,实现多级缓存的查询、同步逻辑。

复制代码
package com.example.cache.config;

import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cache.support.CompositeCacheManager;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;

import java.util.ArrayList;
import java.util.List;

/**
 * SpringBoot3 多级缓存管理器配置(Caffeine + Redis)
 */
@Configuration
@RequiredArgsConstructor
public class MultiLevelCacheConfig {

    // 自动注入 SpringBoot 自动配置的 Caffeine 缓存管理器
    private final CaffeineCacheManager caffeineCacheManager;
    // 自动注入 SpringBoot 自动配置的 Redis 缓存管理器
    private final RedisCacheManager redisCacheManager;

    /**
     * 自定义多级缓存管理器:先查 Caffeine(本地),再查 Redis(分布式)
     */
    @Bean
    public CacheManager multiLevelCacheManager() {
        // 1. 构建复合缓存管理器(支持多缓存管理器联动)
        CompositeCacheManager compositeCacheManager = new CompositeCacheManager();

        // 2. 配置缓存优先级:Caffeine(一级) -> Redis(二级)
        List<CacheManager> cacheManagers = new ArrayList<>();
        cacheManagers.add(caffeineCacheManager);
        cacheManagers.add(redisCacheManager);

        // 3. 设置缓存管理器列表(顺序决定查询优先级)
        compositeCacheManager.setCacheManagers(cacheManagers);

        // 4. 配置默认缓存管理器(当指定缓存名称不存在时,使用 Redis 作为默认缓存)
        compositeCacheManager.setFallbackToNoOpCache(false);
        compositeCacheManager.setCacheManagerCacheResolver((name) -> {
            Cache caffeineCache = caffeineCacheManager.getCache(name);
            Cache redisCache = redisCacheManager.getCache(name);
            if (caffeineCache != null && redisCache != null) {
                // 自定义缓存实现,实现多级缓存的同步逻辑(查询、更新、删除)
                return new MultiLevelCache(name, caffeineCache, redisCache);
            }
            return redisCache != null ? redisCache : caffeineCache;
        });

        return compositeCacheManager;
    }

    /**
     * 自定义多级缓存实现:封装 Caffeine 和 Redis 的缓存操作,实现联动
     */
    static class MultiLevelCache implements Cache {

        private final String name;
        private final Cache caffeineCache;
        private final Cache redisCache;

        public MultiLevelCache(String name, Cache caffeineCache, Cache redisCache) {
            this.name = name;
            this.caffeineCache = caffeineCache;
            this.redisCache = redisCache;
        }

        @Override
        public String getName() {
            return this.name;
        }

        @Override
        public Object getNativeCache() {
            return this;
        }

        /**
         * 核心查询逻辑:先查 Caffeine,未命中则查 Redis,Redis 命中后同步到 Caffeine
         */
        @Override
        public ValueWrapper get(Object key) {
            // 1. 先查询本地 Caffeine 缓存
            ValueWrapper caffeineValue = caffeineCache.get(key);
            if (caffeineValue != null) {
                return caffeineValue;
            }

            // 2. Caffeine 未命中,查询 Redis 缓存
            ValueWrapper redisValue = redisCache.get(key);
            if (redisValue != null) {
                // 3. Redis 命中,将数据同步到 Caffeine 本地缓存(提升下次查询性能)
                caffeineCache.put(key, redisValue.get());
            }

            return redisValue;
        }

        @Override
        public <T> T get(Object key, Class<T> type) {
            // 1. 先查 Caffeine
            T caffeineValue = caffeineCache.get(key, type);
            if (caffeineValue != null) {
                return caffeineValue;
            }

            // 2. 再查 Redis,命中后同步到 Caffeine
            T redisValue = redisCache.get(key, type);
            if (redisValue != null) {
                caffeineCache.put(key, redisValue);
            }

            return redisValue;
        }

        @Override
        public <T> T get(Object key, Callable<T> valueLoader) {
            try {
                // 先查 Caffeine,未命中则执行 valueLoader(查询数据库),执行完成后自动同步到 Caffeine 和 Redis
                T value = caffeineCache.get(key, valueLoader);
                redisCache.put(key, value);
                return value;
            } catch (Exception e) {
                // 若 valueLoader 执行失败(如数据库异常),查询 Redis 缓存兜底
                return redisCache.get(key, valueLoader);
            }
        }

        /**
         * 缓存写入:同时写入 Caffeine 和 Redis,保证缓存一致性
         */
        @Override
        public void put(Object key, Object value) {
            caffeineCache.put(key, value);
            redisCache.put(key, value);
        }

        /**
         * 缓存删除:同时删除 Caffeine 和 Redis 中的数据,避免脏读
         */
        @Override
        public void evict(Object key) {
            caffeineCache.evict(key);
            redisCache.evict(key);
        }

        /**
         * 清空缓存:同时清空 Caffeine 和 Redis 中的对应缓存
         */
        @Override
        public void clear() {
            caffeineCache.clear();
            redisCache.clear();
        }
    }
}

2.4 启用缓存

在 SpringBoot 启动类上添加 @EnableCaching 注解,启用缓存抽象功能:

复制代码
package com.example.cache;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching // 启用 Spring 缓存抽象
public class SpringBoot3CachePracticeApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBoot3CachePracticeApplication.class, args);
    }

}

三、核心实践:多级缓存业务落地(附完整代码)

本节将基于上述配置,结合用户查询业务场景,实现多级缓存的实际使用,演示 @Cacheable、@CachePut、@CacheEvict 注解的用法,同时验证多级缓存的联动效果。

3.1 业务场景定义

模拟用户查询业务:

  • 根据用户 ID 查询用户信息(热点场景,适合多级缓存);

  • 新增/修改用户信息(需同步更新缓存);

  • 删除用户信息(需同步删除缓存);

  • 查询不存在的用户(触发缓存穿透场景,用于后续测试防御方案)。

3.2 实体类与数据层

复制代码
// 用户实体类
package com.example.cache.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
    // 序列化版本号,避免 Redis 序列化异常
    private static final long serialVersionUID = 1L;
    private Long id;
    private String username;
    private String phone;
    private Integer age;
}

// 数据层(模拟数据库操作,实际开发中替换为 MyBatis-Plus/SpringDataJPA)
package com.example.cache.mapper;

import com.example.cache.entity.User;
import org.springframework.stereotype.Repository;
import java.util.HashMap;
import java.util.Map;

@Repository
public class UserMapper {

    // 模拟数据库(内存Map)
    private static final Map<Long, User> USER_DB = new HashMap<>();

    static {
        // 初始化测试数据
        USER_DB.put(1L, new User(1L, "zhangsan", "13800138000", 25));
        USER_DB.put(2L, new User(2L, "lisi", "13800138001", 28));
        USER_DB.put(3L, new User(3L, "wangwu", "13800138002", 30));
    }

    // 根据ID查询用户
    public User selectById(Long id) {
        // 模拟数据库查询耗时(100ms)
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return USER_DB.get(id);
    }

    // 新增用户
    public void insert(User user) {
        USER_DB.put(user.getId(), user);
    }

    // 更新用户
    public void update(User user) {
        USER_DB.put(user.getId(), user);
    }

    // 删除用户
    public void delete(Long id) {
        USER_DB.remove(id);
    }
}

3.3 服务层(缓存核心使用)

在 Service 层使用 Spring Cache 注解,结合多级缓存,实现缓存的查询、更新、删除操作,重点关注缓存 key 生成、缓存条件、过期时间等细节。

复制代码
package com.example.cache.service;

import com.example.cache.entity.User;
import com.example.cache.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserMapper userMapper;

    /**
     * 根据ID查询用户:使用多级缓存
     * @Cacheable:查询缓存,未命中则执行方法(查询数据库),并将结果存入缓存
     *  key:缓存key,格式为 "user:id"(如 user:1)
     *  cacheNames:指定缓存名称(对应配置文件中的 userCache)
     *  unless:缓存条件,当返回值为 null 时不缓存(结合配置文件的 cache-null-values 防御穿透)
     */
    @Cacheable(key = "'user:' + #id", cacheNames = "userCache", unless = "#result == null")
    public User getUserById(Long id) {
        System.out.println("查询数据库,id:" + id);
        return userMapper.selectById(id);
    }

    /**
     * 新增用户:同步更新缓存
     * @CachePut:执行方法后,将结果存入缓存(用于新增/更新场景,保证缓存与数据库一致)
     */
    @CachePut(key = "'user:' + #user.id", cacheNames = "userCache")
    public User addUser(User user) {
        userMapper.insert(user);
        return user;
    }

    /**
     * 更新用户:同步更新缓存
     */
    @CachePut(key = "'user:' + #user.id", cacheNames = "userCache")
    public User updateUser(User user) {
        userMapper.update(user);
        return user;
    }

    /**
     * 删除用户:同步删除缓存
     * @CacheEvict:执行方法后,删除指定缓存
     */
    @CacheEvict(key = "'user:' + #id", cacheNames = "userCache")
    public void deleteUser(Long id) {
        userMapper.delete(id);
    }

    /**
     * 清空用户缓存(用于批量操作后)
     * allEntries = true:清空当前缓存名称下的所有缓存
     */
    @CacheEvict(cacheNames = "userCache", allEntries = true)
    public void clearUserCache() {
        // 方法体可空,仅用于触发缓存清空
    }
}

3.4 控制层(接口测试)

复制代码
package com.example.cache.controller;

import com.example.cache.entity.User;
import com.example.cache.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    // 查询用户
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        long start = System.currentTimeMillis();
        User user = userService.getUserById(id);
        long end = System.currentTimeMillis();
        System.out.println("查询耗时:" + (end - start) + "ms");
        return user;
    }

    // 新增用户
    @PostMapping
    public User addUser(@RequestBody User user) {
        return userService.addUser(user);
    }

    // 更新用户
    @PutMapping
    public User updateUser(@RequestBody User user) {
        return userService.updateUser(user);
    }

    // 删除用户
    @DeleteMapping("/{id}")
    public String deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return "删除成功";
    }

    // 清空缓存
    @DeleteMapping("/cache/clear")
    public String clearCache() {
        userService.clearUserCache();
        return "缓存清空成功";
    }
}

3.5 实践验证

启动项目后,通过 Postman 或浏览器访问接口,验证多级缓存效果:

  1. 第一次访问 /user/1:控制台输出"查询数据库,id:1",耗时约 100ms(数据库查询耗时);

  2. 第二次访问 /user/1:控制台无数据库查询日志,耗时约 1ms(Caffeine 本地缓存命中);

  3. 重启项目(Caffeine 缓存清空),访问 /user/1:控制台无数据库查询日志,耗时约 5ms(Redis 缓存命中,同步到 Caffeine);

  4. 调用 /user 新增用户(id=4),再访问 /user/4:直接命中缓存,无数据库查询;

  5. 调用 /user/4 删除用户,再访问 /user/4:触发数据库查询(缓存已删除)。

验证结果表明,多级缓存已正常工作,实现了"本地缓存优先、分布式缓存兜底"的预期效果,大幅提升了查询性能。

四、核心防御:缓存穿透、雪崩、击穿全方案落地

即使搭建了多级缓存,若不做防御措施,依然会面临缓存穿透、雪崩、击穿三大问题,这三大问题会直接导致数据库压力剧增,甚至引发系统雪崩。本节将针对每个问题,讲解其原理、危害,并结合 SpringBoot3 缓存抽象,给出可落地的防御方案,整合到多级缓存架构中。

4.1 缓存穿透:查询不存在的数据

4.1.1 问题原理与危害

缓存穿透是指:用户查询的数据在缓存中不存在,也在数据库中不存在,导致每次查询都会穿透缓存,直接访问数据库。若恶意攻击者频繁查询不存在的 ID(如 -1、999999),会导致数据库压力剧增,甚至宕机。

4.1.2 防御方案(双重防御)

结合 SpringBoot3 缓存抽象,采用"空值缓存 + 布隆过滤器"双重防御方案,兼顾易用性和性能:

方案1:空值缓存(基础防御)

核心思路:当查询数据库返回 null 时,将 null 值存入缓存(设置较短的过期时间,如 5 分钟),下次查询该 key 时,直接返回 null,避免穿透到数据库。

实现方式:在 application.yml 中配置 spring.cache.redis.cache-null-values: true,同时在 @Cacheable 注解中调整 unless 条件(允许缓存 null 值):

复制代码
// 调整 @Cacheable 注解,允许缓存 null 值(unless 条件改为 #result == null ? false : true,即始终缓存)
@Cacheable(key = "'user:' + #id", cacheNames = "userCache", unless = "#result == null ? false : true")
public User getUserById(Long id) {
    System.out.println("查询数据库,id:" + id);
    return userMapper.selectById(id);
}

注意:空值缓存会占用一定的缓存空间,需合理设置过期时间,避免缓存膨胀。

方案2:布隆过滤器(高级防御)

核心思路:布隆过滤器是一种空间高效的数据结构,用于判断一个元素是否在一个集合中(存在误判率,可调整)。将数据库中所有存在的 key(如用户 ID)存入布隆过滤器,查询时先通过布隆过滤器判断 key 是否存在,若不存在,直接返回 null,无需查询缓存和数据库。

实现方式:使用 Redisson 整合布隆过滤器(Redis 分布式布隆过滤器,支持集群环境),整合到多级缓存查询流程中:

复制代码
// 1. 引入 Redisson 依赖(pom.xml)
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.3</version>
</dependency>

// 2. 布隆过滤器配置
package com.example.cache.config;

import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.cache.mapper.UserMapper;
import com.example.cache.entity.User;

import java.util.Map;

@Configuration
public class BloomFilterConfig {

    // 布隆过滤器预计存储数量(根据实际业务调整)
    private static final long EXPECTED_INSERTIONS = 100000;
    // 布隆过滤器误判率(默认 0.03,误判率越低,占用空间越大)
    private static final double FALSE_POSITIVE_RATE = 0.03;

    /**
     * 初始化用户 ID 布隆过滤器,将数据库中所有用户 ID 存入
     */
    @Bean
    public RBloomFilter<Long> userBloomFilter(RedissonClient redissonClient, UserMapper userMapper) {
        // 获取布隆过滤器(名称:user:bloom:filter,不存在则创建)
        RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter("user:bloom:filter");
        // 初始化布隆过滤器(预计数量、误判率)
        bloomFilter.tryInit(EXPECTED_INSERTIONS, FALSE_POSITIVE_RATE);

        // 从数据库中查询所有用户 ID,存入布隆过滤器(实际开发中可异步初始化)
        Map<Long, User> userDB = userMapper.getUserDB(); // 新增方法,获取所有用户
        for (Long userId : userDB.keySet()) {
            bloomFilter.add(userId);
        }

        return bloomFilter;
    }
}

// 3. 改造 Service 层,添加布隆过滤器校验
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserMapper userMapper;
    private final RBloomFilter<Long> userBloomFilter;

    @Cacheable(key = "'user:' + #id", cacheNames = "userCache", unless = "#result == null ? false : true")
    public User getUserById(Long id) {
        // 布隆过滤器校验:若 ID 不存在,直接返回 null,避免穿透到数据库
        if (!userBloomFilter.contains(id)) {
            System.out.println("布隆过滤器拦截,ID:" + id + " 不存在");
            return null;
        }

        System.out.println("查询数据库,id:" + id);
        return userMapper.selectById(id);
    }

    // 新增用户时,同步将 ID 存入布隆过滤器
    @CachePut(key = "'user:' + #user.id", cacheNames = "userCache")
    public User addUser(User user) {
        userMapper.insert(user);
        userBloomFilter.add(user.getId()); // 同步添加到布隆过滤器
        return user;
    }

    // 删除用户时,布隆过滤器无法删除元素(特性限制),可设置较短的空值缓存兜底
}

说明:布隆过滤器无法删除元素,若业务中存在大量删除操作,可结合空值缓存兜底,或定期重建布隆过滤器(如每天凌晨重建一次)。

4.2 缓存雪崩:大量缓存同时过期

4.2.1 问题原理与危害

缓存雪崩是指:大量缓存 key 在同一时间过期,导致大量请求穿透到数据库,数据库压力剧增,进而引发系统雪崩(数据库宕机 → 服务不可用 → 更多请求失败)。

常见原因:缓存过期时间设置过于集中(如所有缓存都设置为 30 分钟)、Redis 集群宕机导致缓存整体失效。

4.2.2 防御方案(三重防御)

  1. 过期时间加随机值(分散过期时间) 核心思路:在统一的过期时间基础上,添加随机偏移量(如 0~5 分钟),避免大量缓存同时过期。实现方式:自定义缓存配置,为不同缓存 key 设置随机过期时间。

    复制代码
    @Configuration
    public class RedisCacheConfig {
    
        @Bean
        public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
            // Redis 缓存配置
            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofMinutes(30)) // 基础过期时间 30 分钟
                    .serializeKeysWith(RedisSerializationContext.SerializationPair
                            .fromSerializer(new StringRedisSerializer()))
                    .serializeValuesWith(RedisSerializationContext.SerializationPair
                            .fromSerializer(new GenericJackson2JsonRedisSerializer()))
                    .cacheNullValues(true)
                    .prefixCacheNameWith("springboot3:cache:");
    
            // 为不同缓存名称设置不同的过期时间(加随机值)
            Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
            // userCache:30~35 分钟过期
            cacheConfigurations.put("userCache", config.entryTtl(
                    Duration.ofMinutes(30 + new Random().nextInt(6))
            ));
            // productCache:60~65 分钟过期
            cacheConfigurations.put("productCache", config.entryTtl(
                    Duration.ofMinutes(60 + new Random().nextInt(6))
            ));
    
            // 构建 Redis 缓存管理器
            return RedisCacheManager.builder(connectionFactory)
                    .cacheDefaults(config)
                    .withInitialCacheConfigurations(cacheConfigurations)
                    .build();
        }
    }
  2. 缓存预热(提前加载热点缓存) 核心思路:系统启动时,提前将热点数据加载到 Caffeine 和 Redis 缓存中,避免启动后大量请求穿透到数据库。实现方式:使用 Spring 的 CommandLineRunner 接口,启动时执行缓存预热。

    复制代码
    package com.example.cache.init;
    
    import com.example.cache.entity.User;
    import com.example.cache.service.UserService;
    import lombok.RequiredArgsConstructor;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.stereotype.Component;
    
    import java.util.Arrays;
    import java.util.List;
    
    /**
     * 缓存预热:系统启动时,加载热点数据到缓存
     */
    @Component
    @RequiredArgsConstructor
    public class CacheWarmUp implements CommandLineRunner {
    
        private final UserService userService;
    
        @Override
        public void run(String... args) throws Exception {
            // 模拟热点用户 ID 列表(实际开发中可从数据库/配置中心获取)
            List<Long> hotUserIds = Arrays.asList(1L, 2L, 3L, 4L, 5L);
            // 预热缓存:调用查询方法,将数据加载到 Caffeine 和 Redis
            for (Long userId : hotUserIds) {
                userService.getUserById(userId);
            }
            System.out.println("缓存预热完成,加载热点用户数量:" + hotUserIds.size());
        }
    }
  3. **Redis 高可用 + 降级熔断(兜底保护)**核心思路:Redis 集群部署(主从 + 哨兵/集群模式),避免单点故障导致缓存整体失效;同时结合降级熔断组件(如 Resilience4j),当 Redis 宕机时,降级为本地 Caffeine 缓存,避免请求直接穿透到数据库。

    复制代码
    // 1. 引入 Resilience4j 依赖(pom.xml)
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-circuitbreaker-spring-boot3</artifactId>
        <version>2.1.0</version>
    </dependency>
    
    // 2. 改造 Service 层,添加降级熔断
    @Service
    @RequiredArgsConstructor
    public class UserService {
    
        private final UserMapper userMapper;
        private final RBloomFilter<Long> userBloomFilter;
        private final CaffeineCacheManager caffeineCacheManager;
    
        /**
         * @CircuitBreaker:熔断注解,当 Redis 宕机(抛出异常)时,执行 fallbackMethod 降级方法
         * name:熔断规则名称(对应配置文件)
         * fallbackMethod:降级方法名称
         */
        @CircuitBreaker(name = "redisCacheCircuitBreaker", fallbackMethod = "getUserByIdFallback")
        @Cacheable(key = "'user:' + #id", cacheNames = "userCache", unless = "#result == null ? false : true")
        public User getUserById(Long id) {
            if (!userBloomFilter.contains(id)) {
                System.out.println("布隆过滤器拦截,ID:" + id + " 不存在");
                return null;
            }
            System.out.println("查询数据库,id:" + id);
            return userMapper.selectById(id);
        }
    
        /**
         * 降级方法:Redis 宕机时,仅使用 Caffeine 本地缓存查询
         * 注意:方法参数、返回值必须与原方法一致
         */
        public User getUserByIdFallback(Long id, Exception e) {
            System.out.println("Redis 缓存宕机,执行降级策略,使用本地缓存查询,异常信息:" + e.getMessage());
            // 仅查询 Caffeine 本地缓存
            Cache caffeineCache = caffeineCacheManager.getCache("userCache");
            if (caffeineCache != null) {
                Cache.ValueWrapper valueWrapper = caffeineCache.get(id);
                if (valueWrapper != null) {
                    return (User) valueWrapper.get();
                }
            }
            // 本地缓存也未命中,返回 null(避免穿透到数据库)
            return null;
        }
    }
    
    // 3. 配置熔断规则(application.yml)
    resilience4j:
      circuitbreaker:
        instances:
          redisCacheCircuitBreaker:
            # 触发熔断的失败率阈值(50%)
            failure-rate-threshold: 50
            # 滑动窗口大小(100 个请求)
            sliding-window-size: 100
            # 熔断状态持续时间(10 秒,期间请求直接降级)
            wait-duration-in-open-state: 10000
            # 最小请求数(10 个请求后才开始计算失败率)
            minimum-number-of-calls: 10

4.3 缓存击穿:热点 key 过期

4.3.1 问题原理与危害

缓存击穿是指:某个热点 key(如热门商品、高频访问用户)过期时,大量请求同时访问该 key,导致请求穿透到数据库,数据库瞬间压力剧增(类似"单点雪崩")。

与缓存雪崩的区别:缓存雪崩是"大量 key 同时过期",缓存击穿是"单个热点 key 过期"。

4.3.2 防御方案(最优方案:逻辑过期 + 互斥锁)

结合多级缓存特性,采用"逻辑过期 + 互斥锁"方案,既避免了热点 key 过期后的穿透,又保证了并发性能:

方案1:逻辑过期(核心)

核心思路:不设置缓存 key 的物理过期时间(Redis 中 key 永不过期),而是在缓存 value 中添加逻辑过期时间字段,查询时判断逻辑过期时间,若已过期,异步更新缓存,避免大量请求同时穿透到数据库。

方案2:互斥锁(兜底)

核心思路:当热点 key 逻辑过期后,只有一个请求能获取到互斥锁,去查询数据库并更新缓存,其他请求等待或返回旧数据,避免大量请求同时访问数据库。

完整实现代码:

复制代码
// 1. 定义带逻辑过期时间的缓存实体
package com.example.cache.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 带逻辑过期时间的缓存实体
 * @param <T> 缓存数据类型
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CacheData<T> implements Serializable {
    // 缓存数据
    private T data;
    // 逻辑过期时间
    private LocalDateTime expireTime;
}

// 2. 改造 Service 层,实现逻辑过期 + 互斥锁
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserMapper userMapper;
    private final RBloomFilter<Long> userBloomFilter;
    private final RedisTemplate<String, Object> redisTemplate;
    private final CaffeineCacheManager caffeineCacheManager;

    // 互斥锁 key 前缀
    private static final String LOCK_KEY_PREFIX = "springboot3:cache:lock:";
    // 逻辑过期时间(热点用户:1小时)
    private static final Duration LOGIC_EXPIRE_TIME = Duration.ofHours(1);

    /**
     * 热点用户查询:逻辑过期 + 互斥锁防御缓存击穿
     */
    public User getHotUserById(Long id) {
        String cacheKey = "user:" + id;

        // 1. 布隆过滤器校验
        if (!userBloomFilter.contains(id)) {
            System.out.println("布隆过滤器拦截,ID:" + id + " 不存在");
            return null;
        }

        // 2. 先查 Caffeine 本地缓存
        Cache caffeineCache = caffeineCacheManager.getCache("userCache");
        Cache.ValueWrapper caffeineValue = caffeineCache.get(cacheKey);
        if (caffeineValue != null) {
            CacheData<User> cacheData = (CacheData<User>) caffeineValue.get();
            // 判断逻辑过期时间:未过期,直接返回
            if (!isExpired(cacheData.getExpireTime())) {
                return cacheData.getData();
            }
            // 已过期,异步更新缓存(不阻塞当前请求)
            asyncUpdateCache(id, cacheKey);
            return cacheData.getData(); // 返回旧数据,保证响应速度
        }

        // 3. Caffeine 未命中,查 Redis 缓存
        CacheData<User> redisCacheData = (CacheData<User>) redisTemplate.opsForValue().get(cacheKey);
        if (redisCacheData != null) {
            // 未过期,同步到 Caffeine 本地缓存,返回数据
            if (!isExpired(redisCacheData.getExpireTime())) {
                caffeineCache.put(cacheKey, redisCacheData);
                return redisCacheData.getData();
            }
            // 已过期,获取互斥锁,更新缓存
            if (tryLock(cacheKey)) {
                try {
                    // 双重校验:再次查询 Redis,避免重复更新
                    CacheData<User> doubleCheckData = (CacheData<User>) redisTemplate.opsForValue().get(cacheKey);
                    if (doubleCheckData != null && !isExpired(doubleCheckData.getExpireTime())) {
                        caffeineCache.put(cacheKey, doubleCheckData);
                        return doubleCheckData.getData();
                    }
                    // 查询数据库,更新缓存
                    User user = userMapper.selectById(id);
                    if (user != null) {
                        CacheData<User> newCacheData = new CacheData<>(user, LocalDateTime.now().plus(LOGIC_EXPIRE_TIME));
                        redisTemplate.opsForValue().set(cacheKey, newCacheData);
                        caffeineCache.put(cacheKey, newCacheData);
                        return user;
                    }
                } finally {
                    unlock(cacheKey); // 释放锁
                }
            } else {
                // 未获取到锁,返回旧数据(或阻塞等待)
                return redisCacheData.getData();
            }
        }

        // 4. Redis 未命中,查询数据库,更新缓存
        User user = userMapper.selectById(id);
        if (user != null) {
            CacheData<User> newCacheData = new CacheData<>(user, LocalDateTime.now().plus(LOGIC_EXPIRE_TIME));
            redisTemplate.opsForValue().set(cacheKey, newCacheData);
            caffeineCache.put(cacheKey, newCacheData);
            return user;
        }

        return null;
    }

    /**
     * 异步更新缓存(用于 Caffeine 缓存过期时,不阻塞请求)
     */
    @Async // 需在启动类添加 @EnableAsync 启用异步
    public void asyncUpdateCache(Long id, String cacheKey) {
        if (tryLock(cacheKey)) {
            try {
                // 双重校验
                CacheData<User> doubleCheckData = (CacheData<User>) redisTemplate.opsForValue().get(cacheKey);
                if (doubleCheckData != null && !isExpired(doubleCheckData.getExpireTime())) {
                    return;
                }
                User user = userMapper.selectById(id);
                if (user != null) {
                    CacheData<User> newCacheData = new CacheData<>(user, LocalDateTime.now().plus(LOGIC_EXPIRE_TIME));
                    redisTemplate.opsForValue().set(cacheKey, newCacheData);
                    caffeineCacheManager.getCache("userCache").put(cacheKey, newCacheData);
                }
            } finally {
                unlock(cacheKey);
            }
        }
    }

    /**
     * 判断缓存是否逻辑过期
     */
    private boolean isExpired(LocalDateTime expireTime) {
        return LocalDateTime.now().isAfter(expireTime);
    }

    /**
     * 获取互斥锁(Redis SET NX EX)
     */
    private boolean tryLock(String cacheKey) {
        String lockKey = LOCK_KEY_PREFIX + cacheKey;
        // SET NX EX:不存在则设置,过期时间 3 秒(避免死锁)
        Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(3));
        return Boolean.TRUE.equals(success);
    }

    /**
     * 释放互斥锁
     */
    private void unlock(String cacheKey) {
        String lockKey = LOCK_KEY_PREFIX + cacheKey;
        redisTemplate.delete(lockKey);
    }
}

// 3. 启动类添加 @EnableAsync 启用异步方法
@SpringBootApplication
@EnableCaching
@EnableAsync
public class SpringBoot3CachePracticeApplication {
    // ...
}

方案优势:逻辑过期避免了 key 物理过期导致的同时穿透,互斥锁保证了只有一个请求更新缓存,异步更新缓存不阻塞当前请求,兼顾了性能和可靠性。

4.3.3 方案关键补充(避坑指南)

结合上述代码,补充3个核心避坑点,避免实际开发中出现异常:

  • 死锁预防:代码中互斥锁设置了3秒过期时间(Duration.ofSeconds(3)),这是避免死锁的关键------即使获取锁的请求异常中断,锁也会自动过期释放,不会导致后续请求一直阻塞。实际开发中,锁过期时间需根据数据库查询耗时调整(建议比最长查询耗时多1~2秒)。

  • 异步方法启用:asyncUpdateCache方法添加了@Async注解,需确保启动类上添加@EnableAsync注解(代码中已提示),否则异步失效,会阻塞当前请求,失去异步更新的优势。

  • 序列化兼容:CacheData<T>类实现了Serializable接口,确保Redis序列化时不会报错。若使用Jackson序列化,需确保LocalDateTime类型的序列化配置正确(可添加Jackson注解或自定义序列化器),避免出现序列化异常。

  • 锁粒度控制:互斥锁的key是"lock:user:id",粒度为单个用户ID,而非全局锁,这样可避免不同热点key之间的锁竞争,提升并发性能------即使一个用户的缓存更新被锁,其他用户的查询请求不受影响。

五、实践优化:性能调优与监控

搭建完多级缓存和防御方案后,还需要进行性能调优和监控,确保缓存体系稳定高效运行。

5.1 性能调优

  1. Caffeine 本地缓存参数调优:Caffeine的性能依赖于合理的参数配置,需根据业务场景调整,避免内存浪费或缓存命中率过低。

    复制代码
    // 优化 Caffeine 配置(替换原配置文件中的caffeine.spec)
    // 核心参数说明(结合高并发场景)
    spring:
      cache:
        caffeine:
          spec: initialCapacity=200,maximumSize=2000,expireAfterWrite=5m,recordStats
          # initialCapacity:初始容量,根据热点数据初始数量设置(避免频繁扩容)
          # maximumSize:最大容量,结合JVM内存设置(建议不超过JVM堆内存的10%)
          # expireAfterWrite:写入后过期时间,热点数据可缩短(如5m),非热点可延长
          # recordStats:开启统计功能,用于监控缓存命中率(后续监控部分会用到)
  2. Redis 分布式缓存调优

    复制代码
    # Redis 优化配置(application.yml)
    spring:
      data:
        redis:
          # 连接池配置(关键优化,避免频繁创建/销毁连接)
          lettuce:
            pool:
              max-active: 100 # 最大连接数(根据并发量调整,如100~200)
              max-idle: 20 # 最大空闲连接
              min-idle: 5 # 最小空闲连接
              max-wait: 2000ms # 连接等待时间(超时则降级,避免阻塞)
          # 序列化优化(替换默认JDK序列化,提升性能和可读性)
          serializer:
            key:
              type: string
            value:
              type: json
          # 持久化优化(避免持久化影响Redis性能)
          # 生产环境建议:RDB+AOF混合持久化,AOF刷盘策略设为everysecond
          redis:
            rdb:
              enabled: true
            aof:
              enabled: true
              fsync: everysecond
  3. 缓存粒度控制:避免缓存过大或过小,提升缓存命中率和更新效率。 不缓存大对象:若需缓存复杂对象(如包含大量字段的实体),可拆分缓存(如用户基本信息、用户详情分开缓存),避免更新时全量更新缓存;

  4. 不缓存高频更新数据:如实时统计数据、高频修改的状态字段,这类数据缓存收益低,还会增加缓存与数据库的一致性维护成本;

  5. 合理复用缓存:对于相同查询条件的请求,确保缓存key唯一,避免重复缓存(可通过自定义key生成器统一规则)。

  6. 避免缓存滥用:并非所有接口都需要缓存,以下场景不建议使用缓存: 查询频率极低的接口(如后台管理的低频查询);

  7. 数据一致性要求极高的接口(如转账、支付接口,需实时查询数据库);

  8. 查询耗时极短的接口(如查询固定字典数据,耗时<1ms,缓存收益可忽略)。

5.2 缓存监控(可落地实现)

缓存监控是保障缓存体系稳定运行的关键,需实时监控缓存命中率、缓存大小、过期数量、异常情况等指标,及时发现问题并优化。以下是基于SpringBoot3的可落地监控方案(结合Actuator和Micrometer):

5.2.1 引入监控依赖

复制代码
<!-- SpringBoot Actuator:暴露监控指标 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency><!-- Micrometer:指标收集(支持Prometheus、Grafana等) -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- Caffeine 监控支持 -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine-micrometer</artifactId>
    <version>3.1.8</version>
</dependency>

5.2.2 配置监控指标暴露

复制代码
# 监控配置(application.yml)
management:
  endpoints:
    web:
      exposure:
        include: prometheus,health,info,caches # 暴露的监控端点
  metrics:
    tags:
      application: springboot3-cache-practice # 应用标识(便于多应用监控)
  endpoint:
    health:
      show-details: always # 显示健康详情
    caches:
      enabled: true # 启用缓存监控端点(可查看所有缓存的状态)
# Caffeine 监控配置(关联Micrometer)
spring:
  cache:
    caffeine:
      spec: initialCapacity=200,maximumSize=2000,expireAfterWrite=5m,recordStats

5.2.3 核心监控指标说明

启动项目后,访问 http://localhost:8080/actuator/prometheus 即可获取所有监控指标,关键缓存指标如下:

  • 缓存命中率:caffeine_cache_hit_ratio(Caffeine)、redis_cache_hit_ratio(Redis),命中率建议维持在90%以上,低于80%需优化缓存策略(如调整过期时间、增加热点数据缓存);

  • 缓存大小:caffeine_cache_size(Caffeine本地缓存当前大小)、redis_keyspace_keys(Redis指定库的key数量),避免缓存膨胀;

  • 缓存操作次数:caffeine_cache_gets_total(Caffeine查询总数)、caffeine_cache_puts_total(Caffeine写入总数),用于分析缓存使用频率;

  • 异常指标:redis_connection_failures_total(Redis连接失败次数)、cache_get_exceptions_total(缓存查询异常次数),出现异常需及时排查(如Redis宕机、网络问题)。

5.2.4 可视化监控(Prometheus + Grafana)

  1. 部署Prometheus,配置指标拉取地址(指向SpringBoot应用的prometheus端点);

  2. 部署Grafana,导入Caffeine、Redis相关监控面板(可在Grafana官网搜索模板ID,如Caffeine模板ID:12803,Redis模板ID:11835);

  3. 配置告警规则(如缓存命中率低于80%、Redis连接失败次数大于0时,触发告警通知),确保及时发现问题。

六、实战总结与避坑手册

本章汇总全文核心要点,梳理实际开发中最易踩坑的场景及解决方案,助力开发者快速落地多级缓存架构,避免重复踩坑。

6.1 核心架构总结

本文实现的 SpringBoot3 + Caffeine + Redis 多级缓存架构,核心流程与优势如下:

  1. 查询流程:Caffeine本地缓存 → Redis分布式缓存 → 数据库(查询后同步更新两级缓存);

  2. 更新/删除流程:数据库 → 同步更新/删除Redis + Caffeine缓存(保证一致性);

  3. 防御体系:空值缓存+布隆过滤器(防穿透)、过期时间随机+缓存预热+降级熔断(防雪崩)、逻辑过期+互斥锁(防击穿);

  4. 核心优势:兼顾性能(Caffeine本地缓存)与可靠性(Redis分布式缓存),防御方案全覆盖,适配高并发生产环境。

相关推荐
咖啡の猫2 小时前
Redis简单介绍
数据库·redis·缓存
-XWB-2 小时前
【Oracle】Oracle诊断系列(4/6):表空间与对象管理——存储优化与空间规划
数据库·oracle
山峰哥2 小时前
SQL优化全解析:从索引策略到查询性能飞跃
大数据·数据库·sql·编辑器·深度优先
爱吃大芒果2 小时前
Flutter for OpenHarmony 实战: mango_shop 购物车模块的状态同步与本地缓存处理
flutter·缓存·dart
葫三生2 小时前
存在之思:三生原理与现象学对话可能?
数据库·人工智能·神经网络·算法·区块链
不凉帅2 小时前
NO.6 数据库设计基础知识
数据库·分布式数据库·软考·数据库设计
TOOLS指南2 小时前
谷歌AI Gemin怎么使用?Gemini国内使用指南!
数据库·微软
cuber膜拜3 小时前
Weaviate 简介与基本使用
数据库·python·docker·向量数据库·weaviate
MAHATMA玛哈特科技3 小时前
以曲求直:校平技术中的反直觉哲学
前端·数据库·制造·校平机·矫平机·液压矫平机