QPS這麼高,那就來寫個多級快取吧
查詢mysql資料庫時,同樣的輸入需要不止一次獲取值或者一個查詢需要做大量運算時,很容易會想到使用redis快取。但是如果查詢併發量特別大的話,請求redis服務也會特別耗時,這種場景下,將redis遷移到本地減少查詢耗時是一種常見的解決方法
多級快取基本架構
說明:儲存選擇了mysql
、
redis
和
guava cache
mysql
作為持久化,
redis
作為伺服器快取,
guava cache
作為本地快取。二級快取其實就是在
redis
上面在架了一層
guava cahe
guava cache簡單介紹
guava cache
和concurrent hashmap
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
中鍵值對的過期時間,預設快取時長為10s -
maximumSize()
方法指定記憶體中最多可以儲存的鍵值對數量,超過這個數量,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()
方法返回快取中的值 -
抽象類的實現類: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的操作不一樣,兩層快取都是刪除的
天太冷了,更新完畢要學羅文姬女士躺床上玩手機了
最後: 附:[完整專案地址](https://github.com/TiantianUpup/double-cache)