现在的互联网公司大多数都是以Redis作为缓存,使用缓存的优点就不赘述了,写这篇文章的目的就是想帮助同学们如何在工作中更好的去实现缓存

目标

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

实现步骤

  • 自定义注解
  • 定义aop切面

思路:

  • 如果方法标注了@CacheProfiler注解则走aop
  • 如果获取到CacheProfiler类,并且readFromCache()设置的是true,就去getCacheKey()获取缓存的key
  • 根据缓存的key值去redis中查询,如果有就查询缓存,如果没有就执行方法,将方法的返回值作为value存入缓存,并根据CacheProfiler的expire()设置的过期时间给key加上过期时间

自定义注解

/**
* @Description:
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface CacheProfiler {
 
String cacheKey();
 
int expire() default 60;
 
boolean readFromCache() default true;
}

  

讲解下代码:

@interface来修饰一个注解

  • cacheKey() 缓存key的前缀,缓存key由key前缀+入参组成,详见getCacheKey()方法
  • expire() 缓存的过去时间,默认为60秒
  • 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();
}
}

  

   这个demo只是大概写了下,不过也可以实现本文的目标了,通过在方法上使用@CacheProfiler注解实现缓存,通过@CacheProfiler注解的相应参数去实现缓存属性的相关设置

写个Controller测试下

/**
 
* @Description:
 
 
*/
 
@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("明羽",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,就去缓存中读取托底数据
  • 一般的代码中都会try-cache,如果在方法里已经捕获了异常,这就需要在执行的方法里去主动的抛异常,让aop切面可以捕获异常
  • 其中重要的一点就是托底数据什么时候去存,这个可以在每次去存缓存的时候去存一份托底数据,或者定义一些存储策略,托底数据与缓存的数据可以定义为key的前缀不同【其实这个可以存成hash类型的数据,定义一个固定的key为托底数据的key【hash的key】,然后field为缓存的key】