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上下文构建流程确保参数正确绑定