在软件开发中,Cache缓存技术一直是非常重要的主题,不管我们正在进行任何简单的编程工作,我们总能在编程过程中找到一些缓存机制,即使是你使用一个 Map获取静态的值,它也是缓存的一种形式,但我们大多数人并不清楚其中的缓存使用。Guava Cache为我们提供了相比于简单的HashMap更强大和灵活的缓存机制,并不像Ehcache和Memcache那样具有健壮性。在本系列的学习中,我们将学习Guava提供的缓存机制,首先我们学习Guava Cache(一)MapMaker。
我们可以在com.google.common.collect包下面找到MapMaker类,那么问题来了,我们为什么不在Guava Collections系列中学习,尽管我们可能已经在Collections的学习中提到了MapMaker,本篇中,我们将MapMaker作为一个提供最基本缓存功能的类进行学习,MapMaker类使用了流畅的接口API,允许我们快速的构造ConcurrentHashMap,我们来看下面的例子:
ConcurrentMap books = new
MapMaker().concurrencyLevel(2)
.softValues()
.makeMap();
上面的例子中,我们构造了一个ConcurrentHashMap,使用String类型作为key,使用Book对象作为value值,通过对 ConcurrentHashMap声明的泛型进行指定,我们首先调用了concurrencyLevel()方法,设置了我们允许在map中并发修改的数量,我们还指定了softValues()方法,这样map中的value值都包裹在一个SoftReference(软引用)对象中,可以在内存过低的时候被当作垃圾回收。
其他我们可以指定的方法还包括:weakKeys()和weakValues(),但是MapMaker没有提供softKeys(),当我们给keys 或values使用WeakReferences(弱引用)或SoftReference(软引用)时,如果键值的其中一个被当做垃圾回收,整个键值对就会从map中移除,剩余的部分并不会暴露给客户端。
最后值得注意的一点:MapMaker中的softValues()方法在最近的几个guava版本中,已经被标注为 Deprecated,MapMaker中的缓存机制已经被移动到com.google.common.cache.CacheBuilder 中,MapMaker中的softValues()方法也已经被替换为com.google.common.cache.CacheBuilder#softValues,CacheBuilder的实现是来自MapMaker分支的一个简单增强版API。
在开始Guava CacheBuilder的学习之前,我们先来做一些准备工作:
Guava Cache缓存机制有两个基本的接口:Cache和LoadingCache,LoadingCache接口继承自Cache接口,本篇我们首先来学习Cache接口。
Cache
Cache接口提供了键和值的映射,但是Cache接口中提供的一些方法比HashMap提供的更基本。使用maps或caches的传统做法是:我们提供一个key,如果缓存中存在key对应的value值,我们就将这个value值返回,否则的话,如果通过相应的key找到映射关系,就返回null 值,为了在缓存中设置value值,我们可能需要这样做:
put(key,value); |
我们需要明确的在cache或map中关联key和value,Cache使用了传统的put方法设置value值,但是获取value的时候,Guava Cache有自己的调用风格,如下:
V value = cache.get(key, Callable Extends V> value); |
上面的方法会检索当前的value值,如果值不存在的话会从Callback实例中提取value值,通过key关联value,并返回相应的value值,它为我们提供了通过调用一个方法来替代下面风格的编程:
value = cache.get(key);
if(value == null){
value =someService.retrieveValue();
cache.put(key,value);
}
一个回调函数的使用意味着一个异步操作可能会发生,但是如果我们不需要执行一个异步任务我们又该怎么做呢?我们可以使用com.google.common.util. concurrent包里面的Callables类,Callables提供了一个方法用于处理Callable接口,使用代码如下所示:
Callable |
在上面的代码中,我们调用returning()方法构造并返回了一个Callable实例,当这个Callable实例调用get方法时会返回我们传递的“Foo”值,所以我们可以重新实现一下我们最初的代码:
cache.get(key,Callables.returning(someService.retrieveValue()); |
需要记住的是,如果value存在,缓存的value就会被返回。如果我们偏爱“如果存在...否则...”这样的编程风格的话,Guava同样提供了 getIfPresent(key)方法,使我们可以以传统方式编程,同样也一些使缓存中的value值失效的方法,如下所示:
invalidate(key):废弃缓存中当前key对应的所有value值。
invalidateAll():废弃缓存中所有的value值。
invalidateAll(Iterable>keys):废弃传入key集合中对应的所有缓存中的value值。
LoadingCache接口扩展了Cache接口的自动装载的功能,考虑下面的代码实例:
Book book = loadingCache.get(id); |
在上面的代码中,如果book对象在get方法调用执行的时候不可用,LoadingCache知道怎样返回对象,保存在缓存中,然后返回value值。
Loading values
由于LoadingCache的实现是线程安全的,通过同样的key值调用get方法,当缓存还在装载的时候就会阻塞线程。一旦value值被加载,该调用将返回最原始的调用get方法返回的value值,然而,多个通过不同key值的调用则会并发的加载,如果我们拥有一个键值的集合,并且想要去检索每个键对应的value值,我们需要像下面这样调用:
ImmutableMap |
如上所示,getAll方法返回了一个不可变的Map集合,通过指定键值以及与键值关联的缓存中的value值,getAll返回的map可能是所有缓存的value值,也可能是新检索到的value值,或者是已有缓存和新检索到的value值的混合。
Refreshingvalues in the cache
LoadingCache 同样提供了一种缓存中更新value值的机制:
refresh(key); |
通过调用refresh方法,LoadingCache会检索到对应key新的value值,当前的value值在新的value值返回前不会被从缓存出废弃,这意味着如果在加载的过程中调用get方法,则会返回缓存中当前的value值。如果在refresh方法调用中出现异常,原始的value值会依然在缓存中保存。需要记住的是:如果value值被异步的检索,在value值被真正的更新前,方法都将会返回原始的value值。
我们了解了Cache 和 LoadingCache,它们是Guava Cache缓存机制的基础接口,我们本篇即将学习的CacheBuilder类,通过建造者(Builder)模式为我们提供了一种获取Cache和 LoadingCache实例的方式,接下来,我们就开始Guava Cache系列:CacheBuilder的学习。
CacheBuilder提供了许多选项,我们可以选择部分来创建Cache实例,不需要列出所有的选项,这是Builder模式的魅力,接下来,我们通过下面的一些例子,来了解如何在guava 中使用Cache缓存机制,第一个例子演示的是在缓存条目加载到缓存中后,我们如何指定条目失效:
LoadingCache tradeAccountCache =
CacheBuilder.newBuilder()
.expireAfterWrite(5L, TimeUnit.MINUTES)
.maximumSize(5000L)
.removalListener(new TradeAccountRemovalListener())
.ticker(Ticker.systemTicker())
.build(new CacheLoader() {
@Override
public TradeAccount load(String key)throws
Exception {
return
tradeAccountService.getTradeAccountById(key);
}
});
上面的代码中,我们为如下所示的TradeAccount对象构造了一个LoadingCache实例:
publicclassTradeAccount {
privateString id; //ID
privateString owner; //所有者
privatedouble balance; //余额
}
我们来简要的对第一个例子做一些说明:
1. 首先,我们调用了expireAfterWrite方法,它可以自动的使缓存条目在指定的时间后失效,在本例中,是5分钟。
2. 第二步,我们通过maximumSize方法,5000作为传入值,指定了缓存的最大大小,当缓存的大小逼近到最大值时,缓存中一些最近很少使用到的条目将会被移除,不一定在缓存大小达到最大值甚至超过最大值才移除。
3. 我们注册了一个RemovalListener监听器实例,它可以在缓存中的条目被移除后接收通知,更多RemovalListener,我们将在Guava库学习:学习Guava Cache(七)中进行学习,敬请关注。
4. 我们添加了一个Ticker实例,通过调用ticker方法,此方法提供了缓存条目过期的时间,纳秒级的精密度。
5. 最后,我们调用了build方法,传入了一个新的CacheLoader实例,当缓存中的key存在,value不存在时,这个实例将被用来重新获取TradeAccount对象。
在接下来的实例中,我们来看看怎么使缓存条目失效,基于上一次条目被访问后所经过的时间:
LoadingCache bookCache = CacheBuilder.newBuilder()
.expireAfterAccess(20L, TimeUnit.MINUTES)
.softValues()
.removalListener(new BookRemovalListener())
.build(newCacheLoader() {
@Override
publicBook load(String key)throws Exception {
returnbookService.getBookByIsbn(key);
}
});
在这个例子中,我们对代码做了轻微的调整,我们来进行一下说明:
1. 通过调用expireAfterAccess方法,我们指定了缓存中的条目在上一次访问经过20分钟后失效。
2. 我们不再明确的指定缓存的最大值,而是通过调用softValues()方法,让JVM虚拟机将缓存中的条目包装成软引用对象,以限制的缓存大小。如果内存空间不足,缓存中的条目将会被移除。需要注意的是,哪些软引用可以被垃圾回收是由JVM内部进行的LRU计算所决定的。
3. 最后,我们添加了一个类似的RemovalListener监听器,用于处理缓存中value值不存在的条目。
来看最后一个例子,我们将介绍如何自动的刷新LongCache缓存中的条目值:
LoadingCache tradeAccountCache =
CacheBuilder.newBuilder()
.concurrencyLevel(10)
.refreshAfterWrite(5L, TimeUnit.SECONDS)
.ticker(Ticker.systemTicker())
.build(newCacheLoader() {
@Override
publicTradeAccount load(String key)
throws Exception {
return
tradeAccountService.getTradeAccountById(key);
}
});
在上面的例子中,我们再次做了细微的调整,说明如下:
1. 通过调用concurrencyLevel方法,我们设置了并发更新操作的数量为10,如果不设置的话,默认值为4。
2. 不再明确的移除缓存条目,而是通过refreshAfterWrite方法在给定的时间过去后,刷新缓存中的条目值,当缓存条目的值被调用并且已经超过了设置的时间,刷新缓存的触发器将处于活动状态。
3. 我们添加了纳秒级别的Ticker,以刷新那些符合条件的条目值。
4. 最后,通过调用build方法,我们指定了需要使用的Loader。
代码地址:http://git.codeweblog.com/realfighter/xx566-diary/blob/master/src/guava/CacheBuilderTest.java
这里推荐一篇文章,对CacheBuilder的机制有比较深入的讲解:Guava缓存器源码分析——CacheBuilder,可以学习下。
我们学习并了解了使用CacheBuilder方便的构造Cache和LoadingCache实例,我们可以通过CacheBuilder提供的 newBuilder()方法,使用建造者模式构建CacheBuilder实例,另外Guava Cache提供了CacheBuilderSpec类创建CacheBuilder实例,接下来,我们就来开始Guava Cache:CacheBuilderSpec的学习。
CacheBuilderSpec类以解析代表CacheBuilder配置的字符串的形式来创建CacheBuilder实例,需要说明的是,Guava没有处理编译时异常,这将会导致当传入的字符串无效时,会出现编译时异常,下面是一个可以用来创建CacheBuilderSpec实例的有效字符串:
//配置CacheBuilder的字符串
String configString = "concurrencyLevel=10,refreshAfterWrite=5s";
通过上面的字符串,我们可以创建一个与(三)CacheBuilder 中最后一个例子相同的CacheBuilder实例,通过一些方法来指定时间(refreshAfterWrite,expireAfterAccess,等等),间隔的整数后面的‘s’,‘m’,‘h’或‘d’对应于秒,分钟,小时或天数,这里没有毫秒甚至纳秒级别的设置,当我们指定好了配置的字符串,我们可以通过下面的方式创建一个CacheBuilderSpec实例:
//解析字符串,创建CacheBuilderSpec实例
CacheBuilderSpecspec = CacheBuilderSpec.parse(configString);
我们可以通过下面的方式,使用CacheBuilderSpec实例创建CacheBuilder实例:
//通过CacheBuilderSpec实例构造CacheBuilder实例
CacheBuilder.from(spec);
这里,我们调用了CacheBuilder中的静态from方法,使用CacheBuilderSpec 对象构造了CacheBuilder实例,通过格式化的字符串设置了CacheBuilder的属性,我们可以使用返回的CacheBuilder实例,像我们之前一样调用一些适当的方法,比如添加一个RemovalListener或者通过CacheBuilder创建一个LoadingCache实例:
//配置CacheBuilder的字符串
String spec = "concurrencyLevel=10,expireAfterAccess=5m,softValues";
//解析字符串,创建CacheBuilderSpec实例
CacheBuilderSpeccacheBuilderSpec = CacheBuilderSpec.parse(spec);
//通过CacheBuilderSpec实例构造CacheBuilder实例
CacheBuildercacheBuilder = CacheBuilder.from(cacheBuilderSpec);
//ticker:设置缓存条目过期时间
//removalListener:监听缓存条目的移除
cacheBuilder.ticker(Ticker.systemTicker())
.removalListener(new TradeAccountRemovalListener())
.build(newCacheLoader() {
@Override
public TradeAccount load(String key)throws
Exception {
return
tradeAccountService.getTradeAccountById(key);
}
});
上面的例子中,我们注册了一个Ticker实例和RemovalListener实例,通过build方法指定了一个CacheLoader,以字符串方式使用CacheBuilderSpec用于示例的演示目的,通常情况下,这个字符串将从命令行输入,或来自于配置文件的读取。
代码地址: http://git.codeweblog.com/realfighter/xx566-diary/blob/master/src/guava/CacheBuilderSpecTest.java
CacheLoader是一个抽象类,因为其中的load方法是一个抽象方法,翻看源码,其中也提供了一个loadAll方法,用于接收一个 Iterable对象,loadAll方法代表加载所有包含在传入Iterable中的缓存条目(除非我们覆盖loadAll方法),CacheLoader中提供了两个静态方法,能够使我们利用之前Guava函数式编程中学习到的一些工具类,如Function、Supplier。
第一个方法接收一个Function对象,用于进行对象传入到传出的转换
CacheLoader
CacheLoader.from(Function
当Function对象作为CacheLoader.from方法的参数时,我们可以获取到返回的CacheLoader实例,key为传入的对象,value为key经过Function转换后的值,下面是一个简单的示例:
@Test
public void testCacheLoaderFunction() throws Exception {
Function function = newFunction() {
@Override
public Stringapply(Date input) {
returnnew SimpleDateFormat("yyyy-MM-dd").format(input);
}
};
CacheLoader cacheLoader = CacheLoader.from(function);
assertThat(cacheLoader.load(new Date()), is("2014-12-06"));
}
与之类似的,CacheLoader.from方法也可以接收一个Supplier,进行对象的包装构建:
CacheLoader<Object,Value> cacheLoader =
CacheLoader.from(Supplier<Value> supplier);
上面的方法,我们通过一个Supplier方法构建了一个CacheLoader实例,值得注意的是,任意传递给CacheLoader的键,都会导致Supplier的get方法被调用。下面是一个简单的示例:
@Test
publicvoidtestCacheLoaderSupplier() throws Exception {
Supplier supplier = new Supplier() {
@Override
publicLong get() {
//这里简单的返回时间戳
returnSystem.currentTimeMillis();
}
};
CacheLoader
代码地址:http://git.codeweblog.com/realfighter/xx566-diary/blob/master/src/guava/CacheLoaderTest.java
Guava Cache提供了一种非常简便的方式,用于收集缓存执行的统计信息,需要注意的是,跟踪缓存操作将会带来性能的损失,想要收集缓存的信息,我们只需要在使用CacheBuilder的时候声明我们想要收集统计信息即可:
LoadingCache<String,TradeAccount> tradeAccountCache = CacheBuilder.newBuilder() .recordStats()
上面的代码,我们通过建造者模式构造了一个LoadingCache实例,想要启用缓存信息的统计,我们唯一要做的就是在builder里面通过recordStats()注册,而想要获取统计的信息,我们只需要通过Cache或LoadingCache调用stats()方法,就将返回一个CacheStats实例,通过CacheStats实例可以获取到需要的统计信息,来看接下来的例子:
CacheStats cacheStats = cache.stats();
下面是一个概述的清单,我们可以通过CacheStats获取的一些信息:
1. 加载缓存条目值所耗费的平均时间;
2. 请求的缓存条目的命中率;
3. 请求的缓存条目的未命中率;
4. 缓存条数被移除的数量;
涉及缓存性能的还有许多的信息,上面的清单只是一些我们通过CacheStats获取到的一些信息,最后,我们翻开CacheStats的源码,整理一下其中提供的公共方法,如下:
requestCount():返回Cache的lookup方法查找缓存的次数,不论查找的值是否被缓存。
hitCount():返回Cache的lookup方法命中缓存的次数。
hitRate():返回缓存请求的命中率,命中次数除以请求次数。
missCount():返回缓存请求的未命中的次数。
missRate():返回缓存请求未命中的比率,未命中次数除以请求次数。
loadCount():返回缓存调用load方法加载新值的次数。
loadSuccessCount():返回缓存加载新值的成功次数。
loadExceptionCount():返回缓存加载新值出现异常的次数。
loadExceptionRate():返回缓存加载新值出现异常的比率。
totalLoadTime():返回缓存加载新值所耗费的总时间。
averageLoadPenalty():缓存加载新值的耗费的平均时间,加载的次数除以加载的总时间。
evictionCount():返回缓存中条目被移除的次数。
minus(CacheStatsother):返回一个新的表示当前CacheStats与传入CacheStats之间差异的CacheStats实例。
plus(CacheStatsother):返回一个新的表示当前CacheStats与传入CacheStats之间总计的CacheStats实例。
接下来,我们详细的介绍RemovalListener_RemovalNotification。
就像名字所表名的,当从缓存中移除一个条目时,RemovalListener会被通知,与Java中的大多数Listener类似,RemovalListener被设计成了一个接口,且有一个onRemoval()方法,可以接收一个RemovalNotification对象,RemovalListener参数化如下:
RemovalListener
其中,K表示我们想要去监听的键的类型,V表示当缓存条目移除时需要被通知的值的类型,如果我们想要知道任何被移除的条目,可以简单的使用Object做为K 和V的类型。
RemovalNotification
RemovalNotification表示RemovalLitener对象删除一个条目时接收信号的实例对象,RemovalNotification类实现了Map.Entry接口,因此,我们可以访问缓存条目中实际的键和值对象,我们需要注意的是,当由于垃圾回收机制条目被移除时,那些value值可能为null。
通过调用RemovalListener实例的getCause()方法,我们可以确定条目被移除的原因,它将会返回一个RemovalCause的枚举,枚举的可能值如下所示:
· COLLECTED:表明键或值被垃圾回收。
· EXPIRED:表明最近一次写条目或获取条目的时间超时。
· EXPLICIT:表明用户手动的移除条目。
· REPLACED:表明条目不是真正的被移除,只是value值被改变。
· SIZE:表明由于Cache的长度达到或接近设置的最大限制,条目被移除。
当条目被移除时,如果我们需要执行任何的操作,最好是异步的进行
RemovalListenersRemovalListeners能够帮助我们异步的处理,缓存条目移除后的通知,为了让我们的Listener可以在条目被移除时异步的处理通知,我们可以像下面这样调用RemovalListeners. asynchronous方法:
private ExecutorService executor;
private ListeningExecutorService executorService;
@Before
publicvoidsetUp(){
executor = Executors.newCachedThreadPool();
executorService = MoreExecutors.listeningDecorator(executor);
}
@Test
publicvoidtestRemovalListener(){
RemovalListener myRemovalListener = new
RemovalListener() {
@Override
publicvoidonRemoval(RemovalNotification notification){
//Do something here
}
};
RemovalListener removalListener =
RemovalListeners.asynchronous(myRemovalListener, executorService);
}
上面,我们通过构造好的RemovalListener和executorService实例,并被它们作为参数传入RemovalListeners.asynchronous的方法中。这个方法返回了一个RemovalListener实例,将异步的处理移除条目后的通知,这个步骤会发生在我们将这个RemovalListener注册到CacheBuilder实例后。
最近在做性能测试,突然发现内存溢出,根据现象发现是cache调用有问题,所以了解一下cache的命中率。参考了这几篇文章,为了以后方便,则自己作为笔记记录博客。原文详情请参考:
http://www.codeweblog.com/guava%E5%BA%93%E5%AD%A6%E4%B9%A0-%E5%AD%A6%E4%B9%A0guava-cache%E7%9F%A5%E8%AF%86%E6%B1%87%E6%80%BB/