QPS這麼高,那就來寫個多級緩存吧

 

QPS這麼高,那就來寫個多級緩存吧 - 掘金
https://juejin.im/post/5c224cd3f265da610e801e8a

 

 

 

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的操作不一樣,兩層緩存都是刪除的 


天太冷了,更新完畢要學羅文姬女士躺牀上玩手機了

 

 

 

最後: 附: 完整項目地址

關注下面的標籤,發現更多相似文章

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章