Guava Cache

Guava Cache

为什么需要cache

一般稍微并发高一点的项目都需要缓存,为什么呢?cachek基于内存,内存的速度是远高于磁盘的。比如redis中间件是基于内存的常被用于缓存中间件,

Guava是Google提供的一个并发工具包,其中的cache模块便是对缓存的一种解决方案

什么是Guava Cache

Guava cache与ConcurrentMap很相似,但也不完全一样。最基本的ConcurrentMap会一直保存你添加的数据直到你手动将它删除调。相对的,Guava Cache为了限制内存的使用,通常会根据设定的删除策略自动回收。

内存的空间是有限的,所以不是所有的场景都要使用cache的,使用场景如下:

  • 你愿意消耗内存来提升效率
  • 你缓存到内存的内容将会被使用一次以上
  • 可以一定程度上容忍数据一致性问题的出现

引入

1
2
3
4
5
<dependency>  
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>

cache初始化

1
2
3
4
5
6
7
8
9
10
final static Cache<Integer, String> cache = CacheBuilder.newBuilder()  
//设置cache的初始大小为10,要合理设置该值
// 数据过多也就失去了缓存的意义
.initialCapacity(10)
//设置并发数为5,即同一时间最多只能有5个线程往cache执行写入操作
.concurrencyLevel(5)
//设置cache中的数据在写入之后的存活时间为10秒
.expireAfterWrite(10, TimeUnit.SECONDS)
//构建cache实例
.build();

常用接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/** 
* 该接口的实现被认为是线程安全的,即可在多线程中调用
* 通过被定义单例使用
*/
public interface Cache<K, V> {

/**
* 通过key获取缓存中的value,若不存在直接返回null
*/
V getIfPresent(Object key);

/**
* 通过key获取缓存中的value,若不存在就通过valueLoader来加载该value
* 整个过程为 "if cached, return; otherwise create, cache and return"
* 注意valueLoader要么返回非null值,要么抛出异常,绝对不能返回null
*/
V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;

/**
* 添加缓存,若key存在,就覆盖旧值
*/
void put(K key, V value);

/**
* 删除该key关联的缓存
*/
void invalidate(Object key);

/**
* 删除所有缓存
*/
void invalidateAll();

/**
* 执行一些维护操作,包括清理缓存
*/
void cleanUp();
}

缓存回收

Guava Cache提供了三种基本的缓存回收方式:

  • 基于容量回收
  • 定时回收
  • 基于引用回收

基于容量回收(size-based eviction)

如果要规定缓存项的数目不超过固定值,只需要使用CacheBuilder.maximumSize(long)缓存将尝试回收最近没有使用或总体上很少使用的缓存项。—> FBI WARNING 在缓存项的数目达到限定值之前, 缓存就可能进行回收。通常来说,这种情况发生在缓存项的数目逼近最大缓存项数目时

定时回收(Timed Eviction)

CacheBuilder提供两种定时回收的方法:

  1. expireAfterAccess(long, TimeUnit): Access这个单词的意思是访问。我们不难猜到这个方法的意思是在一定时间内没有被访问那么他就过期了。缓存项在给定时间内没有被访问(读/写)则被回收。请注意这种缓存的回收顺序和基于大小回收一样

  2. expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候变得陈旧不可用,这种回收方式是可取的。

    定时回收周期性的在写操作中执行,偶尔在读操作中执行

基于引用的回收(Reference-based Eviction)

通过基于弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:

  • CacheBuilder.weakKeys(): 使用弱引用存储键,当没有其他强或软引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖于恒等式(==),使用弱引用键的缓存用 == 而不是equals()比较键
  • CacheBuilder.weakValues(): 使用弱引用存储值。当值没有其他强或软引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖于恒等式,使用弱引用值得缓存用==而不是equals()
  • CacheBuilder.softValues(): 使用软引用存储值。软引用只有响应内存需要时,才按照全局最近最少使用得顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定。使用软引用的缓存同样用==而不是equals比较值。

显示清除

任何时候你都可以显示的清除缓存项,而不是等待它被回收:

  • 个别清除 Cache.invalidate(key)
  • 批量清除 Cache.invalidateAll(keys)
  • 清除所有缓存项 Cache.invalidate()

移除监听器

通过CacheBuilder.removalListener(RemovalListener), 你可以生明一个监听器,以便缓存项被移除时做一些额外的操作。缓存项被移除时,RemovalListener会获取移除通知RemovalNotification,其中包含移除原因RemovalCause,键和值

清理什么时候发生

使用CacheBuilder构建的缓存不会“自动”的执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,他会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做—如果写操作实在太少

这样做的原因在于:如果要自动的持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程的创建可能受限,那么CacheBuilder就不可用了。

相反,我们把选择权交给你,如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理操作。如果你的缓存只是偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建 自己的维护线程,以固定的时间间隔调用cache.cleanUp(). ScheduledExecutorService可以帮助你很好的实现这样的定时调度。

总结

GuavaCache的实现代码中没有启动任何线程,Cache中的所有维护操作,包括清理移除缓存、写入缓存等,都需要外部调用来实现。这在需要低延迟服务场景中使用时需要关注,可能会在某个调用的响应时间突然变大。GuavaCache毕竟是一款面向本地缓存的,轻量级的cache,适合缓存少量数据,如果你想做缓存成千上万数据,可以为每个key设置不同的存活时间,并且高性能,那并不适合使用GuavaCache.


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!