guava_缓存

Guava Cache

在软件开发的过程,缓存是一个非常重要的话题。 在稍微复杂的开发过程中,我们基本上是不可能不使用到缓存的。 至少我们会使用Map去存储一些东西。 这其实就是一个最简单的缓存。 Guava给我们提供了比简单的使用HashMap更强大更灵活的功能,但是和专业的缓存工具相比,(EHCache,Memcached)功能还有些不足, 那么这一章,我们将覆盖Guava cache的下面几个方面:

– 使用MapMaker类创建ConcurrentMap实例
– 使用CacheBuilder的链式编程的方式创建LoadingCache和Cache实例
– 使用CacheBuilderSpec类通过格式化的字符串创建CacheBuilder实例
– 使用LoadingCache的实例CacheLoader通过指定的key获取对应的值
– 使用CacheStats类查看Cache的使用状态
– 使用RemovalListener类接受一个entry从map中删除的事件

下面让我们逐一介绍一下:

MapMaker

MapMaker类在com.google.common.collect包中,我们来看一下怎样使用MapMaker的链式编程快速创建一个ConcurrentHashMap。

ConcurrentMap books = new
MapMaker().concurrencyLevel(2)
.softValues()
.makeMap();

上面的例子中,我们创建了key为String类型,值为Book类型的ConcurrentHashMap. 其中concurrencyLevel()方法是为了指定同时可以用几个线程修改map的值。 softValues() 会将value包装成SoftReference对象,这样发生内存不够使用时,就会被垃圾回收自动回收。 还有其他的一些方法,比如weakKeys() weakValues(), 但是没有softKeys()这是为什么? 还要去看一下java的引用。 当我们使用weakReference或则SoftReference时,无论是key还是value被回收了,map中都会将整个entry移出,这样就不是出现key在value不在,或则value在key不在的情况。

Guava Caches

在我们使用和了解CacheBuilders之前,我们先了解一些关于Cache的背景知识, 在Guava中,我们有两个基本的接口 Cache 和LoadingCache,其中LoadingCache接口继承了Cache接口

Cache

Cache接口提供了基本的key到value的映射。另外也提供了一些比HashMap更好用的接口。 谈到Cache,我们一般的做法是给定一个key,cache会返回这个key对应的value,如果没有找到对应的value,那么将返回NULL. 如果我们想替换一个key/value值,我们可以调用如下方法:

put(key,value);

这里我们将key和value关联到cache或map中。 Cache接口中有传统的put方法,但是也有自己的特有的方法:

V value = cache.get(key, Callable value);

上面的方法中我们将根据key获取值,如果值在缓存中,就直接返回,如果不在那么就调用Callable获取值,并将获取到的值和对应的key关联,并返回得到的值。 这个方法就完成下面这段代码的功能:

value = cache.get(key);
if(value == null){
value = someService.retrieveValue();
cache.put(key,value);
}

上面的那个方法的使用中,我们需要传入一个Callable对象,但是一般情况下,我们会使用一个匿名的内部类, 这里有个小技巧,如果我们不使用内部类,我们有其他方法吗? 我们可以使用Callables类, 在com.googele.common.util.concurrent包中, Callables中有一个方法可以方便的返回一个值:

Callable value = Callables.returning("Foo");

上面的这条语句中,returning方法会创建返回一个Callable实例,并且当get方法调用时,会返回给定的’Foo’值。 这样的话,我们就可以替换上面例子的实现:

cache.get(key,Callables.returning(someService.retrieveValue());

这里我们要记住,如果值在缓存中,就直接返回, 如果我们期望 存在就返回,不存在就返回null. 那么可以使用getIfPresent(key) 方法。 Guava Cache中也有方法将值失效。
– invalidate(key):调用这个方法可以将指定的key的值丢弃
– invalidateAll(): 这个方法丢弃缓存中的所有值
– invalidateAll(Iterable

Loading Cache

LoadingCache继承了Cache实例,扩展了自己的方法,看一下下面的例子:

Book book = loadingCache.get(id);

在上面的例子中,如果book对象目前还不在缓存中,LoadingCache可以知道怎样去获取对象,存储对象并返回.

Loading values

LoadingCache的设计是线程安全的,针对同一个key的并发调用是会被block的,一旦value获取到了,这个value值就会返回给调用的get方法,但是如果调用get是多个不同的key那么我们就可以进行并发调用:

ImmutableMap<key,value> map = cache.getAll(Iterable Extends
key>);

getAll 返回了一个ImmutableMap 包含了指定的keys 和对应的values。返回的对象,有可能是直接从缓存中取得,有可能是新获取到的,有可能一部分是新获取的一部分是从缓存中取得。

Refreshing values in the cache

LoadingCache 提供了刷新缓存的方法:

refresh(key);

调用loadingcache的refresh方法,loadingcache会重新根据key获取值。 值得注意的地方是: 原来的值会一直等到新值回来才会被替换,在获取新值的过程如果出现异常,原来的值不会被丢弃。

CacheBuilder

CacheBuilder 提供通过建造者方式创建Cache和LoadingCache实例。 Cache实例提供了很多方法。 下面通过例子来感觉一下怎样使用Guava Cache。 第一个例子中我们展示了怎样从Cache中是一个entry失效。

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的定义如下:

public class TradeAccount {
private String id;
private String owner;
private double balance;
}

下面我们分析一下上面的这个例子:
1. 首先是expireAfterWrite方法,这个方法会自动在指定时间之后移出对应的entry。 在这个例子中是5分钟。
2. maximumSize() 指定了缓存的最大的数量。
3. 我们调用了RemovalListener增加了一个removalListener实例,当一个entry移出后,对调用removalListener方法。
4. 通过ticker方法增加了一个Ticker实例,提供了纳秒级的服务
5. 最后调用了build 方法,传入了一个CacheLoader实例,这样当一个key对应的value不在缓存中时,通过这个方法可以获取到对应的value。

下面的例子中,我们将了解基于最后访问时间使得entry失效:

LoadingCache bookCache = CacheBuilder.newBuilder()
.expireAfterAccess(20L,TimeUnit.MINUTES)
.softValues()
.removalListener(new BookRemovalListener())
.build(new CacheLoader() {
@Override
public Book load(String key) throws Exception
{
return bookService.getBookByIsbn(key);
}
});

在这个例子中,我们的做法有点不一样,下面我们来看一下这个例子。

  1. 首先我们调用expireAfterSeconds方法,当一个entry在20分钟没有被访问,就会自动从cache中移出。
  2. 使用jvm的softReference 软引用来代替我们限制cache的大小。 这样当memory不够用时,jvm会自动将entry从cache中移出。 jvm的移出规则是LRU (least-recently-used)算法。
  3. 最后我们加上了RemovalListener 来监听被移出的entry。

下面来看最后一个例子,看样子在guava中怎样自动的刷新缓存。

LoadingCache tradeAccountCache =
CacheBuilder.newBuilder()
.concurrencyLevel(10)
.refreshAfterWrite(5L,TimeUnit.SECONDS)
.ticker(Ticker.systemTicker())
.build(new CacheLoader() {
@Override
public TradeAccount load(String key)
throws Exception {
return
tradeAccountService.getTradeAccountById(key);
}
});

最后一个例子中,我们同样做了一些改变,下面我们解释一下具体的改变:

  1. 首先指定了最多可以并发修改的线程数,我们这里指定了10,如果没有指定默认值是4.
  2. 我们使用在指定时间后自动刷新value的方法替代显示的调用刷新,触发自动更新的条件是这个value被访问并且已经超过指定的时间。
  3. tricker 了纳秒级的触发器
  4. 最后我们传递了一个CacheLoad实例用于获取key对应的value。

CacheBuilderSpec

CacheBuilderSpec 类可以通过指定一个配置的字符串创建一个CacheBuilder实例。 但是这样有个不好的地方,就是我们不能在编译期发现错误,只能在运行期发现错误。 下面是一个用于创建CacheBuilderSpec的字符串:

String configString = "concurrencyLevel=10,refreshAfterWrite=5s"

这个字符串指定了并发更新线程数是10,访问5s后自动刷新,指定时间的方式可以通过(s,m,h,d)分别代表秒,分,时,天。 暂时还没有办法设置毫秒及纳秒级别。 指定了配置的字符串后,我们就可以创建CacheBuilderSpec。 创建方式如下:

CacheBuilderSpec spec = CacheBuilderSpec.parse(configString);

接着我们可以使用CacheBuilder的form方法获取CacheBuilder实例。 完整的例子如下:

String spec =
"concurrencyLevel=10,expireAfterAccess=5m,softValues";
CacheBuilderSpec cacheBuilderSpec =
CacheBuilderSpec.parse(spec);
CacheBuilder cacheBuilder =
CacheBuilder.from(cacheBuilderSpec);
cacheBuilder.ticker(Ticker.systemTicker())
.removalListener(new TradeAccountRemovalListener())
.build(new CacheLoader() {
@Override
public TradeAccount load(String key) throws
Exception {
return
tradeAccountService.getTradeAccountById(key);
}
});

一般这种指定字符串的方式用在命令行或则配置文件中。

CacheLoader

我们在之前的例子中,已经看到了CacheLoader的使用方法,但是还有一些细节还没覆盖到。 CacheLoader 是一个抽象类,因为里面的load方法是一个抽象方法, 在CacheLoader中还有一个loadAll方法. loadAll方法接受一个Iterable参数,其实内部也是循环调用load方法。 CacheLoader中还有两个比较熟悉的静态方法.
1. CacheLoader.from(Function

CacheStats

到目前为止,我们已经知道了很多cache的算法,但是我们现在想知道这些cache算法的工作情况. 但是我们要知道收集cache情况会或多或少降低cache的性能。 要监控cache的性能信息,我们只需要在创建cache实例的时候加上recordStats()方法:

LoadingCache tradeAccountCache =
CacheBuilder.newBuilder()
.recordStats()

我们采用我们比较熟悉的链式编程的方式加上了对cache的监控。 获取监控数据我们只需要要调用stats()方法得到一个CacheStatus实例。 代码示例:

CacheStats cacheStats = cache.stats();

下面是可以从CacheStats中获取到的监控信息:
– 每次获取新的值的时间
– 缓存的命中率
– 缓存失败率
– 值被剔除的次数

还有很多其他的信息,大家可以看cacheStats的接口文档。

RemovalListener

在CacheBuilder的例子中我们已经看到怎样使用RemovalListener。 就想名字所描述的一样,通知发生在当一个entry没移出时。 和java里面的其他listener接口一样,RemovalListener也有一个onRemoval方法。这个方法接受一个RemovalNotification对象,RemovalListener接受参数化的配置 RemovalListener

RemovalNotification

RemovalNotification对象是RemovalListener接受的参数,RemovalNotification对象实现了Map.Entry接口,这样我们就可以获取到被删除的key和value的值,但是值得注意的是我们有可能获取的key或者value的值为空。 因为有可能这些值被jvm回收了。 为了获取被移出的原因我们可以调用getCause()方法。 下面是可能被回收的原因的枚举:

– COLLECTED: 这个是key或者value被垃圾回收了
– ECPRED: 这个是这个entry最后一次访问时间或则最后一次写入时间超时了
– EXPLICIT: 这个表示是用户手动移出
– REPLACED: 这个表明 这个entry没有被移出,但是 value被重新写过
– SIZE: 这个表明因为cache的空间不足原因被移出

一般的,如果我们想在enrty被移出的时候 做一写操作,我们最好采取异步的方式、

RemovalListeners

RemovalListeners类是帮助我们异步处理移出后的动作。 使用方式如下:

RemovalListener<String,TradeAccount> myRemovalListener = new
RemovalListener<String, TradeAccount>() {
@Override
public void onRemoval(RemovalNotification<String,
TradeAccount> notification) {
//Do something here
}
};
RemovalListener<String,TradeAccount> removalListener =
RemovalListeners.asynchronous(myRemovalListener,executorService);

这里我们在asynchronous方法中传入了 RemovalListener 和 ExecutorService 参数。 这样就会调用executorService 帮助我们异步的处理 移出事件。 但是要注意的是,这个方法的要在我们调用 CacheBuilder.addRemovalListener之前。

总结

这一章中我们学习了GUAVA的使用方法。 我们学习
1. MapMaker创建一个ConcurrentMap
2. 我们学习的Cache和LoadingCache的一些常用方法
3. 学习了CacheBuilder的一些常用配置
4. CacheLoader 是LoadingCache的核心组件
5. CacheStats 统计Cache的性能信息
6. RemovalListener类 用来接受entry被移出事件

你可能感兴趣的:(Guava翻译)