在高并发后端系统中,缓存是提升性能、降低数据库压力的核心手段。但单一缓存方案往往难以兼顾性能与可靠性:本地缓存(如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 组合,核心原因如下:
-
Caffeine(本地缓存):基于 Google Guava Cache 优化而来,是目前性能最优的本地缓存框架之一,支持自动过期、内存淘汰(LRU 变体算法),并发性能极强(每秒可处理百万级请求),适合存储热点数据,减少 Redis 访问压力;
-
Redis(分布式缓存):高性能、高可用的分布式缓存中间件,支持多种数据结构,可跨服务、跨节点共享缓存,适合存储非热点但需共享的数据,同时可通过持久化(RDB/AOF)避免缓存丢失;
-
协同逻辑:查询数据时,先查 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 或浏览器访问接口,验证多级缓存效果:
-
第一次访问 /user/1:控制台输出"查询数据库,id:1",耗时约 100ms(数据库查询耗时);
-
第二次访问 /user/1:控制台无数据库查询日志,耗时约 1ms(Caffeine 本地缓存命中);
-
重启项目(Caffeine 缓存清空),访问 /user/1:控制台无数据库查询日志,耗时约 5ms(Redis 缓存命中,同步到 Caffeine);
-
调用 /user 新增用户(id=4),再访问 /user/4:直接命中缓存,无数据库查询;
-
调用 /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 防御方案(三重防御)
-
过期时间加随机值(分散过期时间) 核心思路:在统一的过期时间基础上,添加随机偏移量(如 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(); } } -
缓存预热(提前加载热点缓存) 核心思路:系统启动时,提前将热点数据加载到 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()); } } -
**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 性能调优
-
Caffeine 本地缓存参数调优:Caffeine的性能依赖于合理的参数配置,需根据业务场景调整,避免内存浪费或缓存命中率过低。
// 优化 Caffeine 配置(替换原配置文件中的caffeine.spec) // 核心参数说明(结合高并发场景) spring: cache: caffeine: spec: initialCapacity=200,maximumSize=2000,expireAfterWrite=5m,recordStats # initialCapacity:初始容量,根据热点数据初始数量设置(避免频繁扩容) # maximumSize:最大容量,结合JVM内存设置(建议不超过JVM堆内存的10%) # expireAfterWrite:写入后过期时间,热点数据可缩短(如5m),非热点可延长 # recordStats:开启统计功能,用于监控缓存命中率(后续监控部分会用到) -
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 -
缓存粒度控制:避免缓存过大或过小,提升缓存命中率和更新效率。 不缓存大对象:若需缓存复杂对象(如包含大量字段的实体),可拆分缓存(如用户基本信息、用户详情分开缓存),避免更新时全量更新缓存;
-
不缓存高频更新数据:如实时统计数据、高频修改的状态字段,这类数据缓存收益低,还会增加缓存与数据库的一致性维护成本;
-
合理复用缓存:对于相同查询条件的请求,确保缓存key唯一,避免重复缓存(可通过自定义key生成器统一规则)。
-
避免缓存滥用:并非所有接口都需要缓存,以下场景不建议使用缓存: 查询频率极低的接口(如后台管理的低频查询);
-
数据一致性要求极高的接口(如转账、支付接口,需实时查询数据库);
-
查询耗时极短的接口(如查询固定字典数据,耗时<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)
-
部署Prometheus,配置指标拉取地址(指向SpringBoot应用的prometheus端点);
-
部署Grafana,导入Caffeine、Redis相关监控面板(可在Grafana官网搜索模板ID,如Caffeine模板ID:12803,Redis模板ID:11835);
-
配置告警规则(如缓存命中率低于80%、Redis连接失败次数大于0时,触发告警通知),确保及时发现问题。
六、实战总结与避坑手册
本章汇总全文核心要点,梳理实际开发中最易踩坑的场景及解决方案,助力开发者快速落地多级缓存架构,避免重复踩坑。
6.1 核心架构总结
本文实现的 SpringBoot3 + Caffeine + Redis 多级缓存架构,核心流程与优势如下:
-
查询流程:Caffeine本地缓存 → Redis分布式缓存 → 数据库(查询后同步更新两级缓存);
-
更新/删除流程:数据库 → 同步更新/删除Redis + Caffeine缓存(保证一致性);
-
防御体系:空值缓存+布隆过滤器(防穿透)、过期时间随机+缓存预热+降级熔断(防雪崩)、逻辑过期+互斥锁(防击穿);
-
核心优势:兼顾性能(Caffeine本地缓存)与可靠性(Redis分布式缓存),防御方案全覆盖,适配高并发生产环境。