Redis + Caffeine 协同注解实践:自定义缓存注解实现分层缓存
在分布式系统中,本地缓存(Caffeine) 与 分布式缓存(Redis) 的协同使用能显著提升性能,但手动管理两者的缓存逻辑(如查询顺序、回种策略)会增加代码复杂度。通过自定义注解封装这一过程,可实现“声明式缓存”,简化业务代码并保证一致性。以下是完整的实现方案。
一、设计目标与注解定义
1. 目标
通过自定义注解 @HybridCacheable,实现“本地缓存优先 → Redis 兜底 → 数据库回源”的分层缓存逻辑,同时支持缓存更新、过期时间配置、防穿透/击穿等能力。
2. 注解属性设计
| 属性 | 说明 | 默认值 |
|---|---|---|
value/cacheNames | 缓存名称(用于 Redis 和本地缓存的命名空间) | 空(必填) |
localExpire | 本地缓存(Caffeine)的过期时间(单位:秒) | 300(5分钟) |
redisExpire | Redis 缓存的过期时间(单位:秒) | 600(10分钟) |
sync | 是否同步更新缓存(更新操作时是否同时更新本地和 Redis) | true |
keyGenerator | 自定义缓存键生成器(默认使用方法参数生成) | SimpleKeyGenerator |
unless | 条件判断(结果为空时不缓存) | 空 |
二、核心实现步骤
1. 定义自定义注解 @HybridCacheable
通过 Spring 的 @Target、@Retention 等元注解定义注解,并声明其在运行时可见:
import org.springframework.cache.annotation.Cacheable;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HybridCacheable {
String[] value() default {}; // 缓存名称(必填)
long localExpire() default 300; // 本地缓存过期时间(秒)
long redisExpire() default 600; // Redis 缓存过期时间(秒)
boolean sync() default true; // 是否同步更新
String keyGenerator() default "simpleKeyGenerator"; // 键生成器 Bean 名称
String unless() default ""; // 条件判断 SpEL
}
2. 实现复合缓存管理器
需要自定义 CacheManager,同时管理 Caffeine 本地缓存和 Redis 分布式缓存,并提供统一的 get、put、evict 接口。
2.1 本地缓存(Caffeine)管理器
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Component
public class HybridCacheManager implements CacheManager {
// 本地缓存容器(Caffeine)
private final ConcurrentHashMap<String, Cache<Object, Object>> localCaches = new ConcurrentHashMap<>();
// Redis 缓存管理器(通过 Spring 注入)
@Autowired
private RedisCacheManager redisCacheManager;
// 初始化本地缓存(根据注解配置的名称和过期时间)
@PostConstruct
public void init() {
// 示例:从配置或注解中读取缓存名称和过期时间(实际可结合配置中心)
registerLocalCache("userCache", 300); // 用户缓存,5分钟过期
}
// 注册本地缓存
private void registerLocalCache(String cacheName, long expireSeconds) {
localCaches.computeIfAbsent(cacheName, name ->
Caffeine.newBuilder()
.maximumSize(1000) // 最大容量(可根据配置调整)
.expireAfterAccess(expireSeconds, TimeUnit.SECONDS) // 过期时间
.build()
.asMap()
);
}
@Override
public Cache getCache(String name) {
// 优先从本地缓存获取,不存在则从 Redis 获取
Cache<Object, Object> localCache = localCaches.get(name);
if (localCache == null) {
// 本地缓存未注册时,使用 Redis 作为兜底(可选)
return redisCacheManager.getCache(name);
}
return new HybridCache(localCache, redisCacheManager.getCache(name), name);
}
// 其他方法(如 getCacheNames)按需实现...
}
2.2 复合缓存类 HybridCache
封装本地缓存和 Redis 缓存的读写逻辑,实现“本地优先 → Redis 兜底”的查询顺序:
import org.springframework.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.Callable;
public class HybridCache implements Cache {
private final Cache<Object, Object> localCache; // Caffeine 本地缓存
private final Cache<Object, Object> redisCache; // Redis 分布式缓存
private final String cacheName;
public HybridCache(Cache<Object, Object> localCache, Cache<Object, Object> redisCache, String cacheName) {
this.localCache = localCache;
this.redisCache = redisCache;
this.cacheName = cacheName;
}
@Override
public String getName() {
return cacheName;
}
@Override
public Object getNativeCache() {
return localCache.getNativeCache(); // 返回本地缓存的原生实现(可选)
}
@Override
public ValueWrapper get(Object key) {
// 1. 先查本地缓存
ValueWrapper localValue = localCache.get(key);
if (localValue != null) {
return localValue;
}
// 2. 本地未命中,查 Redis
ValueWrapper redisValue = redisCache.get(key);
if (redisValue != null) {
// 将 Redis 数据回种到本地缓存(避免下次请求再次查 Redis)
localCache.put(key, redisValue.get());
return redisValue;
}
// 3. 缓存均未命中,返回 null(由业务逻辑处理数据库查询)
return null;
}
@Override
public void put(Object key, Object value) {
// 同步更新本地和 Redis 缓存(根据注解的 sync 属性控制)
localCache.put(key, value);
redisCache.put(key, value);
}
@Override
public void evict(Object key) {
// 同步删除本地和 Redis 缓存
localCache.invalidate(key);
redisCache.evict(key);
}
// 其他方法(如 getIfPresent)按需实现...
}
3. 自定义 Key 生成器
默认使用 Spring 的 SimpleKeyGenerator,但支持自定义生成逻辑(如基于方法参数生成唯一键):
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Component("simpleKeyGenerator")
public class SimpleKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
// 生成唯一键(格式:类名:方法名:参数列表)
return target.getClass().getName() + ":" + method.getName() + ":" + Arrays.toString(params);
}
}
4. AOP 切面实现缓存逻辑
通过 AOP 拦截带有 @HybridCacheable 注解的方法,实现“查询 → 缓存 → 更新”的全流程管理。
4.1 切面类 HybridCacheAspect
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.interceptor.CacheOperationInvocationContext;
import org.springframework.cache.interceptor.CacheResolver;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Collection;
@Aspect
@Component
public class HybridCacheAspect {
@Autowired
private CacheResolver cacheResolver;
private final ExpressionParser parser = new SpelExpressionParser();
@Around("@annotation(hybridCacheable)")
public Object around(ProceedingJoinPoint joinPoint, HybridCacheable hybridCacheable) throws Throwable {
// 获取方法信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String[] cacheNames = hybridCacheable.value();
// 解析 SpEL 条件(unless)
String unless = hybridCacheable.unless();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("result", null); // 结果变量(后续赋值)
// 1. 查询缓存
Object result = queryCache(joinPoint, cacheNames, hybridCacheable);
// 2. 缓存未命中,执行目标方法(查询数据库)
if (result == null) {
result = joinPoint.proceed();
}
// 3. 处理 unless 条件(结果为空时不缓存)
if (unless != null && !unless.isEmpty()) {
context.setVariable("result", result);
Boolean shouldCache = parser.parseExpression(unless).getValue(context, Boolean.class);
if (Boolean.FALSE.equals(shouldCache)) {
return result;
}
}
// 4. 回种缓存(根据 sync 属性决定是否同步更新)
if (result != null && hybridCacheable.sync()) {
putToCache(joinPoint, cacheNames, result);
}
return result;
}
private Object queryCache(ProceedingJoinPoint joinPoint, String[] cacheNames, HybridCacheable hybridCacheable) {
for (String cacheName : cacheNames) {
Collection<? extends Cache> caches = cacheResolver.resolveCaches(cacheName);
for (Cache cache : caches) {
// 从复合缓存中获取数据(本地 + Redis)
ValueWrapper wrapper = cache.get(joinPoint.getArgs());
if (wrapper != null) {
return wrapper.get();
}
}
}
return null;
}
private void putToCache(ProceedingJoinPoint joinPoint, String[] cacheNames, Object result) {
for (String cacheName : cacheNames) {
Collection<? extends Cache> caches = cacheResolver.resolveCaches(cacheName);
for (Cache cache : caches) {
// 生成缓存键(使用自定义 KeyGenerator)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Object key = generateKey(joinPoint, method, cacheName);
cache.put(key, result);
}
}
}
private Object generateKey(ProceedingJoinPoint joinPoint, Method method, String cacheName) {
// 使用自定义 KeyGenerator 生成键
return cacheResolver.getKeyGenerator(cacheName).generate(joinPoint.getTarget(), method, joinPoint.getArgs());
}
}
5. 业务代码使用示例
通过 @HybridCacheable 注解标记需要缓存的方法,无需手动处理本地和 Redis 缓存的读写逻辑。
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 本地缓存(Caffeine)最大容量 1000,5 分钟过期
// Redis 缓存 10 分钟过期,同步更新
@HybridCacheable(
value = "userCache",
localExpire = 300,
redisExpire = 600,
sync = true
)
public User getUserById(String userId) {
// 缓存未命中时,自动查询数据库(无需手动处理)
return fetchUserFromDatabase(userId);
}
// 更新用户信息(自动同步更新本地和 Redis 缓存)
@HybridCacheable(
value = "userCache",
localExpire = 300,
redisExpire = 600,
sync = true
)
public void updateUser(User user) {
// 更新数据库(自动触发缓存更新)
updateUserInDatabase(user);
}
// 模拟数据库查询
private User fetchUserFromDatabase(String userId) {
return new User(userId, "张三", 25);
}
// 模拟数据库更新
private void updateUserInDatabase(User user) {
// 实际更新逻辑...
}
}
三、关键场景优化
1. 缓存穿透防护
在 queryCache 方法中,若数据库返回 null,可缓存空值到 Redis(短过期时间)和本地缓存(可选):
private Object queryCache(ProceedingJoinPoint joinPoint, String[] cacheNames, HybridCacheable hybridCacheable) {
for (String cacheName : cacheNames) {
Collection<? extends Cache> caches = cacheResolver.resolveCaches(cacheName);
for (Cache cache : caches) {
ValueWrapper wrapper = cache.get(joinPoint.getArgs());
if (wrapper != null) {
return wrapper.get();
}
}
}
// 缓存未命中,查询数据库
Object result = fetchFromDatabase(joinPoint);
if (result == null) {
// 缓存空值到 Redis(避免重复查询)
putNullToCache(joinPoint, cacheNames);
}
return result;
}
private void putNullToCache(ProceedingJoinPoint joinPoint, String[] cacheNames) {
for (String cacheName : cacheNames) {
Object key = generateKey(joinPoint, cacheName);
redisCacheManager.getCache(cacheName).put(key, null);
// 可选:缓存空值到本地缓存(根据业务需求)
// localCacheManager.getCache(cacheName).put(key, null);
}
}
2. 缓存击穿防护
对热点数据(如高频访问的用户 ID),使用分布式锁(如 Redis 的 SETNX)保证只有一个线程回种缓存:
private Object queryCacheWithLock(ProceedingJoinPoint joinPoint, String[] cacheNames, HybridCacheable hybridCacheable) {
Object key = generateKey(joinPoint, cacheNames);
String lockKey = "lock:" + cacheName + ":" + key;
// 获取锁(Redis 分布式锁)
Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(lock)) {
try {
// 再次检查缓存(避免锁竞争导致的重复查询)
Object result = queryCache(joinPoint, cacheNames, hybridCacheable);
if (result != null) {
return result;
}
// 查询数据库并回种缓存
result = fetchFromDatabase(joinPoint);
if (result != null) {
putToCache(joinPoint, cacheNames, result);
} else {
putNullToCache(joinPoint, cacheNames);
}
return result;
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
} else {
// 未获取到锁,等待或重试(可结合重试机制)
return queryCacheWithLock(joinPoint, cacheNames, hybridCacheable);
}
}
四、总结与优势
通过自定义注解 @HybridCacheable,实现了 Redis 与 Caffeine 的深度协同,核心优势如下:
1. 简化开发
业务代码只需关注核心逻辑,无需手动处理本地和 Redis 缓存的读写、回种逻辑。
2. 高性能
本地缓存(Caffeine)提供纳秒级延迟,Redis 提供跨节点共享,兼顾低延迟与高可用。
3. 强一致性
通过同步更新策略(sync=true),确保本地和 Redis 缓存数据一致。
4. 灵活配置
支持自定义缓存名称、过期时间、键生成器等,适配多样化业务场景。
注意事项:需根据业务场景调整缓存容量、过期时间和同步策略,避免内存溢出或缓存雪崩。对于强一致性要求极高的场景(如金融交易),建议结合分布式锁(如 Redisson)进一步保证原子性。

1077

被折叠的 条评论
为什么被折叠?



