简介
随着系统QPS逐渐提高,在一些读取频繁变更较少数据场景下,适当添加缓存不仅能提升用户访问速度还可以减轻系统压力。
使用缓存主要有两个用途:高性能、高并发
高性能:
-
查询mysql耗时需要300ms,存到缓存redis,每次查询仅需要几毫秒,性能瞬间提升百倍。
高并发
-
mysql 单机支撑2K QPS就容易报警了,使用缓存的话,单机支撑的并发量轻松1s几万~十几万。
缓存位于内存,内存对高并发的良好支持。
目的
通过统一缓存方式,规范缓存实现。
缓存命中走缓存。不命中加锁查询防止大量穿透引起雪崩;
通过统一注解方式,也减少了大量的冗余代码 。
减少重复,干掉重复。
本文主要讲解,一款基于自定义注解拦截+切面+redis的通用方法缓存方式 ;
支持场景
业务自定义的查询方法均可使用;
包括无参数的方法,有参数的方法;
可以根据不同的参数值做缓存 ;
技术位面
Redis
Redisson
fastjson
切面(Aspect)
序列化 (Serializable)
SpringBoot (示例工程)
功能示例图:
核心实现
实现原理简介
自定义注解 @CacheData ;
通过切面 CacheAspect 拦截添加自定义注解的方法 ;
通过参数生成Redis缓存key
-
解析方法的参数并按照参数顺序排序
将转换好的参数转换为Json字符串
-
本来想用toString的考虑到继承等没有重写方法就改用了Json
将参数进行MD5,一方面安全性,一方面保证Redis key不要过长,节约空间 ;
代码实现
1.自定义注解
/** * 自定义缓存注解 * * @author 程序员小强 * @see CacheAspect */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface CacheData {
/** * 自定义缓存key前缀 * 默认:方法名 */ String keyPrefix() default ""; /** * 缓存过期时间 * 单位默认:秒,不设置默认10分钟 */ int expireTime() default 600; /** * 缓存过期-随机数 * 单位默认:秒,默认0分钟 * 注:设置后实际过期时间, * 会在expireTime基础上继续累积(0~randomExpire)之间的秒数,防止缓存大量失效大面积穿透,造成雪崩 */ int randomExpire() default 0; /** * 是否存储为null 的返回 * 注:防止缓存穿透,默认true,建议查询为空时,也进行缓存 */ boolean storageNullFlag() default true;}
2.拦截切面实现
/** * 统一缓存自定义注解拦截实现 * * @author 程序员小强 */@Aspect@Componentpublic class CacheAspect {
private static final Logger log = LoggerFactory.getLogger(CacheAspect.class); private static final String EMPTY = ""; private static final String POINT = "."; private static final String CACHE_KEY_PREFIX = "cache.aspect:"; private static final String LOCK_KEY_PREFIX = "lock."; @Resource private RedisCache redisCache; /** * redisson client对象 */ @Resource private RedissonClient redisson; /** * 匹配所有使用以下注解的方法 * 注意:缓存是基于类+方法+参数内容做的缓存key,重载方法可能会出现问题 * 禁止在同一个类,方法名相同的两个方法使用 * * @see CacheData */ @Pointcut("@annotation(com.example.cache.annotation.CacheData)") public void pointCut() {
} /** * 拦截添加缓存注解的方法 * * @param pjpParam * @return * @throws Throwable * @see CacheData */ @Around("pointCut()&&@annotation(cacheData)") public Object logAround(ProceedingJoinPoint pjpParam, CacheData cacheData) throws Throwable {
//注解为空 if (null == cacheData) {
return pjpParam.proceed(pjpParam.getArgs()); } //仅用于方法 if (null == pjpParam.getSignature() || !(pjpParam.getSignature() instanceof MethodSignature)) {
return pjpParam.proceed(pjpParam.getArgs()); } //方法实例 Method method = ((MethodSignature) pjpParam.getSignature()).getMethod(); //方法类名 String className = pjpParam.getSignature().getDeclaringTypeName(); //方法名 String methodName = method.getName(); log.debug("[ CacheAspect ] >> notReadCacheFlag:{} , refreshCacheFlag :{} , method:{} ", CacheContext.notReadCacheFlag.get(), CacheContext.refreshCacheFlag.get(), className + methodNam