现在的互联网公司大多数都是以
redis
作为缓存,本文分享如何在工作中更好的去实现缓存
目标
在方法上使用注解,实现如果标注了注解的方法会优先走缓存,如果命中缓存则返回缓存中的数据,如果没有命中缓存就穿透到方法中执行方法,然后将方法的返回值存储到缓存中,然后下次就可以在缓存设置的有效时间内从缓存中读取数据了
实现步骤
- 自定义注解
- 定义aop切面
思路:
- 如果方法标注了
@CacheProfiler
注解则走aop
- 如果获取到
CacheProfiler
类,并且readFromCache()设置的是true,就去getCacheKey()
获取缓存的key
- 根据缓存的
key
值去redis
中查询,如果有就查询缓存,如果没有就执行方法,将方法的返回值作为value
存入缓存,并根据CacheProfiler
的expire()
设置的过期时间给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
来修饰一个注解
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();
}
}
通过在方法上使用@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