QPS такой высокий, давайте напишем многоуровневый кеш

Redis

При запросе к базе данных mysql легко подумать об использовании кэша Redis, когда один и тот же ввод должен получить значение более одного раза или запрос должен выполнять много операций. Однако, если параллелизм запросов особенно велик, запрос службы Redis также будет занимать очень много времени.В этом сценарии распространенное решение — перенести Redis на локальный сервер, чтобы сократить время запроса.

Базовая архитектура многоуровневого кэша

基本架构.pngОписание: Выбрано хранилищеmysql,redisиguava cache.mysqlкак настойчивость,redisКак распределенный кеш,guava cacheкак локальный кеш. Кэш второго уровня на самом делеredisеще один слой сверхуguava cahe
二级缓存.png

Краткое введение в кеш гуавы

guava cacheиconcurrent hashmapПохоже, все хранилища типа k-v, ноconcurrent hashmapтолько отображаемые удаленные элементы и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Время истечения пары ключ-значение посередине, время кэширования по умолчанию — 10 с.
    • maximumSize()Метод указывает максимальное количество пар ключ-значение, которое может храниться в памяти, помимо этого числа,guava cacheАлгоритм LRU будет использоваться для устранения пар ключ-значение.
    • Здесь значение кеша загружается CacheLoader, который нужно реализоватьload()метод. при звонкеguava cacheизget()метод, еслиguava cacheЕсли существует, он вернет значение напрямую, в противном случае вызовитеload()метод для загрузки значения вguava cacheсередина. В этом классеloadВ методе есть два абстрактных метода, которые должны быть реализованы подклассами, один из нихgetLoadData()метод, этот метод, как правило, для поиска данных из базы данных, другойgetLoadDataIfNull()метод, когдаgetLoadData()Вызывается, когда метод возвращает нулевое значение,guava cacheОпределите, требуется ли загрузка, по тому, является ли возвращаемое значение нулевым или нет.load()Метод, возвращающий нулевое значение, вызоветInvalidCacheLoadExceptionаномальный:
    • invalidateOne()Метод активно делает недействительным кеш ключа
    • 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. Результаты запроса будут загружены в локальный кеш и возвращены

  • Код:
    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 на последнем шаге, удаляются оба слоя кеша.



Слишком холодно После обновления я должен учиться у г-жи Луо Вэньцзи и играть с ее мобильным телефоном в постели.

Наконец: Прикрепил:полный адрес проектаПриведенный выше код находится в основной ветке

================Следующий контент был обновлен 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
  • Оригинальный метод удаления учеников:
    public int removeStudent(Long id) {
            return studentDao.removeStudent(id);
        }
    
    По сравнению с исходным методом здесь нет кода для удаления кеша, а часть удаления кеша оставлена ​​для завершения аннотации.



Наконец: Прикреплено: [Полный адрес проекта] (https://github.com/TiantianUpup/double-cache) Вышеприведенный код находится в ветке cache_annotation_20190114.

================ Следующий контент был обновлен 2021.02.05 ================
Статья была опубликована в 2019 году. В прошлом этот проект находился в состоянии прекращения обновления, но на github время от времени появлялись звезды, форки и проблемы, которые побуждали его продолжать улучшать этот проект.

На данный момент для задачи [обновление ветки yucache_annotation_20190114] доработаны следующие проблемы:

  • Лавина кэша и проникновение в кэш
  • Добавьте двойное удаление кеша, чтобы сохранить согласованность данных
  • Оптимизируйте логику запросов, чтобы избежать недопустимых загрузок.По проблемам смотрите первый этаж комментариев или описание проблемы на github.