我们项目开发过程中,在实现功能的情况之下对其进行优化是必不可少的,其中一种优化方案就是做数据缓存,对数据做缓存可以减少对数据库的访问压力,在访问量逐步增大的情况下可以分流一部分数据库的压力,对客户端而言,最直观的变化就是请求响应时间变短。
我在设想之初就想通过aop+Redis的形式来实现数据缓存,参阅借鉴了很多资料,结合自身项目需求做了这个设计。
一.设计两个注解
package com.htt.app.cache.annotation;
import com.htt.app.cache.enums.CacheSource;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 缓存读取
* Created by sunnyLu on 2017/7/18.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AopCacheable {
Class type();//反序列化的类型
Class[] keys();//对应redis中的key
int expires() default 0;//过期时间
CacheSource source();//来源 例:pledge_service
}
package com.htt.app.cache.annotation;
import com.htt.app.cache.enums.CacheSource;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 缓存释放
* Created by sunnyLu on 2017/7/18.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AopCacheRelease {
Class[] keys();//对应redis中的key
CacheSource source();//来源 例:pledge_service
}
两个注解都是方法级别,在接口上修饰,通过spring aop拦截接口。
@AopCacheable是读取已经存储缓存的注解会修饰在读取的接口上,
@AopCacheRelease是释放缓存的注解会修饰在写入操作的接口上用于更新缓存
定义了四个属性
1.type为反序列化的类型,我们的缓存数据是以json格式缓存在redis中的,这个type就是方法的返回值类型,当aop通知发现redis中存在对这个接口的缓存时,会已这个type类型来作为反序列化的类型
2.keys对应redis中的key这里须要说明的是redis中的key类型我采用的是hash,其有一个特点key-value,一个key可以对应多个value,在redis中value的表述形式是filed。
这里的keys是个Class数组类型对应缓存数据所属表对应的Entity(一张数据表对应一个Entity实体)
在redis中key的组合规则是keys+方法名+source filed的组合规则是类名+方法名+参数值,这可以唯一标识一次对数据库的查询
3.expires为过期时间
4.source是缓存来源,在分布式架构中一般一个应用会对应许多的服务,而缓存也可能来自不同的服务,source的目的是保证服务于服务之间无论是读取还是释放缓存互不影响
二.切面类编写
package com.htt.app.cache.aspect;
import com.htt.app.cache.utils.FastJsonUtils;
import com.htt.app.cache.utils.JedisUtils;
import com.htt.app.cache.annotation.AopCacheable;
import com.htt.app.cache.annotation.AopCacheRelease;
import com.htt.app.cache.enums.CacheSource;
import com.htt.app.cache.exception.CacheException;
import com.htt.framework.util.PagingResult;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import java.util.List;
import java.util.Map;
/**
* 缓存切面类
* Created by sunnyLu on 2017/7/18.
*/
@Aspect
public class CacheAspect {
@Pointcut(value = "@annotation(cacheRead)",argNames = "cacheRead")
public void pointcut(AopCacheable cacheRead){}
@Pointcut(value = "@annotation(cacheRelease)",argNames = "cacheRelease")
public void pointcut2(AopCacheRelease cacheRelease){}
@Around(value = "pointcut(cacheRead)")
public Object readCache(ProceedingJoinPoint jp,AopCacheable cacheRead) throws Throwable{
Class[] keyClasses = cacheRead.keys();
if (cacheRead.source() == null){
throw new CacheException("the annotation '"+cacheRead.getClass().getSimpleName()+"' must be contains the attribute source");
} else if (keyClasses == null || keyClasses.length == 0){
throw new CacheException("the annotation '"+cacheRead.getClass().getSimpleName()+"' must be contains the attribute keys");
}
// 得到类名、方法名和参数
String clazzName = jp.getTarget().getClass().getName();
String methodName = jp.getSignature().getName();
Object[] args = jp.getArgs();
// 根据类名,方法名和参数生成field
String field = genFiled(clazzName, methodName, args);
// 生成key
String key = genKey(keyClasses,methodName,cacheRead.source());
// result是方法的最终返回结果
Object result = null;
// 检查redis中是否有缓存
if (!JedisUtils.isExists(key,field,JedisUtils.DATA_BASE)) {
// 缓存未命中
// 调用数据库查询方法
result = jp.proceed(args);
// 序列化查询结果
String json = FastJsonUtils.parseJson(result);
// 序列化结果放入缓存
if (cacheRead.expires() > 0){
JedisUtils.hsetexToJedis(key,field,json,cacheRead.expires(),JedisUtils.DATA_BASE);
} else {
JedisUtils.hsetToJedis(key, field, json, JedisUtils.DATA_BASE);
}
} else {
// 缓存命中
// 得到被代理方法的返回值类型
Class returnType = ((MethodSignature) jp.getSignature()).getReturnType();
// 反序列化从缓存中拿到的json
String jsonString = JedisUtils.getFromJedis(key,field,JedisUtils.DATA_BASE);
result = deserialize(jsonString, returnType, cacheRead.type());
}
return result;
}
private Object deserialize(String jsonString, Class returnType, Class modelType) {
// 序列化结果应该是List对象
if (returnType.isAssignableFrom(List.class)) {
return FastJsonUtils.JsonToList(jsonString,modelType);
} else if (returnType.isAssignableFrom(Map.class)){
return FastJsonUtils.JsonToMap(jsonString);
} else if (returnType.isAssignableFrom(PagingResult.class)){
return FastJsonUtils.JsonToPagingResult(jsonString,modelType);
} else {
// 序列化
return FastJsonUtils.JsonToEntity(jsonString,returnType);
}
}
@AfterReturning(value = "pointcut2(cacheRelease)")
public void releaseCache(AopCacheRelease cacheRelease){
//得到key
Class[] keys = cacheRelease.keys();
if (keys == null || keys.length == 0){
throw new CacheException("the annotation '"+cacheRelease.getClass().getSimpleName()+"' must be contains the attribute keys");
} else if (cacheRelease.source() == null){
throw new CacheException("the annotation '"+cacheRelease.getClass().getSimpleName()+"' must be contains the attribute source");
}
// 清除对应缓存
JedisUtils.delPatternKeys(JedisUtils.DATA_BASE, keys,cacheRelease.source());
}
/**
* 根据类名、方法名和参数生成filed
* @param clazzName
* @param methodName
* @param args 方法参数
* @return
*/
private String genFiled(String clazzName, String methodName, Object[] args) {
StringBuilder sb = new StringBuilder(clazzName).append(".").append(methodName);
for (Object obj : args) {
if (obj != null)
sb.append(".").append(obj.toString());
}
return sb.toString();
}
/**
* 根据类名;来源生成key
* @param source
* @return
*/
private String genKey(Class[] keyClasses,String methodName,CacheSource source){
StringBuilder sb = new StringBuilder(source.getDes()).append(".").append(methodName);
for (Class clazz : keyClasses){
sb.append(".").append(clazz.getSimpleName());
}
return sb.toString();
}
}
三.utils包装redis的各种操作
package com.htt.app.cache.utils;
import com.htt.app.cache.enums.CacheSource;
import com.htt.framework.util.PropertiesUtils;
import com.htt.framework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* jedis缓存工具类
* Created by sunnyLu on 2017/5/27.
*/
public class JedisUtils {
public final static int DATA_BASE = 2;
private final static String HTT = "HTT_";
public final static Integer ONE_DAY_CACHE=3600*24;
public final static Integer THREE_DAY_CACHE=3600*24*3;
static final Map<Integer, JedisPool> pools = new HashMap();
static String host = PropertiesUtils.getProperty("redis.host");
static String sPort = PropertiesUtils.getProperty("redis.port");
static int port = 6379;
static String password;
static String sTimeout;
static int timeout;
static JedisPoolConfig jedisPoolConfig;
public JedisUtils() {
}
public static Jedis getJedis(int database) {
JedisPool pool = (JedisPool)pools.get(Integer.valueOf(database));
if(pool == null) {
pool = new JedisPool(jedisPoolConfig, host, port, timeout, password, database);
pools.put(Integer.valueOf(database), pool);
}
Jedis jedis = pool.getResource();
return jedis;
}
public static void returnResource(int database, Jedis jedis) {
JedisPool pool = (JedisPool)pools.get(Integer.valueOf(database));
pool.returnResource(jedis);
}
static {
if(!StringUtils.isEmpty(sPort) && StringUtils.isNumeric(sPort)) {
port = StringUtils.stringToInteger(sPort);
}
sTimeout = PropertiesUtils.getProperty("redis.timeout");
timeout = 2000;
if(!StringUtils.isEmpty(sTimeout) && StringUtils.isNumeric(sTimeout)) {
timeout = StringUtils.stringToInteger(sTimeout);
}
password = PropertiesUtils.getProperty("redis.password");
if(StringUtils.isEmpty(password)) {
password = null;
}
jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(100);
jedisPoolConfig.setMaxIdle(100);
jedisPoolConfig.setTestOnBorrow(true);
jedisPoolConfig.setMinIdle(8);//设置最小空闲数
jedisPoolConfig.setMaxWaitMillis(10000);
jedisPoolConfig.setTestOnReturn(true);
//在空闲时检查连接池有效性
jedisPoolConfig.setTestWhileIdle(true);
//两次逐出检查的时间间隔(毫秒)
jedisPoolConfig.setTimeBetweenEvictionRunsMillis(30000);
//每次逐出检查时 逐出的最大数目
jedisPoolConfig.setNumTestsPerEvictionRun(10);
//连接池中连接可空闲的最小时间,会把时间超过minEvictableIdleTimeMillis毫秒的连接断开
jedisPoolConfig.setMinEvictableIdleTimeMillis(60000);
}
public static void hsetexToJedis(String key,String field,String value,int dataBase){
Jedis jedis = getJedis(dataBase);
jedis.hset(HTT+key,field,value);
jedis.expire(HTT + key,THREE_DAY_CACHE);
returnJedis(dataBase,jedis);
}
public static void hsetexToJedis(String key,String field,String value,int expire,int dataBase){
Jedis jedis = getJedis(dataBase);
jedis.hset(HTT+key,field,value);
jedis.expire(HTT + key,expire);
returnJedis(dataBase,jedis);
}
public static void hsetToJedis(String key,String field,String value,int dataBase){
Jedis jedis = getJedis(dataBase);
jedis.hset(HTT+key,field,value);
returnJedis(dataBase,jedis);
}
public static String getFromJedis(String key,String field,int dataBase){
Jedis jedis = null;
try {
jedis = getJedis(dataBase);
String value = jedis.hget(HTT + key, field);
return value;
} finally {
returnJedis(dataBase,jedis);
}
}
public static Boolean isExists(String key,String field,int dataBase){
Jedis jedis = null;
try {
jedis = getJedis(dataBase);
Boolean result = jedis.hexists(HTT + key,field);
return result;
} finally {
returnJedis(dataBase,jedis);
}
}
public static void delKeys(int dataBase,String... keys){
Jedis jedis = null;
try {
jedis = getJedis(dataBase);
for (String key : keys){
jedis.del(HTT+key);
}
} finally {
returnJedis(dataBase,jedis);
}
}
/**
* 模糊匹配移除key
* @param dataBase 库索引
* @param keys
* @param source 来源 例:pledge-service
*/
public static void delPatternKeys(int dataBase,Class[] keys,CacheSource source){
Jedis jedis = null;
try {
jedis = getJedis(dataBase);
for (Class key : keys){
Set<String> keySet = getKeysByPattern(jedis,key.getSimpleName(),source);
if (keySet == null || keySet.size() == 0)
continue;
jedis.del(keySet.toArray(new String[keySet.size()]));
}
} finally {
returnJedis(dataBase,jedis);
}
}
/**
* 模糊匹配key
* @param dataBase
* @param pattern
* @return
*/
private static Set<String> getKeysByPattern(Jedis jedis,String pattern,CacheSource source){
return jedis.keys("*"+source.getDes()+"*"+pattern+"*");
}
public static void returnJedis(int dataBase,Jedis jedis){
if (jedis != null){
returnResource(dataBase,jedis);
}
}
}
四.utils用于json的序列化和反序列化这里用的是阿里的fastjson
package com.htt.app.cache.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.htt.framework.util.PagingResult;
import java.util.*;
/**
* fastjson序列化反序列化
* Created by sunnyLu on 2017/2/14.
*/
public class FastJsonUtils {
public static String parseJson(Object o){
return JSON.toJSONString(o);
}
/**
* 对单个javabean的解析
* @param jsonString
* @param cls
* @return
*/
public static <T> T JsonToEntity(String jsonString, Class<T> cls) {
T t = null;
try {
t = JSON.parseObject(jsonString, cls);
} catch (Exception e) {
e.printStackTrace();
}
return t;
}
public static <T> List<T> JsonToList(String jsonString, Class<T> cls) {
List<T> list = new ArrayList<T>();
try {
list = JSON.parseArray(jsonString, cls);
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
public static <T> PagingResult<T> JsonToPagingResult(String jsonString, Class<T> cls) {
PagingResult<T> pagingResult = new PagingResult<T>();
try {
pagingResult = JSON.parseObject(jsonString, new
TypeReference<PagingResult<T>>() {
});
//解决类型擦除,须要拼装
List<T> list = JSON.parseArray(JSON.parseObject(jsonString).getString("rows"), cls);
pagingResult.setRows(list);
} catch (Exception e) {
e.printStackTrace();
}
return pagingResult;
}
public static Map<String, Object> JsonToMap(String jsonString) {
Map<String, Object> map = new HashMap<String, Object>();
try {
map = JSON.parseObject(jsonString, new TypeReference<Map<String, Object>>(){});
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
public static Map<String, Object> JsonToLinkedHashMap(String jsonString) {
Map<String, Object> map = new LinkedHashMap<String, Object>();
try {
map = JSON.parseObject(jsonString, new TypeReference<LinkedHashMap<String, Object>>(){});
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
public static List<Map<String, Object>> JsonToListMap(String jsonString) {
List<Map<String, Object>> list = new ArrayList<Map<String,Object>>();
try {
list = JSON.parseObject(jsonString, new TypeReference<List<Map<String, Object>>>(){});
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}
在服务层仅须要启用aop代理,并且注入切面类的bean
<aop:aspectj-autoproxy />
<bean id="aopCache" class="com.htt.app.cache.aspect.CacheAspect"></bean>
接下来就是happy的时候
在须要进行缓存的接口上加上注解
@Override
@AopCacheable(type = HJLoanEntity.class,keys = {HJLoanEntity.class},expires = 60,source = CacheSource.LOAN_SERVICE)
public HJLoanEntity getHJLoanByLoanNum(String loanNumber) {
return hjLoanRepository.getHJLoanByLoanNum(loanNumber);
}
在须要对缓存进行更新的接口上加上注解
@Override
@AopCacheRelease(keys = {HJLoanEntity.class},source = CacheSource.LOAN_SERVICE)
public int editHJLoan(HJLoanEntity entity,List<Long> users) {
return HandlerFactory.doHandle(entity, HandlerEnum.EDIT, null);
}
功能是实现了但是须要注意的是缓存虽然效果好却不能再平台上滥用,应用的尺度拿捏须要慎重的考虑。例如,对于一些时效性要求不高的数据就可以考虑使用缓存且这个时候不须要考虑何时释放缓存,通常情况下我都会对这类缓存加个过期时间,到期缓存会自动失效。而对时效性要求极高的数据并非不可以使用缓存,只是须要从全局上考虑,所有可能对缓存数据会有更新的接口上,其调用完毕都须要对缓存做失效处理(其实最好的策略是直接更新缓存,不过如果须要更新的缓存接口过多的话一个是不好维护,一个是影响写入效率)。
另外一个须要注意的地方是我们的缓存是方法级别的,而同一个接口在不同的应用场景下可能时效性要求不一样,例如某个getXXX接口在场景1可能只是展现到前台,在场景二可能须要通过该接口得到的数据做一系列的写入操作,这个时候对时效性就要求很高。所以我的建议是不要对原始接口做缓存,可以为这个接口做个代理接口,把缓存加在代理接口上,这样应用时就比较灵活,你完全可以在应用了缓存的代理接口和原始接口上自由切换,就是这么任性。
@Override
@AopCacheable(type = HJLoanEntity.class,keys = {HJLoanEntity.class},expires = 60,source = CacheSource.LOAN_SERVICE)
public HJLoanEntity getHJLoanByLoanNum(String loanNumber) {
return hjLoanService.getHJLoanByLoanNum(loanNumber);
}