目录
Target
1.多级缓存生效注解
2.缓存上下文
3.责任链
Hander接口
责任链初始化
5.切面Aop
Coalbum项目
使用缓存
性能对比
Jmeter tips
Target
Caffeine+Redis构建多级缓存,采用责任链模式,使用aop+注解的方式增强目标方法。若缓存命中,返回命中值。若没命中,执行目标方法,将目标方法返回值依次存入缓存链。
1.多级缓存生效注解
1/** 2 * 多级缓存生效注解 3 */ 4@Target(ElementType.METHOD) 5@Retention(RetentionPolicy.RUNTIME) 6public @interface CacheableMultiLevel { 7 8 String keyPrefix() default "";// key 前缀 9 String key() default ""; // 缓存key,空key则计算hash 10 int localExpireSeconds() default 60; // 本地缓存过期时间(秒) 11 int redisExpireSeconds() default 300; // redis缓存过期时间(秒) 12 13}
2.缓存上下文
1/** 2 * 通用缓存上下文:CacheContext 3 */ 4@Data 5public class CacheContext { 6 7 private final String cacheKey; // 当前查询的缓存 key 8 private String cachedValue; // 当前 handler 返回的缓存值,命中则填充 9 private boolean hit = false; // 是否命中缓存 10 11 public void setCachedValue(String cachedValue) { 12 this.cachedValue = cachedValue; 13 this.hit = true; 14 } 15}
3.责任链
Hander接口
Caffeine忽略expireSeconds,无法指定各key的过期时间
RedisCacheHandler 设置expireSeconds+ RandomUtil.randomInt(0, 300) 防止缓存雪崩
put方法中 应链式调用 下级缓存的 put 方法
1/** 2 * 责任链接口:CacheHandler 3 */ 4public interface CacheHandler { 5 /** 6 * 尝试从当前缓存层获取数据 7 * @param context 缓存上下文,包含 key 和结果 8 * @return 如果命中缓存,设置 context.cachedValue 并返回 true;否则返回 false 9 */ 10 boolean handle(CacheContext context); 11 12 void setNext(CacheHandler next); 13 14 void put(String key, String jsonValue, int expireSeconds); 15} 16
责任链初始化
1@Component 2public class CacheHandlerChain { 3 4 @Autowired(required = false) 5 private LocalCacheHandler localCacheHandler; 6 7 @Autowired(required = false) 8 private RedisCacheHandler redisCacheHandler; 9 10 @Autowired(required = false) 11 private DefaultCacheHandler defaultCacheHandler; 12 13 @Getter 14 private CacheHandler chain; 15 16 @PostConstruct 17 public void init() { 18 List<CacheHandler> handlers = new ArrayList<>(); 19 if (localCacheHandler != null) handlers.add(localCacheHandler); 20 if (redisCacheHandler != null) handlers.add(redisCacheHandler); 21 if (defaultCacheHandler != null) handlers.add(defaultCacheHandler); 22 23 for (int i = 0; i < handlers.size() - 1; i++) { 24 handlers.get(i).setNext(handlers.get(i + 1)); 25 } 26 this.chain = handlers.isEmpty() ? defaultCacheHandler : handlers.get(0); 27 } 28 29}
5.切面Aop
优先使用注解中指定的key,为指定则计算方法参数的hash 作为key
- 命中,将json字符串反序列化为returnType类型的对象
- 未命中,目标方法执行后,将返回对象转为json字符串存入链中的各级缓存
TODO:解决缓存击穿,避免缓存突然失效导致 ES 压力飙升
1@Aspect 2@Component 3public class CacheableAspect { 4 5 @Autowired 6 private CacheHandlerChain cacheHandlerChain; 7 8 @Around("@annotation(cacheableMultiLevel)") 9 public Object around(ProceedingJoinPoint joinPoint, CacheableMultiLevel cacheableMultiLevel) throws Throwable { 10 String key = cacheableMultiLevel.key(); 11 if(StrUtil.isBlank(key)){ 12 //未指定key,使用hash值作为key 13 Object[] args = joinPoint.getArgs(); 14 String argJson = JSON.toJSONString(args); 15 key = DigestUtils.md5DigestAsHex(argJson.getBytes()); 16 } 17 String cacheKey = cacheableMultiLevel.keyPrefix()+key; 18 19 CacheContext context = new CacheContext(cacheKey); 20 CacheHandler chain = cacheHandlerChain.getChain(); 21 22 boolean hit = chain.handle(context); 23 // 获取方法签名 24 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); 25 26 // 获取目标方法对象 27 java.lang.reflect.Method method = signature.getMethod(); // 或 getDeclaredMethod() 28 Class<?> returnType = method.getReturnType(); 29 30 if (hit) { 31 System.out.println("[AOP] 缓存命中,直接返回"); 32 return JSON.parseObject(context.getCachedValue(),returnType); // 命中则直接返回 33 } 34 35 // 未命中,放行目标方法,由方法自己查数据源并写入缓存 36 System.out.println("[AOP] 缓存未命中,放行目标方法"); 37 Object res = joinPoint.proceed(); 38 String json = JSON.toJSONString(res); 39 chain.put(cacheKey,json,cacheableMultiLevel.redisExpireSeconds()); 40 return res; 41 } 42 43}
Coalbum项目
使用缓存
主页图片分页查询
1 /** 2 * ES作为数据源 3 */ 4 @PostMapping("/list/page/vo/search") 5 @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_VIEW) 6 @CacheableMultiLevel(keyPrefix = "coalbum:listPictureVOByPage:",redisExpireSeconds =300) 7 public BaseResponse<Page<PictureVO>> listPictureVOByPageFromES(@RequestBody PictureQueryRequest pictureQueryRequest) { 8 9 try { 10 //TODO 权限校验 11 //TODO 解决缓存击穿,避免缓存突然失效导致 ES 压力飙升 12 return ResultUtils.success(pictureSearchService.searchPicture(pictureQueryRequest)); 13 } catch (IOException e) { 14 throw new BusinessException(ErrorCode.SYSTEM_ERROR); 15 } 16 }
性能对比
线程数:50 ;second:1
ES直查:平均响应时间 30 ms

使用多级缓存:平均接口响应时间 14 ms

性能提升50%
Jmeter tips
请求头 携带cookies

《CoAlbum:多级缓存与性能对比》 是转载文章,点击查看原文。