对于大量数据的查询需要使用缓存进行优化,如何实现一个自定义注解实现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
- 将数据存入缓存,需要设置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;
-
下一步需要的@Cache是缓存有效还是缓存无效,进行进一步的逻辑处理;
获取@Cache注解信息,可以通过反射得到;
Method method = ((MethodSignature) signature).getMethod();
Method methodWithAnnotations = joinPoint.getTarget().getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
Cache cache = methodWithAnnotations.getDeclaredAnnotation(Cache.class);
-
缓存有效:读缓存\写缓存;
缓存失效:设置缓存过期;
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时,也应该写入到缓存中;
- 缓存通过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;
}
- 缓存失效,读取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);
}
}
应用于方法上
自定义注解和切面类都实现好了,进行一个简单的应用;
- 缓存注解在getStock方法上,设置缓存有效、过期时间30分钟;
@Cache(cacheValid = Cache.CacheValid.VALID, expireTime = 30)
public List<Stock> getStock(String name) {
return stockDAO.getStock(name);
}
- 缓存注解在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();
}