Redis + Caffeine 协同注解实践:自定义缓存注解实现分层缓存

Redis + Caffeine 协同注解实践:自定义缓存注解实现分层缓存

在分布式系统中,本地缓存(Caffeine)分布式缓存(Redis) 的协同使用能显著提升性能,但手动管理两者的缓存逻辑(如查询顺序、回种策略)会增加代码复杂度。通过自定义注解封装这一过程,可实现“声明式缓存”,简化业务代码并保证一致性。以下是完整的实现方案。


一、设计目标与注解定义

1. 目标

通过自定义注解 @HybridCacheable,实现“本地缓存优先 → Redis 兜底 → 数据库回源”的分层缓存逻辑,同时支持缓存更新、过期时间配置、防穿透/击穿等能力。

2. 注解属性设计

属性说明默认值
value/cacheNames缓存名称(用于 Redis 和本地缓存的命名空间)空(必填)
localExpire本地缓存(Caffeine)的过期时间(单位:秒)300(5分钟)
redisExpireRedis 缓存的过期时间(单位:秒)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 分布式缓存,并提供统一的 getputevict 接口。

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)进一步保证原子性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值