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

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

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

