Redis缓存自定义注解实现

1,141 阅读2分钟

对于大量数据的查询需要使用缓存进行优化,如何实现一个自定义注解实现Redis缓存呢?

自定义注解 @Cache

先考虑缓存同步流程,当读取数据时,会先从缓存中,查看是否存在,存在则直接读取,不存在则从数据库中读取,读取到数据再写入缓存中;当对数据进行增改时,需要将缓存中相关数据设置失效;

缓存有两部分构成,读缓存和缓存失效;

首先定义一个自定义注解@Cache:

  • cacheValid标识缓存状态(缓存有效还是失效);

  • expireTime设置过期时间;

  • 为防范缓存雪崩可以expireMinTime、expireMaxTime设置最小最大过期时间;

  • expireUnit设置过期时间单位;

  • 缓存失效需要设置对应的失效数据方法,保证相关数据缓存失效;

    • thisExpireMethods设置当前类的失效数据方法,默认当前类下数据全部失效
    • otherExpireMethods设置其他类的失效数据方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
    enum CacheValid{ VALID, INVALID};
    CacheValid cacheValid() default CacheValid.VALID;//设置缓存有效、无效
    int expireTime() default 60;//过期时间
    int expireMinTime() default -1;//最小过期时间
    int expireMaxTime() default -1;//最大过期时间
    TimeUnit expireUnit() default TimeUnit.MINUTES;//过期时间单位
    String[] thisExpireMethods() default {"*"};//当前类的失效数据方法
    InValidMethod[] otherExpireMethods() default {};//其他类的失效数据方法
}

元注解@Target表示该注解使用的位置,类、方法、参数还是实例上; 元注解@Retention表示该注解的作用域,编译时、运行时等;

@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface InValidMethod {
    Class inValidClass();
    String[] methods();
}

定义切面类CacheAspect

  1. 将数据存入缓存,需要设置key,类名+方法名+参数可以唯一标识一组数据;通过JoinPoint获取类信息、方法信息、参数信息;
// 3. 环绕通知
@Around(value = "cachePointCut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    String targetClass = joinPoint.getTarget().getClass().getName();
    Signature signature = joinPoint.getSignature();
    String methodName = joinPoint.getSignature().getName();
    StringBuilder args = new StringBuilder();
    for (int i = 0; i < joinPoint.getArgs().length; i++) {
        args.append(i + joinPoint.getArgs()[i].toString() + ";");
    }
    String key = targetClass + ":" + methodName;
  1. 下一步需要的@Cache是缓存有效还是缓存无效,进行进一步的逻辑处理;

    获取@Cache注解信息,可以通过反射得到;

Method method = ((MethodSignature) signature).getMethod();
Method methodWithAnnotations = joinPoint.getTarget().getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
Cache cache = methodWithAnnotations.getDeclaredAnnotation(Cache.class);
  1. 缓存有效:读缓存\写缓存;

    缓存失效:设置缓存过期;

switch (cache.cacheValid()) {
    case VALID:
        if (objectRedisTemplate.opsForHash().hasKey(key, args.toString())) {
            //读缓存
            return readCache(cache, key, args.toString());
        }
        synchronized (cache) {
            if (objectRedisTemplate.opsForHash().hasKey(key, args.toString())) {
                //读缓存
                return readCache(cache, key, args.toString());
            }
            Object proceed = joinPoint.proceed();
            //写缓存
            writeCache(cache, key, args.toString(), proceed);
            return proceed;
        }
    case INVALID:
        Object proceed1 = joinPoint.proceed();
        //缓存失效
        invalidCache(cache, targetClass);
        return proceed1;
}

写缓存使用双重检查锁,防止缓存击穿,保证同一个数据的在同一时刻的访问只有一个查询读数据库写缓存,其他查询读缓存;也有效解决了缓存雪崩的问题; 如果应用于分布式系统,可以使用Redisson分布式锁替代synchronized;

为防止缓存穿透,当读取数据库数据为null时,也应该写入到缓存中;

  1. 缓存通过Redis的hash数据结构进行存储,类名+方法名为key,参数为hash_key,查询数据为value;
//读缓存
private Object readCache(Cache cache, String key, String args){
    System.out.println("读缓存...");
    Object o = objectRedisTemplate.opsForHash().get(key, args);
    stringRedisTemplate.expire(key, getExpireTime(cache), cache.expireUnit());
    return o;
}
//写缓存
private void writeCache(Cache cache, String key, String args, Object proceed){
    System.out.println("写缓存...");
    objectRedisTemplate.opsForHash().put(key, args, proceed);
    stringRedisTemplate.expire(key, getExpireTime(cache), cache.expireUnit());
}
//读取过期时间
private int getExpireTime(Cache cache){
    int expireTime;
    if(cache.expireMinTime() != -1 && cache.expireMaxTime() != -1){
        expireTime = cache.expireMinTime() + new Random().nextInt(cache.expireMaxTime()-cache.expireMinTime());
    } else{
        expireTime = cache.expireTime();
    }
    return expireTime;
}
  1. 缓存失效,读取cache信息获取指定的失效key,有类名和方法名组成,设置过期时间为-1进行失效;
//缓存失效
private void invalidCache(Cache cache, String targetClass){
    System.out.println("缓存失效...");
    Set<String> keys = null;
    if (cache.thisExpireMethods().length == 1 && cache.thisExpireMethods()[0] == "*") {
        keys = stringRedisTemplate.keys(targetClass+"*");
    }else {
        keys = new HashSet<>();
        for (String method : cache.thisExpireMethods()) {
//                keys.addAll(stringRedisTemplate.keys(targetClass+":"+method+"*"));
            keys.add(targetClass+":"+method);
        }
    }
    if(cache.otherExpireMethods().length != 0){
        for (InValidMethod inValidMethod : cache.otherExpireMethods()) {
            for (String method : inValidMethod.methods()) {
//                    keys.addAll(stringRedisTemplate.keys(inValidMethod.inValidClass().getName()+":"+method+"*"));
                keys.add(inValidMethod.inValidClass().getName()+":"+method);
            }
        }
    }
    for (String key : keys) {
        stringRedisTemplate.expire(key, -1, TimeUnit.MINUTES);
    }
}

应用于方法上

自定义注解和切面类都实现好了,进行一个简单的应用;

  1. 缓存注解在getStock方法上,设置缓存有效、过期时间30分钟;
@Cache(cacheValid = Cache.CacheValid.VALID, expireTime = 30)
public List<Stock> getStock(String name) {
    return stockDAO.getStock(name);
}
  1. 缓存注解在addStock方法上,设置缓存失效,设置失效方法为当前类的getStock方法以及overServiceImpl类的kill方法;
@Cache(cacheValid = Cache.CacheValid.INVALID, thisExpireMethods = {"getStock"}, otherExpireMethods = {@InValidMethod(inValidClass = OrderServiceImpl.class, methods = {"kill"})})
public int addStock(String name, Integer count) {
    Stock stock = new Stock();
    stock.setName(name);
    stock.setSale(0);
    stock.setCount(count);
    stock.setVersion(0);
    stockDAO.addStock(stock);
    stockInRedis(stock);
    return stock.getId();
}