对于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使用的更多相关文章

  1. Spring cache简单使用guava cache

    Spring cache简单使用 前言 spring有一套和各种缓存的集成方式.类似于sl4j,你可以选择log框架实现,也一样可以实现缓存实现,比如ehcache,guava cache. [TOC ...

  2. Guava库介绍之实用工具类

    作者:Jack47 转载请保留作者和原文出处 欢迎关注我的微信公众账号程序员杰克,两边的文章会同步,也可以添加我的RSS订阅源. 本文是我写的Google开源的Java编程库Guava系列之一,主要介 ...

  3. Google Java编程库Guava介绍

    本系列想介绍下Java下开源的优秀编程库--Guava[ˈgwɑːvə].它包含了Google在Java项目中使用一些核心库,包含集合(Collections),缓存(Caching),并发编程库(C ...

  4. [Java 缓存] Java Cache之 Guava Cache的简单应用.

    前言 今天第一次使用MarkDown的形式发博客. 准备记录一下自己对Guava Cache的认识及项目中的实际使用经验. 一: 什么是Guava Guava工程包含了若干被Google的 Java项 ...

  5. [转载]Google Guava官方教程(中文版)

      原文链接  译文链接 译者: 沈义扬,罗立树,何一昕,武祖  校对:方腾飞 引言 Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如:集合 [collections] ...

  6. java开发人员,最应该学习和熟练使用的工具类。google guava.(谷歌 瓜娃)

    学习参考文章: http://blog.csdn.net/wisgood/article/details/13297535 http://ifeve.com/google-guava/ http:// ...

  7. Guava学习笔记(一)概览

    Guava是谷歌开源的一套Java开发类库,以简洁的编程风格著称,提供了很多实用的工具类, 在之前的工作中应用过Collections API和Guava提供的Cache,不过对Guava没有一个系统 ...

  8. Guava monitor

    Guava的com.google.util.concurrent类库提供了相对于jdk java.util.concurrent包更加方便实用的并发类,Monitor类就是其中一个.Monitor类在 ...

  9. 使用Guava EventBus构建publish/subscribe系统

    Google的Guava类库提供了EventBus,用于提供一套组件内publish/subscribe的解决方案.事件总线EventBus,用于管理事件的注册和分发.在系统中,Subscribers ...

  10. Guava Supplier实例

    今天想讲一下Guava Suppliers的几点用法.Guava Suppliers的主要功能是创建包裹的单例对象,通过get方法可以获取对象的值.每次获取的对象都为同一个对象,但你和单例模式有所区别 ...

随机推荐

  1. Jdk8 DNS解析

    注:JDK7和JDK8关于DNS解析的实现有差异,该问题在JDK7下可能不存在: Java中的DNS解析一般是通过调用下面的方法: public static InetAddress getByNam ...

  2. java join()基本用法与说明解释

    join()方法的作用,是等待这个线程结束: 也就是说,t.join()方法阻塞调用此方法的线程(calling thread)进入 TIMED_WAITING 状态,直到线程t完成,此线程再继续: ...

  3. arcpy实例教程-地图图层导出到要素类

    arcpy实例教程-地图图层导出到要素类 商务合作,科技咨询,版权转让:向日葵,135-4855_4328,xiexiaokui#qq.com 功能:将选定图层的范围导出到内存要素类 描述:将选定图层 ...

  4. LiteIDE 设置默认编译输出位置

    go build命令默认输出路径是当前工作路径,这个显得比较乱,我习惯于把输出文件放置在项目的build目录下,这样方便查找,已经同步备份的时候排除不必要的文件. go build命令可以指定-o 输 ...

  5. VS2015 创建C++动态库及使用

    转载:https://blog.csdn.net/w_x_myself/article/details/82252646 1.dll的特点 代码复用是提高软件开发效率的重要途径.一般而言,只要某部分代 ...

  6. Java基础 awt Frame 窗体在屏幕的中间显示

        JDK :OpenJDK-11      OS :CentOS 7.6.1810      IDE :Eclipse 2019‑03 typesetting :Markdown   code ...

  7. nodejs相关框架

    sails   https://sailsjs.com/documentation/concepts koa  koa 是由 Express 原班人马打造的,致力于成为一个更小.更富有表现力.更健壮的 ...

  8. Python向excel中写入数据的方法 方法简单

    最近做了一项工作需要把处理的数据写入到Excel表格中进行保存,所以在此就简单介绍使用Python如何把数据保存到excel表格中. 数据导入之前需要安装 xlwt依赖包,安装的方法就很简单,直接 p ...

  9. LeetCode 100. Same Tree (判断树是否完全相同)

    100. Same Tree Given two binary trees, write a function to check if they are the same or not. Two bi ...

  10. Spring Boot接入 apollo 后启动 dubbo 报错

    原文地址:https://xobo.org/spring-boot-apollo-dubbo-xml-error/ 某Spring Boot项目接入 apollo 后启动 dubbo 报错Caused ...