利用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 = 2batisCacheUsedTime - - - 差
myCacheExceptionRate < batisCacheExceptionRate - - - 差
1). 自定义缓存 VS 一般请求:
myCacheRequestNumMax = 2requestNumMax - - - 优
myCacheUsedTime = 0.5usedTime - - - 优
myCacheExceptionRate < exceptionRate - - - 差
综上,自定义缓存的接口,在0异常稳定相应请求方面,优于另外两者;耗时方面,是mybatis的2倍,但却是一般请求接口耗时的一半,比较中性;超出上限后异常的表现,自定义缓存则显得比较飘,有时低,有时高,幅度大,不那么稳定;
8.弊端
正如第7点,其中一个弊端是稳定性差,其他的问题当然要经过实践去了解了;
9.使用原则
- 对单表操作;
- 查询的操作远大于更新的操作;
大家有什么想法,欢迎提出来