利用spring的aop做一个类似mybatis二级缓存的自定义缓存

16 篇文章 0 订阅
13 篇文章 0 订阅

利用spring的aop做一个自定义缓存

最近对接口进行压测,发现在有限服务器资源的情况下,接口响应的效率并不高,所以需要进行优化;服务的处境是,要处理前端的业务逻辑,还要与数据库进行数据传输,所以优化要在5方面进行,

1.前端的合理代码,将前端能完成的逻辑,不请求到服务器上,从而减少消耗服务器资源
2.前端请求服务器接口的过程,数据结构要精简,少内容处理复杂内容
3.服务器则是需要对接口代码优化,逻辑优化等
4.服务器连接数据库的过程,应该合理配置数据库连接池
5.数据库优化,添加索引&约束等

以上五个方面都是需要考虑到的,前端的代码后端无法监督,数据结构和数据库也有dbm处理,因此后端需要关注基本的代码求优风格外,可以考虑的一点是,在应答前端和请求数据库的这两个过程进行优化,力求响应快,而少开数据库链接,这就要用上了缓存的技术,spring的缓存组件,还有中间件redis/mencache等,mybatis 有一级和二级缓存,一级缓存默认开启,处理最规律的那部分数据,二级缓存需要用户做相应的调整;

综上,选择用mybatis的二级缓存比较合适,但是目前对这个不是很熟,于是想起了类似做法,选择利用spring的aop技术来做一个自己的缓存(缓存空间用redis),参考了二级缓存的形式,不多说了,上代码:

1.添加缓存注解类

定义了属性的格式等

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MyCacheable {
	/**
	 *  keyWords : 组成唯一辨识的主键的字段
	 *  格式:
	 *    --"#objName.id" : 对象内的参数值
	 *    --"#id" : 直接参数
	 *    --"str" : 直接字符串
	 * @return
	 */
	String[] keyWords();
	/**
	 * 唯一标识主键所在组名称,用于清除缓存触发条件
	 *  格式:
	 *    --"#objName.id" : 对象内的值
	 *    --"#id" : 直接参数
	 *    --"str" : 直接字符串
	 *    --"#objName.id||#id||str" : 组合
	 * @return
	 */
	String keyGroupName() default "";
	/**
	 * 过期时间, 默认1
	 * @return
	 */
	int expiredTime() default 1;
	/**
	 * 过期时间单位, 默认 TimeUnit.HOURS : 小时
	 * @return
	 */
	TimeUnit expiredTimeUnit() default TimeUnit.HOURS;
}

2.处理添加缓存业务类

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.qisheng.common.util.RedisUtil;
import com.qisheng.reservation.annotation.CacheUtil;
import com.qisheng.reservation.annotation.MyCacheable;
import lombok.extern.slf4j.Slf4j;

/**
* Description: 自定义请求缓存AOP类
* @author: wyh 
* @date 2020年7月8日
 */
@Aspect
@Component
@Slf4j
public class MyCacheableForReservationAnnotation {
	
	@Resource
	RedisUtil redisUtil;
	
	// 环绕
	@Around(value = "@annotation(MyCacheable)")
	public Object dealWithReservationCache(ProceedingJoinPoint joinPoint, MyCacheable MyCacheable) throws Throwable {
		
		// 获取传参
		JSONObject jsonObj = JSONObject.parseObject(JSON.toJSONString(joinPoint.getSignature()));
		
		// 参数名称集
		JSONArray parameterNames = jsonObj.getJSONArray("parameterNames");
		// 参数类型集
		JSONArray parameterTypes = jsonObj.getJSONArray("parameterTypes");
		// 返参类型
		String returnType = jsonObj.getString("returnType");
		// 返参类
		Object resp = Class.forName(returnType).newInstance();
		// 值集
		JSONArray getArgs = JSONArray.parseArray(JSON.toJSONString(joinPoint.getArgs()));
		// 组成主键字集
		String[] keyWords = MyCacheable.keyWords();
		// 缓存组
		String keyGroupName = MyCacheable.keyGroupName();
		
		// 请求域
//		ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//		HttpServletRequest request = attributes.getRequest();
		
		log.info("**-------MyCachable--------** -------------keyWords : {}", JSON.toJSONString(keyWords));
		log.info("**-------MyCachable--------** -------------getArgs : {}", JSON.toJSONString(getArgs));
		log.info("**-------MyCachable--------** -------------parameterTypes : {}", JSON.toJSONString(parameterTypes));
		log.info("**-------MyCachable--------** -------------keyGroupName {}", keyGroupName);
		/**--组key--*/
		StringBuffer key = new StringBuffer();
		String keyV = null;
		for(String keyName : keyWords) {
			keyV = CacheUtil.getKeyValue(keyName, parameterNames, parameterTypes, getArgs);
			key.append(keyV);
			keyV = null;
		}
		// 加入主键组
		if(key != null && !key.toString().isEmpty() && !keyGroupName.isEmpty()) {
			// 获取缓存主键组
			String keyTemp = CacheUtil.getKeyValue(keyGroupName, parameterNames, parameterTypes, getArgs);
			log.info("**-------MyCachable--------** -------------group key {}", keyTemp);
			// 加入分组名称前缀
			if(keyTemp != null && !keyTemp.isEmpty()) {
				String keyStr = keyTemp+"-"+key.toString();
				key.delete(0, key.length());
				key.append(keyStr);
			}
			// 追加key 到缓存组
			List<String> list = redisUtil.getObjList(keyTemp, String.class);
			if(!CollectionUtils.isEmpty(list)) {
				log.info("**-------MyCachable--------** -------------group key list : {}", JSON.toJSONString(list));
				if(!list.contains(key.toString())) {
					list.add(key.toString());
					redisUtil.setObjList(keyTemp, list);
				}
			}else {
				List<String> keyGroups = new ArrayList<String>();
				keyGroups.add(key.toString());
				redisUtil.setObjList(keyTemp, keyGroups);
			}
			
		}
		log.info("**-------MyCachable--------** -------------key {}", key.toString());
		// 缓存是否存在
		String redisValue = redisUtil.get(key.toString());
		
		if(redisValue != null && !redisValue.toString().isEmpty()){
			// 存在-返回缓存
			JSONObject respJson = JSONObject.parseObject(redisValue);
			// 根据返回值必须有的自定义字段 timestamp & metaTs
			respJson.put("timestamp", null);
			respJson.put("metaTs", null);
			resp = JSONObject.parseObject(JSON.toJSONString(respJson), resp.getClass());
			return resp;
		}
		Object object = joinPoint.proceed();
		// 加入缓存
		if(key != null && !key.toString().isEmpty()) {
			redisUtil.setStr(key.toString(), JSON.toJSONString(object), MyCacheable.expiredTime(), MyCacheable.expiredTimeUnit());
		}
		
		// 没有需要处理则执行后面的操作
		return object;
	}
	
}

3.清除缓存注解类

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MyCacheEvict {
	/**
	 *  keyWords : 组成唯一辨识的主键的字段
	 *  格式:
	 *    --"#objName.id" : 对象内的参数值
	 *    --"#id" : 直接参数
	 *    --"str" : 直接字符串
	 * @return
	 */
	String[] keyWords() default "";
	/**
	 * 唯一标识主键所在组名称,用于清除缓存触发条件
	 *  格式:
	 *    --"#objName.id" : 对象内的值
	 *    --"#id" : 直接参数
	 *    --"str" : 直接字符串
	 *    --"#objName.id||#id||str" : 组合
	 * @return
	 */
	String keyGroupName() default "";
}

4.处理清除缓存业务类

import java.util.List;
import javax.annotation.Resource;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.qisheng.common.util.RedisUtil;
import com.qisheng.reservation.annotation.CacheUtil;
import com.qisheng.reservation.annotation.MyCacheEvict;
import lombok.extern.slf4j.Slf4j;

/**
* Description: 自定义清楚缓存AOP类
* @author: wyh 
* @date 2020年7月11日
 */
@Aspect
@Component
@Slf4j
public class MyCacheEvictForReservationAnnotation {
	
	@Resource
	RedisUtil redisUtil;
	
	// 之后
	@After(value = "@annotation(MyCacheEvict)")
	public void dealWithReservationCache(JoinPoint joinPoint, MyCacheEvict MyCacheEvict) throws Throwable {
		
		// 获取传参
		JSONObject jsonObj = JSONObject.parseObject(JSON.toJSONString(joinPoint.getSignature()));
		// 参数名称集
		JSONArray parameterNames = jsonObj.getJSONArray("parameterNames");
		// 参数类型集
		JSONArray parameterTypes = jsonObj.getJSONArray("parameterTypes");
		// 值集
		JSONArray getArgs = JSONArray.parseArray(JSON.toJSONString(joinPoint.getArgs()));
		// 组成主键字集
		String[] keyWords = MyCacheEvict.keyWords();
		// 缓存组
		String keyGroupName = MyCacheEvict.keyGroupName();
		
		// 请求域
//		ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//		HttpServletRequest request = attributes.getRequest();
		
		log.info("**-------MyCacheEvict--------** -------------keyWords : {}", JSON.toJSONString(keyWords));
		log.info("**-------MyCacheEvict--------** -------------getArgs : {}", JSON.toJSONString(getArgs));
		log.info("**-------MyCacheEvict--------** -------------parameterTypes : {}", JSON.toJSONString(parameterTypes));
		log.info("**-------MyCacheEvict--------** -------------keyGroupName {}", keyGroupName);
		/**--组key--*/
		StringBuffer key = new StringBuffer();
		String keyV = null;
		for(String keyName : keyWords) {
			if(keyName == null || keyName.isEmpty()) continue;
			keyV = CacheUtil.getKeyValue(keyName, parameterNames, parameterTypes, getArgs);
			key.append(keyV);
			keyV = null;
		}
		if(keyGroupName.isEmpty()) {
			// 主键组名称为空,则只清掉当前主键缓存
			if(key != null && !key.toString().isEmpty()) {
				redisUtil.delete(key.toString());
				log.info("**-------MyCacheEvict--------** -------------delete “{}” cache", key.toString());
			}
		}else {
			// 主键组名称非空
			// 获取缓存主键组
			String keyTemp = CacheUtil.getKeyValue(keyGroupName, parameterNames, parameterTypes, getArgs);
			log.info("**-------MyCacheEvict--------** -------------group key {}", keyTemp);
			// 加入分组名称前缀
			if(key != null && !key.toString().isEmpty() && keyTemp != null && !keyTemp.isEmpty()) {
				String keyStr = keyTemp+"-"+key.toString();
				key.delete(0, key.length());
				key.append(keyStr);
			}
			// 缓存里主键组对象
			List<String> list = redisUtil.getObjList(keyTemp, String.class);
			log.info("**-------MyCacheEvict--------** -------------group key list : {}", JSON.toJSONString(list));
			if(!CollectionUtils.isEmpty(list)) {
				if(key == null || key.toString().isEmpty()) {
					// 指定主键为空,则清掉数组里全部主键缓存
					for(String keyStr : list) {
						redisUtil.delete(keyStr);
						log.info("**-------MyCacheEvict--------** -------------delete “{}” cache", keyStr);
					}
					redisUtil.delete(keyTemp);
					log.info("**-------MyCacheEvict--------** -------------delete group “{}” cache", keyTemp);
				}else {
					// 指定主键非空,则清组里面这个主键的缓存
					redisUtil.delete(key.toString());
					log.info("**-------MyCacheEvict--------** -------------delete “{}” cache", key.toString());
					if(list.contains(key.toString())) {
						list.remove(key.toString());
						redisUtil.setObjList(keyTemp, list);
					}
				}
			}
			
		}
	}
	
}

5.数据格式工具类

用了redis做缓存空间,因此需对key做处理,然后RedisUtil 工具类是封装了spring容器的redis实例,这里不给出,自己配置和封装;

import java.lang.reflect.Method;
import java.util.Date;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

public class CacheUtil {
	/**
	 *  获取主键key 格式化
	 * @param keyName
	 * @param parameterNames
	 * @param parameterTypes
	 * @param getArgs
	 * @return
	 * @throws Exception
	 */
	public static String getKeyValue(
			String keyName, 
			JSONArray parameterNames, 
			JSONArray parameterTypes, 
			JSONArray getArgs
			) throws Exception {
		if(keyName == null || keyName.isEmpty()) {
			throw new Exception("keyGroupName||keyWords cannot be empty");
		}
		StringBuilder key = new StringBuilder();

		String[] strs = null;
		if(keyName.contains("||")) {
			strs = keyName.split("\\|\\|");
		}else {
			strs = new String[]{keyName};
		}
		boolean isFeild = false;
		String parameterName = null;
		String objName = null;
		String parameterType = null;
		String args = null;
		String[] strsTemp = null;
		int count = 0;
		Object obj = null;
		String getName = null;
		Object value = null;
		for(String str : strs) {
			count = 0;
			if(!str.contains("#")) {
				// 直接字符串
				key.append(str);
			}else {
				if(str.contains(".")) {
					// 对象下的参数
					str = str.replace("#", "");
					strsTemp = str.split("\\.");
					objName = strsTemp[0];
					parameterName = strsTemp[1];
					for(Object str2 : parameterNames) {
						// 找到对象
						if(str2.toString().equals(objName)) {
							parameterType = parameterTypes.getString(count);
							obj = Class.forName(parameterType).newInstance();
							obj = JSONObject.parseObject(getArgs.getString(count), obj.getClass());
							getName = "get" + parameterName.substring(0, 1).toUpperCase() + parameterName.substring(1);
							Method method = obj.getClass().getMethod(getName, new Class[] {});
							value = method.invoke(obj, new Object[]{});
							if(value instanceof Date) {
								value = ((Date) value).getTime();
							}
							key.append(parameterName + (value== null ? "null":value.toString()));
							break;
						}
						count++;
					}
				}else {
					// 直接参数
					str = str.replace("#", "");
					for(int i = 0; i < parameterNames.size(); i++) {
						parameterName = parameterNames.getString(i);
						if(str.equals(parameterName)) {
							isFeild = true;
							break;
						}
						count++;
					}
					if(isFeild) {
						args = getArgs.getString(count);
						key.append(parameterName+args);
						isFeild = false;
					}else {
						throw new Exception("wrong : feild name " + str + " not exist");
					}

				}
			}
		}
		parameterName = null;
		objName = null;
		parameterType = null;
		args = null;
		strsTemp = null;
		obj = null;
		getName = null;
		value = null;
		return key.toString();
	}
}

6.实现

1)注解添加缓存
在这里插入图片描述

2)注解清除缓存
在这里插入图片描述

7.在相同特定压测环境下,有自定义缓存接口、有mybatis二级缓存接口和一般请求接口的性能对比

自定义缓存 :
myCacheRequestNumMax - 0异常比较稳定容纳的请求数上限
myCacheUsedTime - myCacheRequestNumMax 完成需要的耗时
myCacheExceptionRate - 超限请求异常稳定性

mybatis二级缓存:
batisCacheRequestNumMax - 0异常比较稳定容纳的请求数上限
batisCacheUsedTime - batisCacheRequestNumMax 完成需要的耗时
batisCacheExceptionRate - 超限请求异常稳定性

一般请求:
requestNumMax - 0异常比较稳定容纳的请求数上限
usedTime - requestNumMax 完成需要的耗时
exceptionRate - 超限请求异常稳定性

1). 自定义缓存 VS mybatis缓存:

myCacheRequestNumMax = 2.5batisCacheRequestNumMax - - - 优
myCacheUsedTime = 2
batisCacheUsedTime - - - 差
myCacheExceptionRate < batisCacheExceptionRate - - - 差

1). 自定义缓存 VS 一般请求:

myCacheRequestNumMax = 2requestNumMax - - - 优
myCacheUsedTime = 0.5
usedTime - - - 优
myCacheExceptionRate < exceptionRate - - - 差

综上,自定义缓存的接口,在0异常稳定相应请求方面,优于另外两者;耗时方面,是mybatis的2倍,但却是一般请求接口耗时的一半,比较中性;超出上限后异常的表现,自定义缓存则显得比较飘,有时低,有时高,幅度大,不那么稳定;

8.弊端

正如第7点,其中一个弊端是稳定性差,其他的问题当然要经过实践去了解了;

9.使用原则

  1. 对单表操作;
  2. 查询的操作远大于更新的操作;

大家有什么想法,欢迎提出来

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Retank

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值