查詢mysql數據庫時,一樣的輸入須要不止一次獲取值或者一個查詢須要作大量運算時,很容易會想到使用redis緩存。可是若是查詢併發量特別大的話,請求redis服務也會特別耗時,這種場景下,將redis遷移到本地減小查詢耗時是一種常見的解決方法java
mysql
、
redis
和
guava cache
。
mysql
做爲持久化,
redis
做爲分佈式緩存,
guava cache
做爲本地緩存。二級緩存其實就是在
redis
上面再架了一層
guava cahe
guava cache
和concurrent hashmap
相似,都是k-v型存儲,可是concurrent hashmap
只能顯示的移除元素,而guava cache
當內存不夠用時或者存儲超時時會自動移除,具備緩存的基本功能mysql
抽象類:SuperBaseGuavaCache.javagit
@Slf4j
public abstract class SuperBaseGuavaCache<K, V> {
/**
* 緩存對象
* */
private LoadingCache<K, V> cache;
/**
* 緩存最大容量,默認爲10
* */
protected Integer maximumSize = 10;
/**
* 緩存失效時長
* */
protected Long duration = 10L;
/**
* 緩存失效單位,默認爲5s
*/
protected TimeUnit timeUnit = TimeUnit.SECONDS;
/**
* 返回Loading cache(單例模式的)
*
* @return LoadingCache<K, V>
* */
private LoadingCache<K, V> getCache() {
if (cache == null) {
synchronized (SuperBaseGuavaCache.class) {
if (cache == null) {
CacheBuilder<Object, Object> tempCache = null;
if (duration > 0 && timeUnit != null) {
tempCache = CacheBuilder.newBuilder()
.expireAfterWrite(duration, timeUnit);
}
//設置最大緩存大小
if (maximumSize > 0) {
tempCache.maximumSize(maximumSize);
}
//加載緩存
cache = tempCache.build( new CacheLoader<K, V>() {
//緩存不存在或過時時調用
@Override
public V load(K key) throws Exception {
//不容許返回null值
V target = getLoadData(key) != null ? getLoadData(key) : getLoadDataIfNull(key);
return target;
}
});
}
}
}
return cache;
}
/**
* 返回加載到內存中的數據,通常從數據庫中加載
*
* @param key key值
* @return V
* */
abstract V getLoadData(K key);
/**
* 調用getLoadData返回null值時自定義加載到內存的值
*
* @param key
* @return V
* */
abstract V getLoadDataIfNull(K key);
/**
* 清除緩存(能夠批量清除,也能夠清除所有)
*
* @param keys 須要清除緩存的key值
* */
public void batchInvalidate(List<K> keys) {
if (keys != null ) {
getCache().invalidateAll(keys);
log.info("批量清除緩存, keys爲:{}", keys);
} else {
getCache().invalidateAll();
log.info("清除了全部緩存");
}
}
/**
* 清除某個key的緩存
* */
public void invalidateOne(K key) {
getCache().invalidate(key);
log.info("清除了guava cache中的緩存, key爲:{}", key);
}
/**
* 寫入緩存
*
* @param key 鍵
* @param value 鍵對應的值
* */
public void putIntoCache(K key, V value) {
getCache().put(key, value);
}
/**
* 獲取某個key對應的緩存
*
* @param key
* @return V
* */
public V getCacheValue(K key) {
V cacheValue = null;
try {
cacheValue = getCache().get(key);
} catch (ExecutionException e) {
log.error("獲取guava cache中的緩存值出錯, {}");
}
return cacheValue;
}
}
複製代碼
抽象類說明:github
LoadingCache
的單例對象expireAfterWrite()
方法指定guava cache
中鍵值對的過時時間,默認緩存時長爲10smaximumSize()
方法指定內存中最多能夠存儲的鍵值對數量,超過這個數量,guava cache
將採用LRU算法淘汰鍵值對load()
方法。當調用guava cache
的get()
方法時,若是guava cache
中存在將會直接返回值,不然調用load()
方法將值加載到guava cache
中。在該類中,load
方法中是兩個抽象方法,須要子類去實現,一個是getLoadData()
方法,這個方法通常是從數據庫中查找數據,另一個是getLoadDataIfNull()
方法,當getLoadData()
方法返回null值時調用,guava cache
經過返回值是否爲null判斷是否須要進行加載,load()
方法中返回null值將會拋出InvalidCacheLoadException
異常:invalidateOne()
方法主動失效某個key的緩存batchInvalidate()
方法批量清除緩存或清空全部緩存,由傳入的參數決定putIntoCache()
方法顯示的將鍵值對存入緩存getCacheValue()
方法返回緩存中的值抽象類的實現類:StudentGuavaCache.javaredis
@Component
@Slf4j
public class StudentGuavaCache extends SuperBaseGuavaCache<Long, Student> {
@Resource
private StudentDAO studentDao;
@Resource
private RedisService<Long, Student> redisService;
/**
* 返回加載到內存中的數據,從redis中查找
*
* @param key key值
* @return V
* */
@Override
Student getLoadData(Long key) {
Student student = redisService.get(key);
if (student != null) {
log.info("根據key:{} 從redis加載數據到guava cache", key);
}
return student;
}
/**
* 調用getLoadData返回null值時自定義加載到內存的值
*
* @param key
* @return
* */
@Override
Student getLoadDataIfNull(Long key) {
Student student = null;
if (key != null) {
Student studentTemp = studentDao.findStudent(key);
student = studentTemp != null ? studentTemp : new Student();
}
log.info("從mysql中加載數據到guava cache中, key:{}", key);
//此時在緩存一份到redis中
redisService.set(key, student);
return student;
}
}
複製代碼
實現父類的getLoadData()
和getLoadDataIfNull()
方法算法
getLoadData()
方法返回redis中的值getLoadDataIfNull()
方法若是redis緩存中不存在,則從mysql查找,若是在mysql中也查找不到,則返回一個空對象public Student findStudent(Long id) {
if (id == null) {
throw new ErrorException("傳參爲null");
}
return studentGuavaCache.getCacheValue(id);
}
複製代碼
流程圖: sql
代碼實現:數據庫
@Transactional(rollbackFor = Exception.class)
public int removeStudent(Long id) {
//1.清除guava cache緩存
studentGuavaCache.invalidateOne(id);
//2.清除redis緩存
redisService.delete(id);
//3.刪除mysql中的數據
return studentDao.removeStudent(id);
}
複製代碼
流程圖: 緩存
代碼實現:安全
@Transactional(rollbackFor = Exception.class)
public int updateStudent(Student student) {
//1.清除guava cache緩存
studentGuavaCache.invalidateOne(student.getId());
//2.清除redis緩存
redisService.delete(student.getId());
//3.更新mysql中的數據
return studentDao.updateStudent(student);
}
複製代碼
更新和刪除就最後一步對mysql的操做不同,兩層緩存都是刪除的
天太冷了,更新完畢要學羅文姬女士躺牀上玩手機了
最後: 附: 完整項目地址 上述代碼在master分支上
=================如下內容更新於2019.01.18==============
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DoubleCacheDelete {
/**
* 緩存的key
* */
String key();
}
複製代碼
申明瞭一個@DoubleCacheDelete註解@Aspect
@Component
public class DoubleCacheDeleteAspect {
/**
* 獲取方法參數
* */
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
@Resource
private StudentGuavaCache studentGuavaCache;
@Resource
private RedisService<Long, Student> redisService;
/**
* 在方法執行以前對註解進行處理
*
* @param pjd
* @param doubleCacheDelete 註解
* @return 返回中的值
* */
@Around("@annotation(com.cqupt.study.annotation.DoubleCacheDelete) && @annotation(doubleCacheDelete)")
@Transactional(rollbackFor = Exception.class)
public Object dealProcess(ProceedingJoinPoint pjd, DoubleCacheDelete doubleCacheDelete) {
Object result = null;
Method method = ((MethodSignature) pjd.getSignature()).getMethod();
//得到參數名
String[] params = discoverer.getParameterNames(method);
//得到參數值
Object[] object = pjd.getArgs();
SpelParser<String> spelParser = new SpelParser<>();
EvaluationContext context = spelParser.setAndGetContextValue(params, object);
//解析SpEL表達式
if (doubleCacheDelete.key() == null) {
throw new ErrorException("@DoubleCacheDelete註解中key值定義不爲null");
}
String key = spelParser.parse(doubleCacheDelete.key(), context);
if (key != null) {
//1.清除guava cache緩存
studentGuavaCache.invalidateOne(Long.valueOf(key));
//2.清除redis緩存
redisService.delete(Long.valueOf(key));
} else {
throw new ErrorException("@DoubleCacheDelete註解中key值定義不存在,請檢查是否和方法參數相同");
}
//執行目標方法
try {
result = pjd.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
}
複製代碼
將註解攔截到,並解析出SpEL表達式的值並刪除對應的緩存public class SpelParser<T> {
/**
* 表達式解析器
* */
ExpressionParser parser = new SpelExpressionParser();
/**
* 解析SpEL表達式
*
* @param spel
* @param context
* @return T 解析出來的值
* */
public T parse(String spel, EvaluationContext context) {
Class<T> keyClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
T key = parser.parseExpression(spel).getValue(keyClass);
return key;
}
/**
* 將參數名和參數值存儲進EvaluationContext對象中
*
* @param object 參數值
* @param params 參數名
* @return EvaluationContext對象
* */
public EvaluationContext setAndGetContextValue(String[] params, Object[] object) {
EvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < params.length; i++) {
context.setVariable(params[i], object[i]);
}
return context;
}
}
複製代碼
對SpEL解析抽象出專門的一個類public int removeStudent(Long id) {
return studentDao.removeStudent(id);
}
複製代碼
該方法和原先相比沒有了刪除緩存的代碼,刪除緩存的部分都交給註解去完成了