多级缓存框架(Redis + Caffeine)完整指南
一、框架核心概述(是什么)
这是一套 本地缓存(Caffeine)+ 分布式缓存(Redis)的多级缓存框架,基于 Spring 生态实现,核心目标是兼顾缓存访问性能与分布式环境下的数据一致性。
核心特性
- 多级缓存架构:一级缓存(本地 Caffeine)提升响应速度,二级缓存(Redis)保证分布式一致性
- 灵活配置:支持过期时间、过期模式、NULL 值缓存、本地缓存容量限制等
- 动态 Key 生成:基于 SpEL 表达式实现缓存 Key 动态拼接
- 并发安全:内置分布式锁(Redisson),解决缓存穿透/击穿/雪崩问题
- 完整 API:支持缓存增删改查、原子操作、批量处理等
技术栈依赖
- 核心框架:Spring Boot、Spring Context
- 缓存组件:Caffeine(本地缓存)、Redis(分布式缓存)
- 分布式锁:Redisson
- 序列化:Jackson
- 表达式解析:Spring EL
二、框架核心价值(为什么用)
1. 解决的核心问题
| 问题场景 | 框架解决方案 |
|---|---|
| 缓存穿透(查询不存在的数据) | 支持 NULL 值缓存 + 分布式锁防重复查询 |
| 缓存击穿(热点 Key 过期) | 分布式锁 + 缓存自动续期 |
| 缓存雪崩(大量 Key 同时过期) | 过期时间随机偏移 + 多级缓存降级 |
| 分布式缓存一致性 | Redis 集中存储 + 缓存更新同步 |
| 本地缓存性能瓶颈 | Caffeine 本地缓存(O(1) 访问速度) |
2. 相比单一缓存的优势
- 比纯 Redis 缓存:减少网络 IO 开销,本地缓存响应时间提升 10-100 倍
- 比纯本地缓存:支持分布式部署,避免节点间数据不一致
- 比 Spring Cache 原生:支持更灵活的配置(过期模式、容量限制)和更完善的并发控制
三、快速上手(怎么做)
1. 依赖配置(Maven)
xml
<!-- Spring Boot 基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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>
<!-- Redisson 分布式锁 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.25.0</version>
</dependency>
<!-- Jackson 序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
2. 配置文件(application.yml)
yaml
spring:
application:
name: demo-app # 用于 Redis Key 前缀
redis:
host: 127.0.0.1
port: 6379
password: 123456
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 2
3. 基础使用示例
3.1 编程式使用(直接操作缓存 API)
java
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
public class UserService {
// 注入缓存管理器
@Resource
private CacheManager cacheManager;
// 缓存配置(可通过 @ConfigurationProperties 注入)
private final MultiCacheConfig userCacheConfig = new MultiCacheConfig(
30, TimeUnit.MINUTES, 20, true, true // ttl=30分钟,允许缓存NULL,启用本地缓存
);
/**
* 查询用户:缓存命中返回,未命中查库并缓存
*/
public User getUserById(Long userId) {
// 1. 获取缓存实例(缓存名称:userCache)
Cache cache = cacheManager.getCache("userCache", userCacheConfig);
// 2. 生成缓存Key(支持SpEL表达式,这里直接拼接)
String cacheKey = "user:" + userId;
// 3. 缓存查询(未命中时执行Supplier查库)
return (User) cache.get(cacheKey, User.class, () -> {
// 数据库查询逻辑(仅缓存未命中时执行)
return userDao.selectById(userId);
});
}
/**
* 更新用户:同步更新数据库和缓存
*/
public void updateUser(User user) {
Cache cache = cacheManager.getCache("userCache", userCacheConfig);
String cacheKey = "user:" + user.getId();
// 1. 更新数据库
userDao.updateById(user);
// 2. 更新缓存(覆盖旧值)
cache.put(cacheKey, user);
}
/**
* 删除用户:同步删除数据库和缓存
*/
public void deleteUser(Long userId) {
Cache cache = cacheManager.getCache("userCache", userCacheConfig);
String cacheKey = "user:" + userId;
// 1. 删除数据库记录
userDao.deleteById(userId);
// 2. 删除缓存(避免脏数据)
cache.evict(cacheKey);
}
}
3.2 注解式使用(基于 AOP 切面)
java
import java.lang.annotation.*;
// 1. 自定义缓存注解(可扩展)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyCacheable {
String cacheName(); // 缓存名称
String key() default ""; // SpEL表达式Key
MultiCacheConfig config() default @MultiCacheConfig(); // 缓存配置
}
// 2. 注解切面实现(基于 AbstractCacheAspectSupport)
@Aspect
@Component
public class CacheAspect extends AbstractCacheAspectSupport {
@Resource
private CacheManager cacheManager;
@Around("@annotation(myCacheable)")
public Object cacheAroundAdvice(ProceedingJoinPoint joinPoint, MyCacheable myCacheable) throws Throwable {
// 获取方法信息
Method method = getSpecificMethod(joinPoint);
Object[] args = joinPoint.getArgs();
Object target = joinPoint.getTarget();
// 解析缓存Key(支持SpEL表达式)
String cacheKey = (String) generateKey(myCacheable.key(), method, args, target);
// 获取缓存实例
Cache cache = cacheManager.getCache(myCacheable.cacheName(), myCacheable.config());
// 缓存查询:命中返回,未命中执行目标方法并缓存
return cache.get(cacheKey, method.getReturnType(), () -> {
try {
return joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException("缓存加载失败", e);
}
});
}
}
// 3. 业务层使用注解
@Service
public class ProductService {
@MyCacheable(
cacheName = "productCache",
key = "#root.methodName + ':' + #p0", // SpEL:方法名+第一个参数
config = @MultiCacheConfig(ttl = 60, timeUnit = TimeUnit.MINUTES, cacheNullValues = true)
)
public Product getProductById(Long productId) {
// 数据库查询逻辑(缓存未命中时执行)
return productDao.selectById(productId);
}
}
四、核心组件详解
1. 缓存核心接口(Cache)
定义缓存操作标准 API,所有缓存实现的顶层接口:
arduino
public interface Cache {
// 获取缓存(未命中返回null)
<T> T get(String key, Class<T> resultType);
// 获取缓存(未命中执行valueLoader加载并缓存)
<T> T get(String key, Class<T> resultType, Supplier<Object> valueLoader);
// 添加/覆盖缓存
void put(String key, Object value);
// 原子操作:不存在时才添加
boolean putIfAbsent(String key, Object value);
// 单个删除
void evict(String key);
// 批量删除
void multiEvict(Collection<String> keys);
// 清空缓存
void clear();
}
2. 多级缓存实现(MultiLevelCache)
整合 Caffeine 本地缓存和 Redis 分布式缓存:
kotlin
public class MultiLevelCache implements Cache {
private final String cacheName;
private final CaffeineCache localCache; // 一级缓存(本地)
private final RedisCache remoteCache; // 二级缓存(Redis)
private final boolean cacheNullValues; // 是否缓存NULL值
private final boolean enableLocalCache; // 是否启用本地缓存
// 核心逻辑:查询缓存时先查本地,再查Redis,最后查库
@Override
public <T> T get(String key, Class<T> resultType, Supplier<Object> valueLoader) {
// 1. 查本地缓存
if (enableLocalCache) {
T localValue = localCache.get(key, resultType);
if (resultType.isInstance(localValue) && !NullValue.isNullValue(localValue)) {
return localValue;
}
}
// 2. 查Redis缓存
T remoteValue = remoteCache.get(key, resultType);
if (resultType.isInstance(remoteValue) && !NullValue.isNullValue(remoteValue)) {
// 同步到本地缓存
if (enableLocalCache) {
localCache.put(key, remoteValue);
}
return remoteValue;
}
// 3. 缓存未命中,执行加载逻辑(查库)
Object value = valueLoader.get();
// 4. 缓存结果(支持NULL值)
put(key, value);
return resultType.cast(fromStoreValue(value));
}
// 其他方法:put/evict/clear 均同步操作本地和Redis缓存
}
3. 缓存管理器(CacheManager)
负责缓存实例的创建和管理,采用懒加载模式:
arduino
public interface CacheManager {
// 获取缓存(不存在返回null)
Cache getCache(String name);
// 获取缓存(不存在则创建)
Cache getCache(String name, MultiCacheConfig config);
}
// 抽象实现类
public abstract class AbstractCacheManager implements CacheManager {
// 缓存容器(ConcurrentHashMap保证线程安全)
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
@Override
public Cache getCache(String name, MultiCacheConfig config) {
// 双重检查锁:避免重复创建缓存
Cache cache = cacheMap.get(name);
if (cache != null) {
return cache;
}
synchronized (cacheMap) {
cache = cacheMap.get(name);
if (cache == null) {
// 创建缓存实例(子类实现具体缓存类型)
cache = createCache(name, config);
cacheMap.put(name, cache);
}
return cache;
}
}
// 抽象方法:由子类实现缓存创建逻辑
protected abstract Cache createCache(String name, MultiCacheConfig config);
}
4. 分布式锁组件(Redisson)
解决并发场景下的缓存问题,核心 API:
arduino
public interface DistributedLocker {
// 获取单个锁
boolean lock(String lockKey, long leaseTime, long waitTime, TimeUnit unit);
// 获取联锁(多个Key)
boolean multiLock(Collection<String> lockKeys, long leaseTime, long waitTime, TimeUnit unit);
// 释放锁
void unlock(String lockKey);
// 释放联锁
void multiUnlock(Collection<String> lockKeys);
}
// 工具类封装(静态调用)
public class DistributedLockUtils {
private static DistributedLocker locker;
// 获取锁并执行逻辑
public static <T> T executeWithLock(String lockKey, long leaseTime, long waitTime, TimeUnit unit, Supplier<T> supplier) {
if (locker.lock(lockKey, leaseTime, waitTime, unit)) {
try {
return supplier.get();
} finally {
locker.unlock(lockKey);
}
}
throw new RuntimeException("获取锁失败");
}
}
5. 工具类
5.1 JSON 序列化工具(JsonUtils)
typescript
public class JsonUtils {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 对象转JSON字符串
public static String toJson(Object obj) {
try {
return OBJECT_MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON序列化失败", e);
}
}
// JSON字符串转对象
public static <T> T fromJson(String json, Class<T> clazz) {
try {
return OBJECT_MAPPER.readValue(json, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON反序列化失败", e);
}
}
}
5.2 SpEL 表达式解析工具(CacheExpressionEvaluator)
typescript
public class CacheExpressionEvaluator extends CachedExpressionEvaluator {
private final Map<ExpressionKey, Expression> keyCache = new ConcurrentHashMap<>(64);
// 创建表达式上下文(包含方法、参数、目标对象等)
public EvaluationContext createContext(Method method, Object[] args, Object target) {
CacheExpressionRootObject root = new CacheExpressionRootObject(method, args, target);
return new MethodBasedEvaluationContext(root, method, args, getParameterNameDiscoverer());
}
// 解析SpEL表达式
public Object evaluateKey(String keyExpr, Method method, Object[] args, Object target) {
if (StringUtils.isEmpty(keyExpr)) {
return "";
}
EvaluationContext context = createContext(method, args, target);
ExpressionKey key = new ExpressionKey(keyExpr, method);
return keyCache.computeIfAbsent(key, k -> getExpressionParser().parseExpression(keyExpr))
.getValue(context);
}
}
五、进阶配置与最佳实践
1. 缓存策略配置
1.1 过期模式选择
java
// 过期模式枚举
public enum ExpireMode {
WRITE, // 写入后开始计时(适合写少读多场景)
ACCESS // 访问后刷新过期时间(适合热点数据)
}
// 配置示例:热点数据使用ACCESS模式
MultiCacheConfig hotDataConfig = new MultiCacheConfig();
hotDataConfig.setExpireMode(ExpireMode.ACCESS.name());
hotDataConfig.setTtl(10); // 10分钟
hotDataConfig.setEnableFirstCache(true);
1.2 防止缓存雪崩
arduino
// 配置过期时间随机偏移(避免大量Key同时过期)
public class MultiCacheConfig {
// 最大偏移比例(10%)
private static final double MAX_OFFSET_RATIO = 0.1;
// 获取带随机偏移的过期时间(秒)
public long getRandomTtl() {
long baseTtl = getTtlToSeconds();
double offset = baseTtl * MAX_OFFSET_RATIO;
// 随机增减偏移量
return (long) (baseTtl + (Math.random() * 2 * offset - offset));
}
}
2. 性能调优
2.1 本地缓存(Caffeine)调优
arduino
// 高性能配置:基于访问频率和过期时间
CaffeineCacheConfig config = new CaffeineCacheConfig();
config.setInitialCapacity(100); // 初始容量
config.setMaximumSize(5000); // 最大容量(避免内存溢出)
config.setExpireMode(ExpireMode.ACCESS); // 访问过期
config.setTtl(5); // 5分钟过期
2.2 Redis 缓存调优
yaml
spring:
redis:
lettuce:
pool:
max-active: 16 # 最大连接数
max-idle: 8 # 最大空闲连接
min-idle: 4 # 最小空闲连接
timeout: 2000ms # 超时时间(避免长时间阻塞)
3. 常见问题解决方案
3.1 缓存穿透(查询不存在的数据)
kotlin
// 方案:缓存NULL值 + 分布式锁
public User getUserById(Long userId) {
String cacheKey = "user:" + userId;
Cache cache = cacheManager.getCache("userCache", config);
// 1. 查缓存(包括NULL值)
User user = cache.get(cacheKey, User.class);
if (user != null) {
return user;
}
// 2. 分布式锁:防止并发查询不存在的数据
return DistributedLockUtils.executeWithLock(
"lock:user:" + userId, 5, 3, TimeUnit.SECONDS,
() -> {
// 3. 再次查缓存(避免锁等待期间已缓存)
User cachedUser = cache.get(cacheKey, User.class);
if (cachedUser != null) {
return cachedUser;
}
// 4. 查库(不存在则返回null)
User dbUser = userDao.selectById(userId);
// 5. 缓存结果(包括NULL值)
cache.put(cacheKey, dbUser);
return dbUser;
}
);
}
3.2 缓存击穿(热点Key过期)
ini
// 方案:缓存自动续期 + 分布式锁
public Product getHotProduct(Long productId) {
String cacheKey = "hot:product:" + productId;
Cache cache = cacheManager.getCache("hotProductCache", config);
return DistributedLockUtils.executeWithLock(
cacheKey + ":lock", -1, 3, TimeUnit.SECONDS, // leaseTime=-1:自动续期
() -> {
Product product = cache.get(cacheKey, Product.class);
if (product != null) {
// 访问后刷新过期时间(ACCESS模式自动实现)
return product;
}
// 查库并缓存
Product dbProduct = productDao.selectById(productId);
cache.put(cacheKey, dbProduct);
return dbProduct;
}
);
}
3.3 缓存一致性(更新/删除后同步缓存)
typescript
// 方案:更新数据库后同步更新缓存,删除数据库后删除缓存
@Transactional
public void updateProduct(Product product) {
// 1. 更新数据库(事务内)
productDao.updateById(product);
// 2. 更新缓存(事务提交后执行,避免事务回滚导致缓存脏数据)
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
Cache cache = cacheManager.getCache("productCache", config);
cache.put("product:" + product.getId(), product);
}
}
);
}
六、完整可复用核心代码
1. 缓存配置类
typescript
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Value;
@Configuration
@ConditionalOnBean(RedisTemplate.class)
public class CacheAutoConfiguration {
@Value("${spring.application.name}")
private String appName;
// 注册缓存管理器
@Bean
public CacheManager cacheManager(RedisTemplate<String, String> redisTemplate) {
return new DefaultCacheManager(appName, new RedisCacheTemplate(redisTemplate));
}
// 注册分布式锁
@Bean
public DistributedLocker distributedLocker(RedissonClient redissonClient) {
return new RedissonDistributedLocker(redissonClient);
}
}
2. 多级缓存配置模型
java
import java.util.concurrent.TimeUnit;
public class MultiCacheConfig {
// 缓存过期时间(默认10分钟)
private long ttl = 10;
// 时间单位(默认分钟)
private TimeUnit timeUnit = TimeUnit.MINUTES;
// 过期时间比例(用于动态调整,1-100)
private int ttlProportion = 10;
// 是否缓存NULL值(默认false)
private boolean cacheNullValues = false;
// 是否启用本地缓存(默认false)
private boolean enableLocalCache = false;
// 本地缓存初始容量(默认10)
private int initialCapacity = 10;
// 本地缓存最大容量(默认3000)
private long maximumSize = 3000;
// 过期模式(默认WRITE)
private String expireMode = ExpireMode.WRITE.name();
// Getter/Setter 省略
// 转换为秒级过期时间
public long getTtlToSeconds() {
return timeUnit.toSeconds(ttl);
}
// 带比例的过期时间(最小3秒)
public long getTtlWithProportion() {
long base = getTtlToSeconds();
long proportioned = (base * ttlProportion) / 100;
return Math.max(proportioned, 3);
}
}
// 过期模式枚举
enum ExpireMode {
WRITE, ACCESS
}
3. 空值标识类
java
import java.io.Serializable;
public class NullValue implements Serializable {
private static final long serialVersionUID = 1L;
public static final NullValue INSTANCE = new NullValue();
private NullValue() {}
// 反序列化时返回单例
private Object readResolve() {
return INSTANCE;
}
@Override
public boolean equals(Object obj) {
return obj == this || obj == null;
}
@Override
public int hashCode() {
return NullValue.class.hashCode();
}
// 判断是否为空值
public static boolean isNull(Object value) {
return value == null || value == INSTANCE;
}
}
4. Redis 缓存模板
typescript
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class RedisCacheTemplate {
private final StringRedisTemplate redisTemplate;
public RedisCacheTemplate(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
// 设置缓存(带过期时间)
public void set(String key, String value, long ttl, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, ttl, unit);
}
// 获取缓存
public String get(String key) {
return redisTemplate.opsForValue().get(key);
}
// 删除缓存
public boolean delete(String key) {
return redisTemplate.delete(key);
}
// 批量删除
public long delete(Collection<String> keys) {
return redisTemplate.delete(keys);
}
// 判断Key是否存在
public boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
// 设置过期时间
public boolean expire(String key, long ttl, TimeUnit unit) {
return redisTemplate.expire(key, ttl, unit);
}
}