1. 程式人生 > >QPS這麼高,那就來寫個多級快取吧

QPS這麼高,那就來寫個多級快取吧

查詢mysql資料庫時,同樣的輸入需要不止一次獲取值或者一個查詢需要做大量運算時,很容易會想到使用redis快取。但是如果查詢併發量特別大的話,請求redis服務也會特別耗時,這種場景下,將redis遷移到本地減少查詢耗時是一種常見的解決方法

多級快取基本架構

基本架構.png
說明:儲存選擇了 mysqlredisguava cache mysql作為持久化, redis作為伺服器快取, guava cache作為本地快取。二級快取其實就是在 redis上面在架了一層 guava cahe
二級快取.png

guava cache簡單介紹

guava cacheconcurrent 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中鍵值對的過期時間,預設快取時長為10s

  • maximumSize()方法指定記憶體中最多可以儲存的鍵值對數量,超過這個數量,guava cache將採用LRU演算法淘汰鍵值對

  • 這裡採用CacheLoader的方式載入快取值,需要實現load()方法。當呼叫guava cacheget()方法時,如果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中也查詢不到,則返回一個空物件

查詢

  • 流程圖:
    查詢.png
    • 1.查詢本地快取是否命中
    • 2.本地快取不命中查詢redis快取
    • 3.redis快取不命中查詢mysql
    • 4.查詢到的結果都會被load到本地快取中在返回
  • 程式碼實現:
public Student findStudent(Long id) {
        if (id == null) {
            throw new ErrorException("傳參為null");
        }

        return studentGuavaCache.getCacheValue(id);
    }
複製程式碼

刪除

  • 流程圖:

    刪除.png

  • 程式碼實現:

@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);
    }
複製程式碼

更新

  • 流程圖:

    更新.png

  • 程式碼實現:

 @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)