redis 缓存注解

复制代码
import java.lang.annotation.*;

@Target(ElementType.METHOD) // 注解作用于方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LockCache {
    /**
     * 缓存的key,支持Spring EL表达式
     */
    String key() default "";

    /**
     * 缓存过期时间,单位秒,默认60秒
     */
    long expire() default 60;
    /**
     * 加锁时长,单位毫秒,默认2000毫秒
     */
    long lockTime() default 2000;
}

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.expression.EvaluationContext;
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 org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
public class LockCacheAspect {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 定义切点,匹配所有带有@RedisCache注解的方法[reference:8]
    @Pointcut("@annotation(com.i9i.redis.lockableCache.LockCache)")
    public void cachePointcut() {}

    @Around("cachePointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 获取注解和Key
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        LockCache redisCache = method.getAnnotation(LockCache.class);
        String cacheKey = parseKey(redisCache.key(), signature, joinPoint.getArgs());
        if (!StringUtils.hasText(cacheKey)) {
            return joinPoint.proceed();
        }

        // 2. 先查缓存,命中直接返回
        Object cachedValue = redisTemplate.opsForValue().get(cacheKey);
        if (cachedValue != null) {
            return cachedValue;
        }

        // 3. 缓存未命中,尝试获取分布式锁(防止缓存击穿)
        String lockKey = "LOCK:" + cacheKey;
        String lockValue = UUID.randomUUID().toString();
        long startTime = System.currentTimeMillis();

        try {
            // 自旋获取锁
            while (true) {
                // 尝试加锁(SET NX),并设置锁的过期时间
                Boolean locked = redisTemplate.opsForValue()
                        .setIfAbsent(lockKey, lockValue, redisCache.lockTime(), TimeUnit.MILLISECONDS);

                if (Boolean.TRUE.equals(locked)) {
                    // 拿到锁,查询数据库并重建缓存
                    try {
                        // 再次检查缓存(double-check),避免第一个线程刚查完还没写入时其他线程就进来了
                        Object cachedAgain = redisTemplate.opsForValue().get(cacheKey);
                        if (cachedAgain != null) {
                            return cachedAgain;
                        }

                        // 执行原方法(查数据库)
                        Object result = joinPoint.proceed();

                        // 写入缓存(即使结果为null也缓存,防止穿透)
                        long expireTime = redisCache.expire();
                        if (result == null) {
                            // 对null值设置较短的过期时间,防止穿透
                            redisTemplate.opsForValue().set(cacheKey, null, 30, TimeUnit.SECONDS);
                        } else {
                            redisTemplate.opsForValue().set(cacheKey, result, expireTime, TimeUnit.SECONDS);
                        }
                        return result;
                    } finally {
                        // 释放锁(使用lua脚本保证原子性:只释放自己加的锁)
                        releaseLock(lockKey, lockValue);
                    }
                }

                // 没拿到锁,判断是否超过等待超时时间
                if (System.currentTimeMillis() - startTime > redisCache.lockTime()*2) {
                    // 超时后,不再等待,直接查库(兜底策略,避免请求堆积)
                    // 或者可以抛出异常,视业务而定
                    return joinPoint.proceed();
                }

                // 短暂休眠后重试(自旋),避免空轮询消耗CPU
                Thread.sleep(50);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            // 发生中断异常,直接降级查库
            return joinPoint.proceed();
        }
    }

    /**
     * 释放锁:使用 Lua 脚本确保只有锁的持有者才能删除
     */
    private void releaseLock(String lockKey, String lockValue) {
        String luaScript =
                "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                        "   return redis.call('del', KEYS[1]) " +
                        "else " +
                        "   return 0 " +
                        "end";
        redisTemplate.execute(
                new DefaultRedisScript<>(luaScript, Long.class),
                Collections.singletonList(lockKey),
                lockValue
        );
    }

    /**
     * 解析SpEL表达式
     */
    private String parseKey(String keySpel, MethodSignature signature, Object[] args) {
        if (keySpel == null || keySpel.isEmpty()) {
            return null;
        }

        // 1. 尝试直接返回普通字符串(不包含 # 或 $)
        // 如果 keySpel 不包含 SpEL 标识符,无需解析,直接返回
        if (!keySpel.contains("#") && !keySpel.contains("$")) {
            return keySpel;
        }

        // 2. 使用 DefaultParameterNameDiscoverer 获取参数名(安全且不依赖 -parameters)
        ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
        String[] paramNames = discoverer.getParameterNames(signature.getMethod());

        // 如果仍然获取不到(极少数情况),使用备用方案:用 #p0, #p1 形式
        if (paramNames == null) {
            paramNames = new String[args.length];
            for (int i = 0; i < args.length; i++) {
                paramNames[i] = "p" + i; // 这样在 SpEL 中可以用 #p0, #p1 引用
            }
        }

        // 3. 构建 SpEL 上下文
        ExpressionParser parser = new SpelExpressionParser();
        EvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < paramNames.length; i++) {
            context.setVariable(paramNames[i], args[i]);
        }
        // 额外注入 #args 数组,方便直接取所有参数
        context.setVariable("args", args);

        // 4. 解析并返回
        return parser.parseExpression(keySpel).getValue(context, String.class);
    }
}

添加可锁定缓存功能以防止缓存击穿

  • 实现 LockCache 注解用于标记需要缓存保护的方法

  • 创建 LockCacheAspect 切面处理缓存逻辑和分布式锁机制

  • 集成 RedisTemplate 实现缓存存储和锁操作

  • 使用 Lua 脚本确保锁释放的原子性操作

  • 实现自旋锁机制和超时兜底策略

  • 支持 SpEL 表达式解析动态缓存键

  • 添加双重检查机制避免并发问题

  • 实现空值缓存防止缓存穿透

  • 添加DefaultParameterNameDiscoverer支持以安全获取参数名

  • 实现备用参数命名方案防止编译参数丢失导致解析失败

  • 增加普通字符串直接返回逻辑提升性能

  • 添加#args变量支持访问所有参数数组

  • 完善SpEL上下文构建流程确保参数正确绑定