本地缓存实现,支持多种缓存过期策略
本文主要结合一些例子介绍了一下Guava缓存的使用以及其一些简单特点,如果想了解缓存、JVM缓存、分布式缓存等特点,请自行搜索资料
— By Syahfozy
/**
* CacheLoader
* LoadingCache是附带CacheLoader构建而成的缓存实现
* 创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法
*/
LoadingCache cachedFib = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterAccess(10, TimeUnit.MINUTES)
.build(
new CacheLoader() {
public Integer load(Integer key) {
return fib(key);
}
});
try {
// 这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值
int value = cachedFib.get(20);
// 6765
System.out.println(value);
// 如果定义的CacheLoader没有声明任何检查型异常,则可以通过getUnchecked(K)查找缓存
// 一旦CacheLoader声明了检查型异常,就不可以调用getUnchecked(K)
value = cachedFib.getUnchecked(20);
// 6765
System.out.println(value);
// 执行批量查询
ImmutableMap immutableMap = cachedFib.getAll(Ints.asList(20, 21, 22));
// {20=6765, 21=10946, 22=17711}
System.out.println(immutableMap);
// 如果批量的加载比多个单独加载更高效,可以重载CacheLoader.loadAll来利用这一点, getAll(Iterable)的性能也会相应提升
LoadingCache cachedFib2 = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(
new CacheLoader() {
@Override
public Integer load(Integer key) {
return fib(key);
}
@Override
public Map loadAll(Iterable extends Integer> keys) throws Exception {
return super.loadAll(keys);
}
});
} catch (
ExecutionException e) {
e.printStackTrace();
}
所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable)方法。
这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。
这个方法简便地实现了模式"如果有缓存则返回;否则运算、缓存、然后返回"。
Cache cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(); // look Ma, no CacheLoader
...
try {
// 如果有缓存则返回;否则运算、缓存、然后返回
cache.get(key, new Callable() {
@Override
public Value call() throws AnyException {
return doThingsTheHardWay(key);
}
});
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
使用cache.put(key, value)方法可以直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值。
使用Cache.asMap()视图提供的任何方法也能修改缓存。但请注意,asMap视图的任何方法都不能保证缓存项被原子地加载到缓存中。进一步说,asMap视图的原子运算在Guava Cache的原子加载范畴之外,所以相比于Cache.asMap().putIfAbsent(K,V),Cache.get(K, Callable) 应该总是优先使用。
cache.put(21, 10946);
cache.asMap().putIfAbsent(23, 6766);
System.out.println(cache.asMap());
一个残酷的现实是,我们几乎一定没有足够的内存缓存所有数据。你你必须决定:什么时候某个缓存项就不值得保留了?
Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。
如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或总体上很少使用的缓存项。——警告:在缓存项的数目达到限定值之前,缓存就可能进行回收操作——通常来说,这种情况发生在缓存项的数目逼近限定值时。
另外,不同的缓存项有不同的“权重”(weights)——例如,如果你的缓存值,占据完全不同的内存空间,你可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重。在权重限定场景中,除了要注意回收也是在重量逼近限定值时就进行了,还要知道重量是在缓存创建时计算的,因此要考虑重量计算的复杂度。
LoadingCache graphs = CacheBuilder.newBuilder()
.maximumWeight(100000)
.weigher(new Weigher() {
public int weigh(Key k, Graph g) {
return g.vertices().size();
}
})
.build(
new CacheLoader() {
public Graph load(Key key) { // no checked exception
return createExpensiveGraph(key);
}
});
CacheBuilder提供两种定时回收的方法:
Cache timed = CacheBuilder.newBuilder()
.expireAfterAccess(100, TimeUnit.SECONDS)
.build();
Cache timed2 = CacheBuilder.newBuilder()
.expireAfterWrite(100, TimeUnit.SECONDS)
.build();
测试定时回收:
对定时回收进行测试时,不一定非得花费两秒钟去测试两秒的过期。你可以使用Ticker接口和CacheBuilder.ticker(Ticker)方法在缓存中自定义一个时间源,而不是非得用系统时钟。
通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:
CacheBuilder.newBuilder().weakKeys().build();
CacheBuilder.newBuilder().weakValues().build();
CacheBuilder.newBuilder().softValues().build();
任何时候,你都可以显式地清除缓存项,而不是等到它被回收:
Cache cacheClean = CacheBuilder.newBuilder().build();
// 个别清除
cacheClean.invalidate(1);
// 批量清除
cacheClean.invalidateAll(Ints.asList(1, 2, 3));
// 清除所有缓存项
cacheClean.invalidateAll();
通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值。
请注意,RemovalListener抛出的任何异常都会在记录到日志后被丢弃[swallowed]。
/**
* 移除监听器
*/
CacheLoader loader = new CacheLoader () {
public Integer load(Integer key) throws Exception {
return fib(key);
}
};
RemovalListener removalListener = new RemovalListener() {
public void onRemoval(RemovalNotification removal) {
System.out.println(String.format("key: %d, value: %d be removed . Cause: %s ", removal.getKey(), removal.getValue(), removal.getCause()));
}
};
Cache listenCache = CacheBuilder.newBuilder()
.expireAfterAccess(2, TimeUnit.SECONDS)
.removalListener(removalListener)
.build(loader);
listenCache.put(1, fib(1));
listenCache.put(2, fib(2));
listenCache.invalidateAll();
// 把监听器装饰为异步操作
// 避免代价高昂的监听器方法在同步模式下拖慢正常的缓存请求
RemovalListener async = RemovalListeners.asynchronous(removalListener, Executors.newSingleThreadExecutor());
listenCache = CacheBuilder.newBuilder()
.expireAfterAccess(2, TimeUnit.SECONDS)
.removalListener(async)
.build(loader);
listenCache.put(1, fib(1));
listenCache.put(2, fib(2));
listenCache.invalidateAll();
// 使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,只会在写操作时顺带做少量的维护工作
// 如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作
// 如果你的缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()
listenCache.cleanUp();
Guava做cache时候数据的移除分为被动移除和主动移除两种。
被动移除分为三种:
基于大小的移除:数量达到指定大小,会把不常用的键值移除
基于时间的移除:expireAfterAccess(long, TimeUnit) 根据某个键值对最后一次访问之后多少时间后移除
expireAfterWrite(long, TimeUnit) 根据某个键值对被创建或值被替换后多少时间移除
基于引用的移除:主要是基于java的垃圾回收机制,根据键或者值的引用关系决定移除
主动移除分为三种:1).单独移除:Cache.invalidate(key)
2).批量移除:Cache.invalidateAll(keys)
3).移除所有:Cache.invalidateAll()
如果配置了移除监听器RemovalListener,则在所有移除的动作时会同步执行该listener下的逻辑。
如需改成异步,使用:RemovalListeners.asynchronous(RemovalListener, Executor)
可能遇到的问题:
在put操作之前,如果已经有该键值,会先触发removalListener移除监听器,再添加配置了expireAfterAccess和expireAfterWrite,但在指定时间后没有被移除。
解决方案:CacheBuilder构建的缓存不会在特定时间自动执行清理和回收工作,也不会在某个缓存项过期后马上清理,它不会启动一个线程来进行缓存维护,因为a)线程相对较重,b)某些环境限制线程的创建。它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做。当然,也可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。
//有些键不需要刷新,并且我们希望刷新是异步完成的
LoadingCache reload = CacheBuilder.newBuilder()
.maximumSize(1000)
// 可以为缓存增加自动定时刷新功能
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(
new CacheLoader() {
public Integer load(Integer key) { // no checked exception
return fib(key);
}
public ListenableFuture reload(final Integer key, Integer prev) {
if (neverNeedsRefresh(key)) {
return Futures.immediateFuture(prev);
} else {
// asynchronous!
ListenableFutureTask task = ListenableFutureTask.create(new Callable() {
public Integer call() {
return fib(key);
}
});
Executors.newSingleThreadExecutor().execute(task);
return task;
}
}
});
CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回CacheStats对象以提供如下统计信息:
此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。
简单使用:
Cache record = CacheBuilder
.newBuilder()
// 用来开启Guava Cache的统计功能
.recordStats().
build();
CacheStats stats = record.stats();
// 缓存命中率
stats.hitRate();
// 加载新值的平均时间,单位为纳秒
stats.averageLoadPenalty();
// 缓存项被回收的总数,不包括显式清除
stats.evictionCount();
asMap视图提供了缓存的ConcurrentMap形式,但asMap视图与缓存的交互需要注意:
Cache numCache = CacheBuilder.newBuilder().build();
numCache.put(1, "one");
numCache.put(2, "two");
// 包含当前所有加载到缓存的项
ConcurrentMap asMap = numCache.asMap();
// {2=two, 1=one}
System.out.println(asMap);
// 当前所有已加载键
Set keySet =numCache.asMap().keySet();
// [2, 1]
System.out.println(keySet);
// 实质上等同于cache.getIfPresent(key),而且不会引起缓存项的加载
String one = numCache.asMap().get(0);
// null
System.out.println(one);
缓存加载方法(如Cache.get)不会抛出InterruptedException。我们也可以让这些方法支持InterruptedException,但这种支持注定是不完备的,并且会增加所有使用者的成本,而只有少数使用者实际获益。
想了解更多请见Guava缓存