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分支上
相关文章
相关标签/搜索