Guava Cache在实际项目中的应用
对于Guava Cache本身就不多做介绍了,一个非常好用的本地cache lib,可以完全取代自己手动维护ConcurrentHashMap。
背景
目前需要开发一个接口I,对性能要求有非常高的要求,TP99.9在20ms以内。初步开发后发现耗时完全无法满足,mysql稍微波动就超时了。

主要耗时在DB读取,请求一次接口会读取几次配置表Entry表。而Entry表的信息更新又不频繁,对实时性要求不高,所以想到了对DB做一个cache,理论上就可以大幅度提升接口性能了。
DB表结构(这里的代码都是为了演示,不过原理、流程和实际生产环境基本是一致的)
CREATE TABLE `entry` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` int(11) NOT NULL,
`value` varchar(50) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `unique_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
接口中的查询是根据name进行select操作,这次的目的就是设计一个cache类,将DB查询cache化。
基础使用
首先,自然而然的想到了最基本的guava cache的使用,如下:
@Slf4j
@Component
public class EntryCache { @Autowired
EntryMapper entryMapper; /**
* guava cache 缓存实体
*/
LoadingCache<String, Entry> cache = CacheBuilder.newBuilder()
// 缓存刷新时间
.refreshAfterWrite(10, TimeUnit.MINUTES)
// 设置缓存个数
.maximumSize(500)
.build(new CacheLoader<String, Entry>() {
@Override
// 当本地缓存命没有中时,调用load方法获取结果并将结果缓存
public Entry load(String appKey) {
return getEntryFromDB(appKey);
} // 数据库进行查询
private Entry getEntryFromDB(String name) {
log.info("load entry info from db!entry:{}", name);
return entryMapper.selectByName(name);
}
}); /**
* 对外暴露的方法
* 从缓存中取entry,没取到就走数据库
*/
public Entry getEntry(String name) throws ExecutionException {
return cache.get(name);
} }
这里用了refreshAfterWrite,和expireAfterWrite区别是expireAfterWrite到期会直接删除缓存,如果同时多个并发请求过来,这些请求都会重新去读取DB来刷新缓存。DB速度较慢,会造成线程短暂的阻塞(相对于读cache)。
而refreshAfterWrite,则不会删除cache,而是只有一个请求线程会去真实的读取DB,其他请求直接返回老值。这样可以避免同时过期时大量请求被阻塞,提升性能。
但是还有一个问题,那就是更新线程还是会被阻塞,这样在缓存key集体过期时,可能还会使响应时间变得不满足要求。
后台线程刷新
就像上面所说,只要刷新缓存,就必然有线程被阻塞,这个是无法避免的。
虽然无法避免线程阻塞,但是我们可以避免阻塞用户线程,让用户无感知即可。
所以,我们可以把刷新线程放到后台执行。当key过期时,有新用户线程读取cache时,开启一个新线程去load DB的数据,用户线程直接返回老的值,这样就解决了这个问题。
代码修改如下:
@Slf4j
@Component
public class EntryCache { @Autowired
EntryMapper entryMapper; ListeningExecutorService backgroundRefreshPools =
MoreExecutors.listeningDecorator(new ThreadPoolExecutor(10, 10,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>())); /**
* guava cache 缓存实体
*/
LoadingCache<String, Entry> cache = CacheBuilder.newBuilder()
// 缓存刷新时间
.refreshAfterWrite(10, TimeUnit.MINUTES)
// 设置缓存个数
.maximumSize(500)
.build(new CacheLoader<String, Entry>() {
@Override
// 当本地缓存命没有中时,调用load方法获取结果并将结果缓存
public Entry load(String appKey) {
return getEntryFromDB(appKey);
} @Override
// 刷新时,开启一个新线程异步刷新,老请求直接返回旧值,防止耗时过长
public ListenableFuture<Entry> reload(String key, Entry oldValue) throws Exception {
return backgroundRefreshPools.submit(() -> getEntryFromDB(key));
} // 数据库进行查询
private Entry getEntryFromDB(String name) {
log.info("load entry info from db!entry:{}", name);
return entryMapper.selectByName(name);
}
}); /**
* 对外暴露的方法
* 从缓存中取entry,没取到就走数据库
*/
public Entry getEntry(String name) throws ExecutionException {
return cache.get(name);
} /**
* 销毁时关闭线程池
*/
@PreDestroy
public void destroy(){
try {
backgroundRefreshPools.shutdown();
} catch (Exception e){
log.error("thread pool showdown error!e:{}",e.getMessage());
} }
}
改动就是新添加了一个backgroundRefreshPools线程池,重写了一个reload方法。
ListeningExecutorService是guava的concurrent包里的类,负责一些线程池相关的工作,感兴趣的可以自己去了解一下。
在reload方法里提交一个新的线程,就可以用这个线程来刷新cache了。
如果刷新cache没有完成的时候有其他线程来请求该key,则会直接返回老值。
同时,千万不要忘记销毁线程池。
初始化问题
上面两步达到了不阻塞刷新cache的功能,但是这个前提是这些cache已经存在。
项目刚刚启动的时候,所有的cache都是不存在的,这个时候如果大批量请求过来,同样会被阻塞,因为没有老的值供返回,都得等待cache的第一次load完毕。
解决这个问题的方法就是在项目启动的过程中,将所有的cache预先load过来,这样用户请求刚到服务器时就会直接读cache,不用等待。
@Slf4j
@Component
public class EntryCache { @Autowired
EntryMapper entryMapper; ListeningExecutorService backgroundRefreshPools =
MoreExecutors.listeningDecorator(new ThreadPoolExecutor(10, 10,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>())); /**
* guava cache 缓存实体
*/
LoadingCache<String, Entry> cache = CacheBuilder.newBuilder()
// 缓存刷新时间
.refreshAfterWrite(10, TimeUnit.MINUTES)
// 设置缓存个数
.maximumSize(500)
.build(new CacheLoader<String, Entry>() {
@Override
// 当本地缓存命没有中时,调用load方法获取结果并将结果缓存
public Entry load(String appKey) {
return getEntryFromDB(appKey);
} @Override
// 刷新时,开启一个新线程异步刷新,老请求直接返回旧值,防止耗时过长
public ListenableFuture<Entry> reload(String key, Entry oldValue) throws Exception {
return backgroundRefreshPools.submit(() -> getEntryFromDB(key));
} // 数据库进行查询
private Entry getEntryFromDB(String name) {
log.info("load entry info from db!entry:{}", name);
return entryMapper.selectByName(name);
}
}); /**
* 对外暴露的方法
* 从缓存中取entry,没取到就走数据库
*/
public Entry getEntry(String name) throws ExecutionException {
return cache.get(name);
} /**
* 销毁时关闭线程池
*/
@PreDestroy
public void destroy(){
try {
backgroundRefreshPools.shutdown();
} catch (Exception e){
log.error("thread pool showdown error!e:{}",e.getMessage());
} } @PostConstruct
public void initCache() {
log.info("init entry cache start!");
//读取所有记录
List<Entry> list = entryMapper.selectAll(); if (CollectionUtils.isEmpty(list)) {
return;
}
for (Entry entry : list) {
try {
this.getEntry(entry.getName());
} catch (Exception e) {
log.error("init cache error!,e:{}", e.getMessage());
}
}
log.info("init entry cache end!");
}
}
结果
让我们用数据看看这个cache类的表现:

200QPS,TP99.9是9ms,完美达标。
可以看出来,合理的使用缓存对接口性能还是有很大提升的。
Guava Cache在实际项目中的应用的更多相关文章
- Guava Cache探索及spring项目整合GuavaCache实例
背景 对于高频访问但是低频更新的数据我们一般会做缓存,尤其是在并发量比较高的业务里,原始的手段我们可以使用HashMap或者ConcurrentHashMap来存储. 这样没什么毛病,但是会面临一个问 ...
- Guava Cache用法介绍
背景 缓存的主要作用是暂时在内存中保存业务系统的数据处理结果,并且等待下次访问使用.在日长开发有很多场合,有一些数据量不是很大,不会经常改动,并且访问非常频繁.但是由于受限于硬盘IO的性能或者远程网络 ...
- 【Guava】Guava Cache用法
背景 缓存的主要作用是暂时在内存中保存业务系统的数据处理结果,并且等待下次访问使用.在日长开发有很多场合,有一些数据量不是很大,不会经常改动,并且访问非常频繁.但是由于受限于硬盘IO的性能或者远程网络 ...
- (翻译)Google Guava Cache
翻译自Google Guava Cache This Post is a continuation of my series on Google Guava, this time covering G ...
- guava cache使用和源码分析
guava cache的优点和使用场景,用来判断业务中是否适合使用此缓存 介绍常用的方法,并给出示例,作为使用的参考 深入解读源码. guava简介 guava cache是一个本地缓存.有以下优点: ...
- Guava Cache用法介绍<转>
Guava Cache是在内存中缓存数据,相比较于数据库或redis存储,访问内存中的数据会更加高效.Guava官网介绍,下面的这几种情况可以考虑使用Guava Cache: 愿意消耗一些内存空间来提 ...
- Guava Cache源码浅析
1. 简介 Guava Cache是指在JVM的内存中缓存数据,相比较于传统的数据库或redis存储,访问内存中的数据会更加高效,无网络开销. 根据Guava官网介绍,下面的这几种情况可以考虑使用Gu ...
- 第七章 企业项目开发--本地缓存guava cache
1.在实际项目开发中,会使用到很多缓存技术,而且数据库的设计一般也会依赖于有缓存的情况下设计. 常用的缓存分两种:本地缓存和分布式缓存. 常用的本地缓存是guava cache,本章主要介绍guava ...
- 企业项目开发--本地缓存guava cache(1)
此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 1.在实际项目开发中,会使用到很多缓存技术,而且数据库的设计一般也会依赖于有缓存的情况下设计. 常用的缓存分 ...
随机推荐
- 大数据学习——Hbase
1. Hbase基础 1.1 hbase数据库介绍 1.简介 hbase是bigtable的开源java版本.是建立在hdfs之上,提供高可靠性.高性能.列存储.可伸缩.实时读写nosql的数据库系统 ...
- Scrapy应用之抓取《宦海沉浮》小说
目标站点 http://www.shushu8.com/huanhaichenfu/ 第一步:新建项目 KeysdeMacBook:Desktop keys$ scrapy startprojec ...
- 10大vim插件
Taglist taglist是一个用于显示定位程序中各种符号的插件,例如宏定义.变量名.结构名.函数名这些东西 我们将其称之为符号(symbols),而在taglist中将其称之为tag.显然,要想 ...
- ecmascript6入门
ECMAScript 6 入门 阮一峰
- 九度oj 题目1140:八皇后
题目描述: 会下国际象棋的人都很清楚:皇后可以在横.竖.斜线上不限步数地吃掉其他棋子.如何将8个皇后放在棋盘上(有8 * 8个方格),使它们谁也不能被吃掉!这就是著名的八皇后问题. 对于某个满足要求的 ...
- VMware 如何通过现有虚拟机克隆新的虚拟机 (图文)
本文做的是克隆主机,并重命名主机名,做好主机名与IP的对应关系,并可以通过主机名访问对方的主机. 首先说一下克隆虚拟机的作用 克隆虚拟机的作用 因工作需要,需要用到多个虚拟机环境时,再新建几个比较麻烦 ...
- *AtCoder Regular Contest 094 F - Normalization
$n \leq 200000$的abc字符串,现能进行如下变换零次或若干次:选一个$i<n$且$s_i \neq s_{i+1}$,把$s_i$和$s_{i+1}$替换成abc三个字母中除了这两 ...
- 无记录时显示gridview表头,并增加一行显示“没有记录”【绑定SqlDataSource控件时】
原文发布时间为:2008-08-04 -- 来源于本人的百度文章 [由搬家工具导入] using System;using System.Data;using System.Configuration ...
- 标准C程序设计七---02
Linux应用 编程深入 语言编程 标准C程序设计七---经典C11程序设计 以下内容为阅读: <标准C程序设计>(第7版) 作者 ...
- 小窥React360——用React创建360全景VR体验
前言 混迹VR届的发烧友兼开发者们一定不要错过这款FaceBook推出的跨端VR开发框架——React360,称为360全景体验框架更为准确,因为其前身是FaceBook和Oculus2017年 ...