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

查詢mysql數據庫時,一樣的輸入須要不止一次獲取值或者一個查詢須要作大量運算時,很容易會想到使用redis緩存。可是若是查詢併發量特別大的話,請求redis服務也會特別耗時,這種場景下,將redis遷移到本地減小查詢耗時是一種常見的解決方法java

多級緩存基本架構

基本架構.png
說明:存儲選擇了 mysqlredisguava cachemysql做爲持久化, redis做爲分佈式緩存, guava cache做爲本地緩存。二級緩存其實就是在 redis上面再架了一層 guava cahe
二級緩存.png

guava cache簡單介紹

guava cacheconcurrent hashmap相似,都是k-v型存儲,可是concurrent hashmap只能顯示的移除元素,而guava cache當內存不夠用時或者存儲超時時會自動移除,具備緩存的基本功能mysql

封裝guava cache

  • 抽象類: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

    • 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.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中也查找不到,則返回一個空對象

查詢

  • 流程圖:
    查詢.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);
        }
    複製代碼

刪除

  • 流程圖: sql

    刪除.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的操做不同,兩層緩存都是刪除的


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



最後: 附: 完整項目地址 上述代碼在master分支上

=================如下內容更新於2019.01.18==============

基於註解的方式使用多級緩存

  • 爲何須要提供基於註解的方式使用多級緩存
    1:在不使用註解方式使用多級緩存,業務代碼和緩存代碼耦合,使用註解能夠進行解耦,業務代碼和緩存代碼分開
    2:開發方便
  • 註解的定義
    @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表達式的值並刪除對應的緩存
  • 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解析抽象出專門的一個類
  • 原來的刪除student的方法:
    public int removeStudent(Long id) {
            return studentDao.removeStudent(id);
        }
    複製代碼
    該方法和原先相比沒有了刪除緩存的代碼,刪除緩存的部分都交給註解去完成了


    最後: 附: 完整項目地址 上述代碼在cache_annotation_20190114分支上
相關文章
相關標籤/搜索