内容简介:查询mysql数据库时,同样的输入需要不止一次获取值或者一个查询需要做大量运算时,很容易会想到使用redis缓存。但是如果查询并发量特别大的话,请求redis服务也会特别耗时,这种场景下,将redis迁移到本地减少查询耗时是一种常见的解决方法说明:存储选择了
查询 mysql 数据库时,同样的输入需要不止一次获取值或者一个查询需要做大量运算时,很容易会想到使用 redis 缓存。但是如果查询并发量特别大的话,请求redis服务也会特别耗时,这种场景下,将redis迁移到本地减少查询耗时是一种常见的解决方法
多级缓存基本架构
说明:存储选择了 mysql
、 redis
和 guava cache
mysql
作为持久化, redis
作为服务器缓存, guava cache
作为本地缓存。二级缓存其实就是在 redis
上面在架了一层 guava cahe
guava cache简单介绍
guava cache
和 concurrent 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 cache
的get()
方法时,如果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() getLoadDataIfNull()
查询
- 流程图:
- 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的操作不一样,两层缓存都是删除的
天太冷了,更新完毕要学罗文姬女士躺床上玩手机了
最后: 附:[完整项目地址](https://github.com/TiantianUpup/double-cache)
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 轻松学会HTTP缓存(强缓存,协商缓存)
- 常见面试题之缓存雪崩、缓存穿透、缓存击穿
- HTTP缓存 - 强缓存/协商缓存/浏览器刷新
- mybatis教程--查询缓存(一级缓存二级缓存和整合ehcache)
- 如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
- 缓存穿透和缓存击穿处理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
构建之法(第二版)
邹欣 / 人民邮电出版社 / 2015-7 / 59
软件工程牵涉的范围很广, 同时也是一般院校的同学反映比较空洞乏味的课程。 但是软件工程的技术对于投身IT 产业的学生来说是非常重要的。作者邹欣有长达20年的一线软件开发经验,他利用业余时间在数所高校进行了长达6年的软件工程教学实践,总结出了在16周的时间内让同学们通过 “做中学 (Learning By Doing)” 掌握实用的软件工程技术的教学计划,并得到高校师生的积极反馈。在此基础上,作者对......一起来看看 《构建之法(第二版)》 这本书的介绍吧!