假设一个场景,目录菜单下会有许多商品,我们需要对各菜单下的商品进行分页查询。
我一开始觉得非常简单,在查询方法上添加以下注解
@Cacheable(value = "product", key = "'menu_' + #request.menuId", condition = "#request.menuId != null", unless = "#result.list.size() == 0")
在增删改方法上添加以下注解
@CacheEvict(value = "product", key = "'menu_' + #request.menuId")
但很快发现了问题,因为其中不含page,意味着每次分页查询相同菜单目录时的key是不变的,这导致每次查询的结果都一致
于是我做了以下改动
@Cacheable(value = "product", key = "'menu_' + #request.menuId + ':' + #request.page + '_' + #request.pageSize", condition = "#request.menuId != null", unless = "#result.list.size() == 0")
查询的问题是解决了,但是增删改并不知道自己属于哪一页,这导致增删改全部失效,但@CacheEvict中唯一能在此情况下删除缓存的方法就是allEntries = true
@CacheEvict(value = "product", key = "'menu_' + #request.menuId", allEntries = true)
但是这会影响其他缓存,把所有缓存都删掉,因此使用@CacheEvict是无法解决该问题的
既然是对某个方法进行相应删除缓存操作,我们可以通过aop的方式自定义方法删除缓存
@Target({java.lang.annotation.ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheRemove {
String cacheName() default "";
String key() default "";
String prefix() default "";
}
cacheName用于指出缓存组件,key指定一个spEL表达式,通过spEL表达式获取方法参数,prefix表示key前缀字符,以防止单单存在的id会存在重复而误删其他缓存,prefix与key拼接表示需要删除的缓存对应的key
@Aspect
@Component
public class CacheRemoveAspect {
@Resource
RedisCache redisCache;
@Pointcut(value = "@annotation(com.yxy.xxx.annotation.CacheRemove)")
public void pointcut() {
}
@Around("pointcut()")
private Object process(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 1.获取注解参数值 SpEL表达式
CacheRemove cacheBatchEvict = methodSignature.getMethod().getAnnotation(CacheRemove.class);
String cacheName = cacheBatchEvict.cacheName();
String spEL = cacheBatchEvict.key();
if (!StringUtils.hasText(spEL)) {
return null;
}
// 2.获取目标方法参数
Object cacheKey = generateKeyListBySpEL(spEL, joinPoint);
if (cacheKey == null) {
return null;
}
String partKey = cacheBatchEvict.prefix() + cacheKey;
// 3.清除缓存
Collection<String> keys = redisCache.keys(cacheName + "::*");
for (String key : keys) {
if (key.contains(partKey)) {
redisCache.deleteObject(key);
}
}
// 4.执行目标方法
return joinPoint.proceed();
}
public String generateKeyListBySpEL(String spEL, ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 创建解析器
SpelExpressionParser parser = new SpelExpressionParser();
// 获取表达式
Expression expression = parser.parseExpression(spEL);
// 设置解析上下文
EvaluationContext context = new StandardEvaluationContext();
Object[] args = joinPoint.getArgs();
// 获取运行时参数名称
DefaultParameterNameDiscoverer discover = new DefaultParameterNameDiscoverer();
String[] parameterNames = discover.getParameterNames(method);
assert parameterNames != null;
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
// 解析
Object value = expression.getValue(context);
Assert.notNull(value, "SPEL解析错误");
return value.toString();
}
}
通过拼接key得到某类下的唯一标识再进行模糊查询删除
我们只需在增删改方法上加上该注解即可实现
@CacheRemove(cacheName = "product", key = "#request.menuId", prefix = "menu_")