缓存常常会有这种需求,就是根据不同的参数查询不同的列表缓存,但是只要更新了某个对象,那么这个对象相关的所有列表缓存都需要更新。
之前我是用注解的方式模糊搜索并删除缓存,
但是随着项目的使用量,数据,并发量日益庞大,连redis都开始成为瓶颈,这时候使用模糊查询会非常耗费资源,所以我研究了一下如何避免模糊查询删除列表数据。
有两种思路
第一种是专门维护缓存的键名,比如建立一个列表缓存 '前缀'+公司id,把所有相关的列表键名都保存在这个列表里,要更新数据时查到这个列表,然后遍历删除就行。这种方法很灵活,能适应很多的情况,不影响查询的代码,但是维护起来很麻烦,如果只往列表里加键名,那么可能会多很多无效的数据,并且太庞大了。如果要在缓存删除的时候同步这个key列表,那么可能产生并发的问题。或者可以建立一个定时器定时地遍历key列表查看里面的键名是否有效。总之会增加很多维护端的代码量
第二种是直接保存成map,保存缓存时先找到这个map,再把数据插入map,而数据更新时直接把这个map删掉就行了。这个方法维护很简单,跟其他一般的redis缓存一样使用@CacheEvict就行,需要修改的是查询端。
综合考虑,查询端比较单纯且集中,一般只用修改一个方法,而且不用考虑并发之类的,所以我选择第二种方法。
先实现redis存取map的方法
@Service
public class RedisServiceImpl implements IRedisService {
Logger logger=LoggerFactory.getLogger(this.getClass());
@Resource(name = "redisTemplate")
RedisTemplate<String, Object> redis;
/**
* 查询map的数据
* @param key
* @param hashKey
* @param value
* @param time
* @param unit
*/
@Override
public Object getHashValue(String key, String hashKey) {
Object object = redis.opsForHash().get(key, hashKey);
return object;
}
/**
* 更新map的数据
* @param key
* @param hashKey
* @param value
* @param time
* @param unit
*/
@Override
public void putHashValue(String key, String hashKey, Object value) {
redis.opsForHash().put(key, hashKey, value);
}
/**
* 更新map的数据(限时失效)
* @param key
* @param hashKey
* @param value
* @param time
* @param unit
*/
@Override
public void putHashValue(String key, String hashKey, Object value, Long time, TimeUnit unit) {
redis.opsForHash().put(key, hashKey, value);
if(time > 0) {
redis.expire(hashKey, time, unit);
}
}
自定义注解
@Target({ java.lang.annotation.ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheMap {
String key();//redis的key
String hashKey();//map的key
long time() default 0L;//缓存失效时间,为0则不失效
TimeUnit timeUnit() default TimeUnit.MINUTES;//缓存失效时间单位
}
aop切面
@Aspect
@Component
public class CacheMapAspect {
Logger logger=LoggerFactory.getLogger(this.getClass());
@Resource
IRedisService redisService;
@Around(value = "@annotation(com.example.CacheMap)")//使用环绕切入
private Object process(ProceedingJoinPoint joinPoint) throws Throwable{
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Object[] args = joinPoint.getArgs();//切面方法的参数
Method method = signature.getMethod();//切面方法
CacheMap cacheMap = method.getAnnotation(CacheMap.class);//获得注解
String key = parseKey(cacheMap.key(), method, args);//redis的key
String hashKey = parseKey(cacheMap.hashKey(), method, args);//map的key
Object hashValue = redisService.getHashValue(key, hashKey);
if(hashValue == null) {
hashValue = joinPoint.proceed();
long time = cacheMap.time();
if(time > 0) {
TimeUnit unit = cacheMap.timeUnit();
redisService.putHashValue(key, hashKey, hashValue, time, unit);
}else {
redisService.putHashValue(key, hashKey, hashValue);
}
}
return hashValue;
}
/**
* 获取缓存的key
* key 定义在注解上,支持SPEL表达式
* @param pjp
* @return
*/
private String parseKey(String key,Method method,Object [] args){
//获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscoverer u =
new LocalVariableTableParameterNameDiscoverer();
String [] paraNameArr=u.getParameterNames(method);
//使用SPEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
//SPEL上下文
StandardEvaluationContext context = new StandardEvaluationContext();
//把方法参数放入SPEL上下文中
for(int i=0;i<paraNameArr.length;i++){
context.setVariable(paraNameArr[i], args[i]);
}
List<String> pList = descFormat(key);//获取#p0这样的表达式
//将p0作为参数放入SPEL上下文中
for(String p:pList) {
context.setVariable(p.substring(1), args[NumberUtils.intValueOf(p.substring(2))]);
}
return parser.parseExpression(key).getValue(context,String.class);
}
/**
* 提取出#p[数字]这样的表达式
* @param desc
* @return
*/
private static List<String> descFormat(String desc){
List<String> list = new ArrayList<>();
Pattern pattern = Pattern.compile("#p[0-9]+");
Matcher matcher = pattern.matcher(desc);
while(matcher.find()){
String t = matcher.group(0);
list.add(t);
}
return list;
}
}
和@CacheRemove一样,这个注解需要加在实现类上,以权限为例,每个人员的模块权限不同,但是公司更新模块时,该公司的所有人员权限缓存都要删除
/**
* 根据当前登录的用户id查找当前显示的功能模块
*
* @param userId 用户的id
* @param companyId 公司id
* @return 正常返回当前的功能的模块的信息
* @throws Exception
*/
@Override
@LogServiceTrack
@CacheMap(key="'baseModulePermissionList'+#p1",hashKey="'baseModulePermissionList'+#p1+#p0")
public List<BaseModuleModel> getBaseModuleList(String userId, String companyId) throws Exception {
//1:校验当前的用户的id是否为空
{
if(null == userId){
throw new Exception("用户的id不能为空");
}
}
return baseModuleDao.getModuleList(userId, companyId);
}
删除缓存时使用@CacheEvict就行了
@Caching(evict= {
@CacheEvict(value="baseModulePermissionList",key="'baseModulePermissionList'+#p0"),
})
@Override
public void deletePermission(String companyId) {
}
不过使用map有一个坏处,就是不能灵活地根据不同的参数进行删除了,比如这个权限我只能根据公司来删除,而不能根据用户来删除,如果使用模糊删除的话,我可以使用'baseModulePermissionList‘+#companyId+‘*’来删除公司相关权限,使用'baseModulePermissionList'+'*'+#userId来删除用户的权限,如果使用第一种维护key的方法,我可以维护两套List。
所以一般情况下最好是使用在一些特定的独立的查询接口,不要跟其他元素交叉。当key的粒度较大时,比如公司id,更新可能会很频繁。
总之还是要根据实际情况选择不同的方案