博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
QPS这么高,那就来写个多级缓存吧
阅读量:6623 次
发布时间:2019-06-25

本文共 8934 字,大约阅读时间需要 29 分钟。

查询mysql数据库时,同样的输入需要不止一次获取值或者一个查询需要做大量运算时,很容易会想到使用redis缓存。但是如果查询并发量特别大的话,请求redis服务也会特别耗时,这种场景下,将redis迁移到本地减少查询耗时是一种常见的解决方法

多级缓存基本架构

说明:存储选择了
mysql
redis
guava cache
mysql作为持久化,
redis作为分布式缓存,
guava cache作为本地缓存。二级缓存其实就是在
redis上面再架了一层
guava cahe

guava cache简单介绍

guava cacheconcurrent hashmap类似,都是k-v型存储,但是concurrent hashmap只能显示的移除元素,而guava cache当内存不够用时或者存储超时时会自动移除,具有缓存的基本功能

封装guava cache

  • 抽象类:SuperBaseGuavaCache.java

    @Slf4jpublic abstract class SuperBaseGuavaCache
    { /** * 缓存对象 * */ private LoadingCache
    cache; /** * 缓存最大容量,默认为10 * */ protected Integer maximumSize = 10; /** * 缓存失效时长 * */ protected Long duration = 10L; /** * 缓存失效单位,默认为5s */ protected TimeUnit timeUnit = TimeUnit.SECONDS; /** * 返回Loading cache(单例模式的) * * @return LoadingCache
    * */ private LoadingCache
    getCache() { if (cache == null) { synchronized (SuperBaseGuavaCache.class) { if (cache == null) { CacheBuilder
    tempCache = null; if (duration > 0 && timeUnit != null) { tempCache = CacheBuilder.newBuilder() .expireAfterWrite(duration, timeUnit); } //设置最大缓存大小 if (maximumSize > 0) { tempCache.maximumSize(maximumSize); } //加载缓存 cache = tempCache.build( new CacheLoader
    () { //缓存不存在或过期时调用 @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
    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@Slf4jpublic class StudentGuavaCache extends SuperBaseGuavaCache
    { @Resource private StudentDAO studentDao; @Resource private RedisService
    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中也查找不到,则返回一个空对象

查询

  • 流程图:
    • 1.查询本地缓存是否命中
    • 2.本地缓存不命中查询redis缓存
    • 3.redis缓存不命中查询mysql
    • 4.查询到的结果都会被load到本地缓存中在返回
  • 代码实现:
    public Student findStudent(Long id) {        if (id == null) {            throw new ErrorException("传参为null");        }        return studentGuavaCache.getCacheValue(id);    }复制代码

删除

  • 流程图:

  • 代码实现:

    @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);    }复制代码

更新

  • 流程图:

  • 代码实现:

    @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@Componentpublic class DoubleCacheDeleteAspect {    /**     * 获取方法参数     * */    LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();    @Resource    private StudentGuavaCache studentGuavaCache;    @Resource    private RedisService
    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
    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
    { /** * 表达式解析器 * */ ExpressionParser parser = new SpelExpressionParser(); /** * 解析SpEL表达式 * * @param spel * @param context * @return T 解析出来的值 * */ public T parse(String spel, EvaluationContext context) { Class
    keyClass = (Class
    ) ((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分支上

转载地址:http://uxjpo.baihongyu.com/

你可能感兴趣的文章
.met mvc 一种判断是否已登录的方式
查看>>
pthread线程使用小结
查看>>
MySQL备份账号权限
查看>>
15个重要的Android代码
查看>>
(转)android 牛人必修 ant 编译android工程
查看>>
求最大公约数与最小公倍数
查看>>
C# Winform 跨线程更新UI控件常用方法总结(转)
查看>>
eclipse菜单栏不显示 + the system is running in lou-graphics mode问题
查看>>
【WebService】使用jaxb完成对象和xml的转换
查看>>
如何去除My97 DatePicker控件上右键弹出官网的链接 - 如何debug混淆过的代码
查看>>
nginx学习之进程控制篇(三)
查看>>
02-线性结构3 Reversing Linked List
查看>>
透明色设置
查看>>
selenium + python自动化测试unittest框架学习(三)webdriver元素定位(一)
查看>>
webpack学习总结(一)
查看>>
Spring MVC——参数装填方式
查看>>
php性能优化--opcache
查看>>
文档预览的设计思考
查看>>
bzoj1196[HNOI2006]公路修建问题
查看>>
用Mybatis返回Map,List<Map>
查看>>