GitHub - google/guava: Google core libraries for Java

JVM缓存

首先是 JVM 缓存,也可以认为是堆缓存。

其实就是创建一些全局变量,如 Map、List 之类的容器用于存放数据。

这样的优势是使用简单但是也有以下问题:

  • 只能显式的写入,清除数据。
  • 不能按照一定的规则淘汰数据,如 LRU,LFU,FIFO 等。
  • 清除数据时的回调通知。
  • 其他一些定制功能等

Ehcache、Guava Cache

出现了一些专门用作 JVM 缓存的开源工具出现了,如 Guava Cache。

它具有上文 JVM 缓存不具有的功能,如自动清除数据、多种清除算法、清除回调等。

但也正因为有了这些功能,这样的缓存必然会多出许多东西需要额外维护,自然也就增加了系统的消耗

Guava Cache

Guava Cache是Google开源的Java重用工具集库Guava里的一款缓存工具,它的设计灵感来源于ConcurrentHashMap**,使用多个segments方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求,同时支持多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等**

Guava Cache有两种缓存加载的方式:CacheLoader 和 Callable,这两种方式都是按照”获取缓存-如果没有-则计算”[get-if-absent-compute]的规则加载的。不同的是,CacheLoader是在创建Cache的时候,实现了一个统一的根据key获取value的方法,而Callable更加灵活,允许你在get的时候指定一个callable来获取value

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()  
            .maximumSize(1000)
            .build(
                new CacheLoader<Key, Graph>() {
                    public Graph load(Key key) throws AnyException {
                        return createExpensiveGraph(key);
                    }
                });
 
...
try {  
    return graphs.get(key);
} catch (ExecutionException e) {
    throw new OtherException(e.getCause());
}
Cache<String, String> cache = CacheBuilder.newBuilder()  
            .maximumSize(1000)
            .build();
 
// 获取某个key时,在Cache.get中单独为其指定load方法
String resultVal = cache.get("hello", new Callable<String>() {  
                        public String call() {
                            String strProValue="hello world!";
                            return strProValue;
                        }
                    });

使用cache.put(key, value)方法可以直接向缓存中插入值,这会直接覆盖掉该key之前缓存的值。使用Cache.asMap()视图提供的任何方法也能相应的修改缓存。但是,asMap视图的任何方法都不能保证缓存项被原子地加载到缓存中

因为我们的数据是缓存在java堆内存上的,存储容量受到堆内存大小限制。当我们缓存的数据量很大时,会影响到GC,所以缓存必须要有回收策略。Guava Cache提供三种回收方式:

基于容量回收

通过CacheBuilder.maximumSize(long)设置缓存项的最大数目,当达到最大数目后,继续添加缓存项,Guava Cache会根据LRU策略回收缓存项来保证不超过最大数目

另外,可以通过CacheBuilder.weigher(Weigher)设置不同缓存项的权重,Guava Cache根据权重来回收缓存项

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()  
        .maximumWeight(100000)
        .weigher(new Weigher<Key, Graph>() {
            public int weigh(Key k, Graph g) {
                return g.vertices().size();
            }
        })
        .build(
            new CacheLoader<Key, Graph>() {
                public Graph load(Key key) { // no checked exception
                    return createExpensiveGraph(key);
                }
            });

定时回收

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

  • expireAfterAccess(long, TimeUnit):缓存项在给定时间范围内没有读/写访问,那么下次访问时,会被回收,然后同步load()(一个线程去load,其他线程等待)。
  • expireAfterWrite(long, TimeUnit):缓存项在给定时间范围内没有写访问,那么下次访问时,会被回收,然后同步load()(一个线程去load,其他线程等待)

Guava Cache不会专门维护一个线程来回收这些过期的缓存项,只有在读/写访问时,才去判断该缓存项是否过期,如果过期,则会回收

基于引用回收

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

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

CacheBuilder如果没有指明,默认是强引用的

keyStrength = builder.getKeyStrength();  
...
Strength getKeyStrength() {  
    return MoreObjects.firstNonNull(keyStrength, Strength.STRONG);
}

显式清除

除了系统回收外,也可以主动清除:

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

刷新

CacheBuilder.refreshAfterWrite(long, TimeUnit)刷新和回收不太一样,刷新表示为key加载新值,这个过程可以是异步的(加载新值的操作默认是同步调load(),异步需要重写CacheLoader.reload())。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成

//有些键不需要刷新,并且我们希望刷新是异步完成的
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()  
    .maximumSize(1000)
    .refreshAfterWrite(1, TimeUnit.MINUTES)
    .build(
        new CacheLoader<Key, Graph>() {
            public Graph load(Key key) { // no checked exception
                return getGraphFromDatabase(key);
            }
 
        public ListenableFuture<Key, Graph> reload(final Key key, Graph prevGraph) {
            if (neverNeedsRefresh(key)) {
                return Futures.immediateFuture(prevGraph);
            }else{
                // asynchronous!
                ListenableFutureTask<Key, Graph> task=
                                ListenableFutureTask.create(new Callable<Key, Graph>() {
                        public Graph call() {
                        return getGraphFromDatabase(key);
                    }
                });
                executor.execute(task);
                return task;
            }
        }
    });

因为刷新动作和回收一样,都是在检索的时候才会触发,所以当你的缓存配置了CacheBuilder.refreshAfterWrite(long, TimeUnit)时,如果部分缓存项很久没有被访问,那么再次被访问时,可能会获得过期很久的数据,这显然是不行的。而单独配置expireAfterWrite(long, TimeUnit)也是有问题的,如果热点数据突然过期,因为同步load()必然会影响读效率

通常我们都是CacheBuilder.refreshAfterWrite(long, TimeUnit)expireAfterWrite(long, TimeUnit) 同时配置,并且刷新的时间间隔要比过期的时间间隔短!这样当较长时间没有被访问的缓存项突然被访问时,会触发过期回收而不是刷新,后面会分析这一块的源码,而热点数据只会触发刷新操作不会触发回收操作

移除监听器

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