对于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. C#实现图像拖拽以及锚点缩放功能

    本文主要实现C#窗体图像拖拽以及锚点缩放功能 1.新建Windows窗体应用项目,添加一个panel控件,在panel控件上添加picturebox控件 代码如下: using System; usi ...

  2. PL/SQL【32位】连接Oracle 11g【64位】

    下载64位Oracle安装: 下载PL/SQL安装:下载instantclient-basic-win32-12.1.0.1.0.zip,解压后剪切instantclient_12_1文件夹,粘贴到O ...

  3. 带有Q_OBJECT的类要放在头文件的第一个类位置,否则可能无法moc

    如果头文件中有多个类,带有Q_OBJECT的类要放在头文件的第一个类位置,否则可能无法moc

  4. pycharm安装、首次使用及汉化

    摘自:https://blog.csdn.net/qq_30463497/article/details/84992480 一.下载pycharm安装包打开PyCharm的官方下载地址:http:// ...

  5. LeetCode_455. Assign Cookies

    455. Assign Cookies Easy Assume you are an awesome parent and want to give your children some cookie ...

  6. c# .net 4.5.2 asp.net mvc 使用hangfire

    一定要有hangfire数据库,否则hangfire会报错. (obStorage.Current property value has not been initialized. You must ...

  7. linux_permission

    linux命令与权限(permission denied):权限问题 a  上调权限 chmod 777 test.php b  更改为root su root 提示输入root密码 c  root权 ...

  8. python + selenium + unittest实现简单的UI自动化

    使用的版本是python 3,其中HTMLTestRunner是修改版本,参考以下博客并下载 https://blog.csdn.net/zhanin123/article/details/78950 ...

  9. PAt 1099

    1099 Build A Binary Search Tree (30 分)   A Binary Search Tree (BST) is recursively defined as a bina ...

  10. matlab 双坐标折线图画法

    %%各时段电量需求 clc close all clear all cost_gd = [2200 1800 3800 4600]; cost_bj = [2.7 2.2 1.8 3.6]; cost ...