现在的互联网公司大多数都是以redis作为缓存,本文分享如何在工作中更好的去实现缓存

目标

   在方法上使用注解,实现如果标注了注解的方法会优先走缓存,如果命中缓存则返回缓存中的数据,如果没有命中缓存就穿透到方法中执行方法,然后将方法的返回值存储到缓存中,然后下次就可以在缓存设置的有效时间内从缓存中读取数据了

实现步骤

  1. 自定义注解
  2. 定义aop切面

思路:

  1. 如果方法标注了@CacheProfiler注解则走aop
  2. 如果获取到CacheProfiler类,并且readFromCache()设置的是true,就去getCacheKey()获取缓存的key
  3. 根据缓存的key值去redis中查询,如果有就查询缓存,如果没有就执行方法,将方法的返回值作为value存入缓存,并根据CacheProfilerexpire()设置的过期时间给key加上过期时间
自定义注解
/**
 * @Description:
 * @author: chenmingyu
 * @date: 2018/5/17 20:37
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface CacheProfiler {

    String cacheKey();

    int expire() default 60;

    boolean readFromCache() default true;
}

代码

@interface来修饰一个注解

  1. cacheKey() 缓存key的前缀,缓存key由key前缀+入参组成,详见getCacheKey()方法
  2. expire()缓存的过去时间,默认为60
  3. readFromCache() 是否读取缓存,默认为true
定义aop切面
    @Pointcut("@annotation(org.my.cache.annotation.CacheProfiler)")
    public void cachePoint() {
    }

    @Around("cachePoint()")
    public Object beforeExec(ProceedingJoinPoint joinPoint) {
        Object obj = null;
        try {
            //获取方法
            Method method = this.getMethod(joinPoint);
            CacheProfiler cacheProfiler = (CacheProfiler) method.getAnnotation(CacheProfiler.class);
            //方法上没有注解,直接执行方法然后返回
            if (null == cacheProfiler) {
                return joinPoint.proceed();
            }
            //缓存key
            String cacheKey = this.getCacheKey(joinPoint, cacheProfiler);
            //true:从缓存读
            if (cacheProfiler.readFromCache()) {
                obj = jedis.get(cacheKey);
            } else {
                return joinPoint.proceed();
            }
            if (null == obj) {
                obj = joinPoint.proceed();
            }else{
                return obj;
            }
            if (null != obj) {
                jedis.setex(cacheKey,cacheProfiler.expire(), JSONObject.toJSONString(obj));
            }
            return obj;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
    }
        return obj;
    }

代码

@Pointcut定义切入点为@CacheProfiler注解@Around表示使用@CacheProfiler``注解的方法将走环绕通知 getMethod(JoinPoint jp)获取目标方法getCacheKey(ProceedingJoinPoint jp, CacheProfiler cacheProfiler)获取缓存的key

获取CacheProfiler的方法
//获取方法
private Method getMethod(JoinPoint jp) throws Exception {
    MethodSignature msig = (MethodSignature) jp.getSignature();
    Method method = msig.getMethod();
    return method;
}
获取缓存的key的方法
private String getCacheKey(ProceedingJoinPoint jp, CacheProfiler cacheProfiler) {
        StringBuilder sb = new StringBuilder(cacheProfiler.cacheKey());
        if (jp.getArgs() != null && jp.getArgs().length != 0) {
            Object[] arr$ = jp.getArgs();
            int len$ = arr$.length;

            for (int i$ = 0; i$ < len$; ++i$) {
                Object obj = arr$[i$];
                if (obj != null) {
                    sb.append("_").append(String.valueOf(obj));
                }
            }
            return sb.toString();
        } else {
            return sb.toString();
        }
    }

   通过在方法上使用@CacheProfiler注解实现缓存,通过@CacheProfiler注解的相应参数去实现缓存属性的相关设置

验证
/**
* @Description:
* @author: chenmingyu
* @date: 2018/5/20 11:18
*/
@RestController
public class CacheController {

   @RequestMapping("/getUser")
   @CacheProfiler(cacheKey = "USER_CACHE_KEY",expire = 3*60,readFromCache = true)
   public List<User> getUser(Integer type){

       List<User> users = new ArrayList<>();
       if(type==1){
           User user = new User("my",24,"北京");
           users.add(user);
       }else if(type==2){
           User user = new User("小娜",24,"北京");
           users.add(user);
       }
       return users;

   }

}

以上就实现了注解缓存

托底缓存的实现

   托底缓存的实现也很简单,首先说下可能需要托底缓存的场景,就比如一个电商网站,去获取商品列表,结果调用接口的时候出错了,这个时候又不希望网站的页面出现天窗这个时候就需要托底数据了,如果接口出现异常,也会返回托底数据

思路
  • 可以给CacheProfiler注解加一个属性是否读取托底缓存 属性为boolean
  • 然后在joinPoint.proceed()的时候加上try-cache
  • 如果执行方法的时候报异常了,或者返回一些自定义的数据,并且上面那个是否读取托底缓存属性为true,就去缓存中读取托底数据
  • 其中重要的一点就是托底数据什么时候去存,这个可以在每次去存缓存的时候去存一份托底数据,或者定义一些存储策略,托底数据与缓存的数据可以定义为key的前缀不同,其实这个可以存成hash类型的数据,定义一个固定的key为托底数据的key【hash的key】,然后field为托底缓存数据的key