我們項目開發過程中,在實現功能的情況之下對其進行優化是必不可少的,其中一種優化方案就是做數據緩存,對數據做緩存可以減少對數據庫的訪問壓力,在訪問量逐步增大的情況下可以分流一部分數據庫的壓力,對客戶端而言,最直觀的變化就是請求響應時間變短。
我在設想之初就想通過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);
}