缓存常常会有这种需求,就是根据不同的参数查询不同的列表缓存,但是只要更新了某个对象,那么这个对象相关的所有列表缓存都需要更新。

之前我是用注解的方式模糊搜索并删除缓存,


但是随着项目的使用量,数据,并发量日益庞大,连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,更新可能会很频繁。

总之还是要根据实际情况选择不同的方案