一、接口声明
Guava 通过接口 LoadingCache 提供了一个强大的基于内存的LoadingCache<K,V>。
在缓存中自动加载值,它提供了很多实用的方法,在有缓存需求时非常有用。
@GwtCompatible
public interface LoadingCache<K, V> extends Cache<K, V>, Function<K, V> {//...}
二、接口方法
- ConcurrentMap<K,V> asMap()
- 返回存储在此缓存中的线程安全的键值对映射视图
- void cleanUp()
- 执行缓存所需的任何挂起维护操作
- V get(K key, Callable<? extends V> loader)
- 返回与此key关联的value,必要时从加载器获取该值
- ImmutableMap<K,V> getAllPresent(Iterable<?> keys)
- 返回与此key关联的value的map映射
- V getIfPresent(Object key)
- 返回与此key关联的value,如果没有,返回null
- void invalidate(Object key)
- 废弃缓存中当前key对应的所有value值
- void invalidateAll()
- 废弃缓存中的所有value值
- void put(K key, V value)
- 将value和缓存中的key进行关联
- void putAll(Map<K,V> map)
- 将map的所有映射复制到缓存
- size()
- 返回缓存中的条目数量
- stats()
- 返回缓存的累计统计信息的当前快照,如果缓存未记录统计信息,返回一组默认值
三、Guava Cache的适用性
- 缓存在很多场景下都是相当有用的,
例如,计算或者检索一个值的代价很高,并且对相同的输入需要不止一次的获取值的时候,就应当考虑使用缓存。 - 区别:Guava Cache 和 ConcurrentMap相似,但不完全一样。
最基本的区别是 ConcurrentMap会一致保存所有添加的元素,直到显式的移除。
相对的,Guava Cache为了限制内存的占用,通常都设定为自动回收元素。
在某些场景下,尽管LoadingCache不回收元素,它也是很有用的,应为它会自动加载缓存。 - 通常来说,Guava Cache 适用于:
- 愿意消耗一些内存空间来提升速度
- 预料到某些键的值会被查询一次以上
- 缓存中存放的数据总量不会超过内存容量(Guava Cache是单个应用运行时的本地缓存,它不会把数据放到文件或者外部服务器)
四、创建Cache范例
Cache实例从CacheBuilder构建器中获取
LoadingCache<String, Object> employeeCache = CacheBuilder.newBuilder()
.maximumSize(100) //最多可以缓存100条记录
.expireAfterAccess(30, TimeUnit.MINUTES) //缓存将在访问30分钟后过期
.build(new CacheLoader<String, Object>() {
@Override
public Object load(String key) throws Exception {
// 从数据库中加载
return getFromDataBase(key);
}
});
五、加载
在使用缓存前,首先问自己一个问题:有没有合理的默认方式来加载或者计算与key关联的value?
- 如果有的话,你应当使用CacheLoader。
- 如果没有或者你想要覆盖默认的加载运算,同时保留"get-if-absent-compute"的原子语句,
你应当在调用get时传入一个Callable实例。 - 注意:缓存元素也可以通过Cache.put的方式直接插入,
但是自动插入是首选项,因为它可以更容易地推断所有缓存内容的一致性。
任何缓存都应该提供get-if-absent-compute这一基础原子语义,具体含义如下:
- 从缓存中读取
- 缓存中存在该数据,直接返回
- 缓存中不存在该数据,从数据源中取
- 数据源中存在该数据,放入缓存,并返回
- 数据源中不存在该数据,返回空
5.1 CacheLoader (缓存加载器,自动加载)
LoadingCache 是附带CacheLoader构建而成的缓存实现。
创建CacheLoader通常只需要简单的实现V load(K key)throw Exception 方法。
// 根据员工ID为员工创建缓存
LoadingCache<String, Object> employeeCache = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterAccess(30, TimeUnit.MINUTES)
.build(new CacheLoader<String, Object>() {
// 一般重载load方法即可
@Override
public Object load(String key) throws Exception {
// 从数据库加载
return getFromDataBase(key);
}
@Override
public ListenableFuture<Object> reload(String key, Object oldValue) throws Exception {
return super.reload(key, oldValue);
}
@Override
public Map<String, Object> loadAll(Iterable<? extends String> keys) throws Exception {
return super.loadAll(keys);
}
});
5.2 get方法 (推荐使用)
从LoadingCache查询的正规方式是使用get(key)的方法。
这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值。
由于CacheLoader可能抛出异常,LoadingCache.get(key)也可以声明为抛出ExecutionException异常。
如果定义的CacheLoader么有声明任何检查时异常,则可以通过getUnchecked(key)查找缓存。
5.3 getAll方法 (批量查询)
getAll(Iterable<K>)方法用来执行批量查询的。
默认情况下,对每个不在缓存中的键,getAll方法会单独调用CacheLoader.load来加载缓存项。
如果批量加载比多个单独加载更加高效,你可以重载CacheLoader.loadAll()来利用这一点。
注意:CacheLoader.loadAll 的实现可以为没有明确请求的键加载缓存值。
例如,为某组中的任意键计算值时,能够获取该组中的所有键值,loadAll方法就可以实现为在同一时间获取该组的其他键值。
try {
List<String> list = Lists.newArrayList("100", "103", "110");
ImmutableMap<String, Object> cacheAll = employeeCache.getAll(list);
cacheAll.forEach((k,v)->{
System.out.println("key: "+k+", value: "+v);
});
} catch (ExecutionException e) {
e.printStackTrace();
}
5.4 Callable回调
所有类型的Guava Cache,不管有没有自动加载功能,都支持get(k,Callable<V>)方法。
这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。
在整个加载方法完成前,缓存项相关的可观察状态都不会更改。
这个方法简单实现了缓存模式"如果有缓存直接返回,否则运算、缓存、然后返回"。
try {
String key = "100";
Object value = employeeCache.get(key, new Callable<Employee>() {
@Override
public Employee call() throws Exception {
// 从数据库中取值
return getFromDataBase(key);
}
});
} catch (ExecutionException e) {
e.printStackTrace();
}
六、显式插入 ( cache.put(key,value) )
使用cache.put(key,value)方法可以直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值。
使用Cache.asMap()视图提供的任何方法也能修改缓存。
注意:
asMap()视图的任何方法不能保证缓存项原子的加载到缓存中。
进一步说:asMap()视图的原子运算在Guava Cache的原子加载范畴之外,
所以相比于Cache.asMap.putIfAbsent(K,V),
Cache.get(K,Callable<V>)优先使用。
try {
//显示插入
employeeCache.put("001","若风");
Object value1 = employeeCache.get("001");
//更改asMap视图内容, 同步更改缓存内容
ConcurrentMap<String, Object> asMap = employeeCache.asMap();
asMap.put("002","细雨");
Object value2 = employeeCache.get("002");
} catch (ExecutionException e) {
e.printStackTrace();
}
七、缓存回收
一个残酷的现实是,我们几乎一定没有足够的内存缓存所有数据。
所以Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收、基于引用回收
7.1 基于容量的回收(size-based eviction) 总数量回收、总权重回收
如果要规定缓存项的数目不超过固定值,只需要使用CacheBuilder.maximumSize(long).
缓存将尝试回收最近没有有使用或者总体上很少使用的缓存项。
通常来说,这种情况发生在缓存项的数目逼近限定值时。
另外,不同的缓存项有不同的权重(weights),例如:如果你的缓存值,占据完全不同的内存空间,
你可以使用CacheBuilder.weight(Weight)指定一个权重函数,并且使用CacheBuilder.maximumWeight(long)指定最大总重。
在权重限定场景中,除了要注意回收也是在总权重逼近限定值就进行了,还要知道总权重是在创建时计算的,因此要考虑到权重计算的复杂度。
LoadingCache<String, Employee> employeeCache1 = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterAccess(30, TimeUnit.MINUTES)
.maximumWeight(100)
.weigher(new Weigher<String, Employee>() {
@Override
public int weigh(String key, Employee employee) {
return employee.getDept().length();
}
})
.build(new CacheLoader<String, Employee>() {
@Override
public Employee load(String key) throws Exception {
return getFromDataBase(key);
}
});
7.2 定时回收 (Timed Eviction)
CacheBuilder 提供两种定时回收的方法:
- expireAfterAccess(long,TimeUnit)
- 缓存项在给定的时间内没有被读访问或者写访问,则回收。
- 注意:这种缓存的回收顺序和基于大小回收是一样的。
- expireAfterWrite(long,TimeUnit)
- 缓存项在给定的时间内没有被写访问(创建或者覆盖),则回收。
- 如果认为缓存数据总是在固定时候后边的陈旧不可用,这种回收方式是可取的。
7.3 基于引用的回收(Reference-based Eviction)
通过使用弱引用的键、或者弱引用的值、或者软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:
- CacheBuilder.weakKeys()
- 使用弱引用存储key,当key没有其他(强或者软)引用时,缓存项可以被垃圾回收。
- 因为垃圾回收仅依赖恒等式(==),使用弱引用key的缓存用==而不是equals比较key。
- CacheBuilder.weakValues()
- 使用弱引用存储value,当value没有其他(强或者软)引用时,缓存项可以被垃圾回收。
- 因为垃圾回收仅依赖恒等式(==),使用弱引用value的缓存用==而不是equals比较value。
- CacheBuilder.softValues()
- 使用软引用存储value,软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。
- 考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性质的缓存大小限定(基于容量回收)
- 使用软引用value的缓存一样基于==而不是equals来比较value
八、显式清除缓存
任何时候,你都可以显式的清除缓存项,而不是等到它被回收:
- 个别清除:Cache.invalidate(key)
- 批量清除:Cache.invalidateAll(keys)
- 清除所有缓存项:Cache.invalidateAll()
九、移除监听器
通过 CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便于缓存项被移除时做一些额外操作。
缓存项被移除时,RemovalListener会获取到移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、key、value.
注意: RemovalListener抛出的任何异常都会被记录到日志后丢弃[swallowed]
LoadingCache<String, Object> loadingCache = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterAccess(30, TimeUnit.MINUTES)
.removalListener(new RemovalListener<String, Object>() {
@Override
public void onRemoval(RemovalNotification<String, Object> removalNotification) {
System.out.println("notification: " + removalNotification);
}
})
.build(new CacheLoader<String, Object>() {
@Override
public Object load(String key) throws Exception {
return getFromDataBase(key);
}
});
默认情况下,监听器方法时在移除缓存同步调用的。因为缓存的维护和请求响应通常时同时进行的。
代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。
在这种情况下,你可以使用RemovalListeners.asynchronous(RemovalListener,Executor)把监听器装饰为异步操作。
RemovalListener<String, Object> asyncRemovalListener = RemovalListeners.asynchronous(new RemovalListener<String, Object>() {
@Override
public void onRemoval(RemovalNotification<String, Object> removalNotification) {
}
}, new Executor() {
@Override
public void execute(Runnable command) {
}
});
十、清理什么时候发生?
使用 CacheBuilder 构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,
也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做。
这样做的原因在于:如果要自动的持续的清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。
此外某些环境下线程创建可能受到限制,这样CacheBuilder就不可用了。
相反,Guava把选择权交到你手里。如果你的缓存时高吞吐的,那就无需担心缓存的维护和清理工作了。
如果你的缓存只是偶尔写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程。
以固定的时间间隔调用Cache.cleanUp(), ScheduledExecutorService可以帮助你很好的实现这样的定时调度。
十一、刷新缓存(refresh刷新(可异步)、reload扩展刷新行为、refreshAfterWrite配合expireAfterWrite检索时刷新)
刷新和回收不太一样。正如 LoadingCache.refresh(K) 声明的,刷新表示为键加载新值,这个过程可以是异步的。
在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。
如果刷新过程抛出异常,缓存将会保留旧值,而异常会记录到日志后被丢弃[swallowed]
重载 CacheLoader.reload(K,V) 可以扩展刷新时的行为,这个方法允许开发者在计算新值时使用旧值。
CacheBuilder.refreshAfterWrite(long,TimeUnit)可以为缓存增加自动定时刷新功能让缓存项保持可用。
注意:缓存项只有在被检索时才会真正刷新(如果CacheLoader.refresh实现为异步,那么检索不会被刷新托慢)
所以,如果你在缓存上同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为盲目地定时重置,
如果缓存项没有被检索,那么刷新就不会真的发生,缓存项在过期时间后也变得可以回收。
十二、其他特性
12.1 统计( recordStats()开启统计功能 )
CacheBuilder.recordStats()用来开启Guava Cache的统计功能。
统计功能打开后,Cache.stats()方法会返回CacheStats对象以提供如下统计信息:
- hitRate(): 缓存命中率
- averageLoadPenalty(): 加载新值的平均时间,单位为纳秒
- evictionCount(): 缓存项被回收的总数,不包括显示清除
此外,还有其他很多统计信息,这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。
12.2 asMap视图
asMap 视图提供了缓存的ConcurrentMap形式,但asMap视图与缓存的交互需要注意:
- cache.asMap() 包含当前所有加载到缓存的项。因此相应地,cache.asMap().keySet()包含当前所有已加载的键。
- asMap().get(key) 实质上等同于 cache.getIfPresent(key), 而不会引起缓存项的加载。
- 所有读写操作都会重置相关缓存项的访问时间,包括Cache.asMap().get(Object)和Cache.asMap.put(K,V)方法
但是不包括Cache.asMap().containsKey(Object) 和 Cache.asMap()的集合视图上的操作。
例如:遍历Cache.asMap().entrySet() 不会重置缓存项的读取时间。
12.3 中断(建议使用 AsyncLoadingCache)
缓存加载方法(如 Cache.get )不会抛出中断异常InterruptedException。我们也可以让这些方法支持InterruptedException.
但是这种支持注定是不完备的,并且会增加所有使用者的成本,而只有少数使用者受益。
Cache.get 请求到未缓存的值会遇到两种情况:
- 简单情况:等待另一个正在加载值的线程
- 使用可中断的等待就实现了中断支持。
- 复杂情况:当前线程加载值
- 因为加载值的CacheLoader是用户提供的,如果它是中断的,那我们也可以实现中断,否则无能为力。
如果用户提供的CacheLoader是可中断的,为什么不让Cache.get也支持中断呢?从某种意义上说,其实是支持的。
如果CacheLoader抛出InterruptedException,Cache.get将立即返回(就和其他异常情况一样)
此外,在加载缓存值的线程中,Cache.get捕获到InterruptedException后将恢复中断,
而其他线程中的InterruptedException则被包装成了ExecutionException.
原则上,我们可以拆除包装,把ExecutionException变成InterruptedException,但这会让所有的LoadingCache使用者
都要处理中断异常,即使他们提供的CacheLoader不是可中断的。
如果你考虑到所有非加载线程的等待仍可以被中断,这种做法也许是值得的。
但是许多缓存只是在单线程中使用,他们的用户仍然必须捕获不可能抛出的InterruptedException异常。
即使是那些跨线程共享缓存的用户,也只是有时候能中断他们的get调用,取决于哪个线程先发起请求。
对于这个决定,我们的指导原则是让缓存始终表现得好像是在当前线程加载值。这个原则让使用缓存或者每次都计算值可以简单的相互切换。
如果老代码(加载值的代码)是不可中断的,那么新代码(使用缓存加载值的代码)多半也应该是不可中断的。
如上所述,Guava Cache某种意义上支持中断,另一个意义上来说,Guava Cache不支持中断,
这就使得LoadingCache成了要给有漏洞的抽象:当加载过程被中断了,就当作其他异常一样处理,这在大多数情况下是可以的。
但是如果多个线程等待加载同一个缓存项,即使加载线程中断了,
它也不应该让其他线程都失败(捕获到包装的ExecutionException里面的InterruptedException)。
正确的行为应该是让剩余的线程重试加载。为此,我们记录了一个bug,然而与其冒着风险修复这个bug,
我们可能会花更多的精力取实现另一个建议AsyncLoadingCache,这个实现会返回一个有正确中断行为的Future对象。