查詢mysql資料庫時,同樣的輸入需要不止一次獲取值或者一個查詢需要做大量運算時,很容易會想到使用redis快取。但是如果查詢併發量特別大的話,請求redis服務也會特別耗時,這種場景下,將redis遷移到本地減少查詢耗時是一種常見的解決方法
多級快取基本架構
說明:儲存選擇了mysql
、redis
和guava cache
。
mysql
作為持久化,redis
作為分散式快取, guava cache
作為本地快取。二級快取其實就是在redis
上面再架了一層guava cahe
guava cache簡單介紹
guava cache
和concurrent hashmap
類似,都是k-v型儲存,但是concurrent hashmap
只能顯示的移除元素,而guava cache
當記憶體不夠用時或者儲存超時時會自動移除,具有快取的基本功能
封裝guava cache
-
抽象類:SuperBaseGuavaCache.java
@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; } } 複製程式碼
-
抽象類說明:
- 1.雙重鎖檢查併發安全的獲取
LoadingCache
的單例物件 expireAfterWrite()
方法指定guava cache
中鍵值對的過期時間,預設快取時長為10smaximumSize()
方法指定記憶體中最多可以儲存的鍵值對數量,超過這個數量,guava cache
將採用LRU演算法淘汰鍵值對- 這裡採用CacheLoader的方式載入快取值,需要實現
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()
方法返回快取中的值
- 1.雙重鎖檢查併發安全的獲取
-
抽象類的實現類:StudentGuavaCache.java
@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中也查詢不到,則返回一個空物件
查詢
- 流程圖:
- 1.查詢本地快取是否命中
- 2.本地快取不命中查詢redis快取
- 3.redis快取不命中查詢mysql
- 4.查詢到的結果都會被load到本地快取中在返回
- 程式碼實現:
public Student findStudent(Long id) { if (id == null) { throw new ErrorException("傳參為null"); } return studentGuavaCache.getCacheValue(id); } 複製程式碼
刪除
-
流程圖:
-
程式碼實現:
@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==============
基於註解的方式使用多級快取
- 為什麼需要提供基於註解的方式使用多級快取
1:在不使用註解方式使用多級快取,業務程式碼和快取程式碼耦合,使用註解可以進行解耦,業務程式碼和快取程式碼分開
2:開發方便 - 註解的定義
申明瞭一個@DoubleCacheDelete註解@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface DoubleCacheDelete { /** * 快取的key * */ String key(); } 複製程式碼
- 註解的攔截
將註解攔截到,並解析出SpEL表示式的值並刪除對應的快取@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表示式解析
對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; } } 複製程式碼
- 原來的刪除student的方法:
該方法和原先相比沒有了刪除快取的程式碼,刪除快取的部分都交給註解去完成了public int removeStudent(Long id) { return studentDao.removeStudent(id); } 複製程式碼
最後: 附: 完整專案地址 上述程式碼在cache_annotation_20190114分支上