对于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在实际项目中的应用的更多相关文章

  1. Guava Cache探索及spring项目整合GuavaCache实例

    背景 对于高频访问但是低频更新的数据我们一般会做缓存,尤其是在并发量比较高的业务里,原始的手段我们可以使用HashMap或者ConcurrentHashMap来存储. 这样没什么毛病,但是会面临一个问 ...

  2. Guava Cache用法介绍

    背景 缓存的主要作用是暂时在内存中保存业务系统的数据处理结果,并且等待下次访问使用.在日长开发有很多场合,有一些数据量不是很大,不会经常改动,并且访问非常频繁.但是由于受限于硬盘IO的性能或者远程网络 ...

  3. 【Guava】Guava Cache用法

    背景 缓存的主要作用是暂时在内存中保存业务系统的数据处理结果,并且等待下次访问使用.在日长开发有很多场合,有一些数据量不是很大,不会经常改动,并且访问非常频繁.但是由于受限于硬盘IO的性能或者远程网络 ...

  4. (翻译)Google Guava Cache

    翻译自Google Guava Cache This Post is a continuation of my series on Google Guava, this time covering G ...

  5. guava cache使用和源码分析

    guava cache的优点和使用场景,用来判断业务中是否适合使用此缓存 介绍常用的方法,并给出示例,作为使用的参考 深入解读源码. guava简介 guava cache是一个本地缓存.有以下优点: ...

  6. Guava Cache用法介绍<转>

    Guava Cache是在内存中缓存数据,相比较于数据库或redis存储,访问内存中的数据会更加高效.Guava官网介绍,下面的这几种情况可以考虑使用Guava Cache: 愿意消耗一些内存空间来提 ...

  7. Guava Cache源码浅析

    1. 简介 Guava Cache是指在JVM的内存中缓存数据,相比较于传统的数据库或redis存储,访问内存中的数据会更加高效,无网络开销. 根据Guava官网介绍,下面的这几种情况可以考虑使用Gu ...

  8. 第七章 企业项目开发--本地缓存guava cache

    1.在实际项目开发中,会使用到很多缓存技术,而且数据库的设计一般也会依赖于有缓存的情况下设计. 常用的缓存分两种:本地缓存和分布式缓存. 常用的本地缓存是guava cache,本章主要介绍guava ...

  9. 企业项目开发--本地缓存guava cache(1)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 1.在实际项目开发中,会使用到很多缓存技术,而且数据库的设计一般也会依赖于有缓存的情况下设计. 常用的缓存分 ...

随机推荐

  1. chrome 下载插件包及离线安装 附 Advanced Rest Client 下载

    最近需要测试http rest服务,由于chrome插件的轻便,首先想到了用chrome插件,在google商店找到Advanced Rest Client,用了一阵感觉不错. 于是项目组其他同事也要 ...

  2. Python第三方库之openpyxl(8)

    Python第三方库之openpyxl(8) 饼图 饼图将数据绘制成一个圆片,每个片代表整体的百分比.切片是按顺时针方向绘制的,0在圆的顶部.饼图只能取一组数据.该图表的标题将默认为该系列的标题. 2 ...

  3. Java程序员---技能树

    计算机基础: 比如网络相关的知识. 其中就包含了 TCP 协议,它和 UDP 的差异.需要理解 TCP 三次握手的含义,拆.粘包等问题. 当然上层最常见的 HTTP 也需要了解,甚至是熟悉. 这块推荐 ...

  4. 图论trainning-part-1 H. Qin Shi Huang's National Road System

    H. Qin Shi Huang's National Road System Time Limit: 1000ms Memory Limit: 32768KB 64-bit integer IO f ...

  5. hdu 1536 sg (dfs实现)

    S-Nim Time Limit: 5000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Submi ...

  6. Codeforces Round #287 (Div. 2) E. Breaking Good [Dijkstra 最短路 优先队列]

    传送门 E. Breaking Good time limit per test 2 seconds memory limit per test 256 megabytes input standar ...

  7. AFNetworking实时监测网络连接

    
// 网络变化消息 [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(checkNetwork) ...

  8. GridView动态添加View

    activity_main.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout ...

  9. Linux 下 GCC 编译共享库控制导出函数的方法

    通过一些实际项目的开发,发现这样一个现象,在 Windows 下可以通过指定 __declspec(dllexport) 定义来控制 DLL(动态链接库)中哪些函数可以导出,暴露给其他程序链接使用,哪 ...

  10. BZOJ 1090 字符串折叠(Hash + DP)

    题目链接 字符串折叠 区间DP.$f[l][r]$为字符串在区间l到r的最小值 正常情况下 $f[l][r] = min(f[l][r], f[l][l+k-1]+f[l+k][r]);$ 当$l$到 ...