guava使用
对于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使用的更多相关文章
- Spring cache简单使用guava cache
Spring cache简单使用 前言 spring有一套和各种缓存的集成方式.类似于sl4j,你可以选择log框架实现,也一样可以实现缓存实现,比如ehcache,guava cache. [TOC ...
- Guava库介绍之实用工具类
作者:Jack47 转载请保留作者和原文出处 欢迎关注我的微信公众账号程序员杰克,两边的文章会同步,也可以添加我的RSS订阅源. 本文是我写的Google开源的Java编程库Guava系列之一,主要介 ...
- Google Java编程库Guava介绍
本系列想介绍下Java下开源的优秀编程库--Guava[ˈgwɑːvə].它包含了Google在Java项目中使用一些核心库,包含集合(Collections),缓存(Caching),并发编程库(C ...
- [Java 缓存] Java Cache之 Guava Cache的简单应用.
前言 今天第一次使用MarkDown的形式发博客. 准备记录一下自己对Guava Cache的认识及项目中的实际使用经验. 一: 什么是Guava Guava工程包含了若干被Google的 Java项 ...
- [转载]Google Guava官方教程(中文版)
原文链接 译文链接 译者: 沈义扬,罗立树,何一昕,武祖 校对:方腾飞 引言 Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如:集合 [collections] ...
- java开发人员,最应该学习和熟练使用的工具类。google guava.(谷歌 瓜娃)
学习参考文章: http://blog.csdn.net/wisgood/article/details/13297535 http://ifeve.com/google-guava/ http:// ...
- Guava学习笔记(一)概览
Guava是谷歌开源的一套Java开发类库,以简洁的编程风格著称,提供了很多实用的工具类, 在之前的工作中应用过Collections API和Guava提供的Cache,不过对Guava没有一个系统 ...
- Guava monitor
Guava的com.google.util.concurrent类库提供了相对于jdk java.util.concurrent包更加方便实用的并发类,Monitor类就是其中一个.Monitor类在 ...
- 使用Guava EventBus构建publish/subscribe系统
Google的Guava类库提供了EventBus,用于提供一套组件内publish/subscribe的解决方案.事件总线EventBus,用于管理事件的注册和分发.在系统中,Subscribers ...
- Guava Supplier实例
今天想讲一下Guava Suppliers的几点用法.Guava Suppliers的主要功能是创建包裹的单例对象,通过get方法可以获取对象的值.每次获取的对象都为同一个对象,但你和单例模式有所区别 ...
随机推荐
- 第08组 Alpha冲刺(3/4)
队名 八组评分了吗 组长博客 小李的博客 作业博客 作业链接 组员1李昕晖(组长) 过去两天完成了哪些任务 文字/口头描述 11月19日了解各个小组的进度与难以攻破的地方,晚上安排开会,安排新的冲刺任 ...
- 一句命令激活windows/office (https://03k.org/kms.html)
https://03k.org/kms.html 本站上线KMS服务~一句命令激活windows/office kissshot2015年11月1日 装机必备, 软件下载 240 条评论 服务器地址: ...
- 将pip源设置国内源
windows (1)打开文件资源管理器(文件夹地址栏中) (2)地址栏上面输入 %appdata% (3)在这里面新建一个文件夹 pip (4)在pip文件夹里面新建一个文件叫做 pip.ini , ...
- SSAS 项目部署失败的问题
在创建SSAS项目过程中,创建数据源.数据源视图.多维数据集.纬度等一切都没有问题.但是在“进程”这一步的时候,发现总是报错,提示如下.OLE DB 错误: OLE DB 或 ODBC 错误 : 用户 ...
- djang-celery使用带密码的redis
前言: 网上很多django-celery使用redis(使用不带密码的redis)的用法都是千篇一律,那带密码的redis该怎么使用了呢,没有看到一篇有帮助的,在官网搜了下,发现以下用法,请看下面 ...
- java js ur特殊格式处理 json 特殊格式处理
url特殊格式处理: js中使用 encodeURIComponent() 编码对应的value $.ajax({ type: "post", url: "/tb_are ...
- EasyNVR摄像机网页无插件直播方案H5前端构建之:区分页面是自跳转页面还是分享页面
背景分析 EasyNVR整套方案的架构中,涉及到前端设备(摄像机IPC.硬盘录像机NVR.编码器等).流媒体服务端(EasyNVR).客户端终端设备(PC.浏览器.Android.iOS.微信). 在 ...
- svg轻松实现文字水印
1. 水印图片生成采用svg,这样可以运行时生成名字或其他信息的图片 svg模板 <svg xmlns="http://www.w3.org/2000/svg" xmlns: ...
- JAVA SpringBoot2 整合 ueditor 的 并实现前后端分离
1,下载 jsp 版本的 百度编辑器,点击此处下载 2,解压文件,解压后目录如下 3,我们将 jsp 目录暂时移动到别的地方,剩下的文件作为一个 H5 前端项目使用,笔者这边导入到 idea 进行开发 ...
- golang xorm时区问题
mysql连接后面加 &loc=Local 否则执行sql的时间格式,存到数据库会按0时区 UTC存储