Java缓存之瑞士军刀Guava Cahce初探

/ CacheGuavaJava技术 / 没有评论 / 2906浏览

alt

guava ['gwɑ:və]n. 番石榴,番石榴其果实

上图就是番石榴的样子,真是涨知识了。言归正传,下面我们来学习一下Google的Guava Cache

适用场景

GuavaCache与ConcurrentMap的区别

CacheLoader介绍

LoadingCache是附带CacheLoader构建而成的缓存实现。从字面意思理解就是缓存加载器,继承虚类CacheLoader需要重载方法 public abstract V load(K key) throws Exception;该方法实现value的计算逻辑,当获取不到key对应的value值时 GuavaCache会计算一次值并把值缓存到内存中,再次获取如果缓存中存在对应的值就直接获取不在调用费时的计算逻辑,例如:

new CacheLoader<Integer, String>() {
    @Override
    public String load(Integer integer) throws Exception {
        return productValue(integer);
}

final static String productValue(Integer integer) throws InterruptedException {
    String value = "";
    if (integer != null) {
        value += String.valueOf(integer) + System.currentTimeMillis();
    }
    Thread.sleep(1000);
    return value;
}

productValue是一个耗时操作,以一个整数为key,以key+System.currentTimeMillis()字符串为value,这里为了模拟费时操作 使用了Thread.sleep(1000);,第一次获取数据会比较慢,以后只要缓存有效获取效率能大幅提高。 注意:自己定义的CacheLoader如果可能抛出异常使用get,如果会抛出异常则使用getUnchecked

Callable介绍

所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable)方法。这个方法返回缓存中相应的值,或者用给定的Callable 运算并把结果加入到缓存中。这个方法简便地实现了模式”如果有缓存则返回;否则运算、缓存、然后返回”。例如:

Cache<Integer, String> myCache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .build();
try {
    for (int i = 0; i < 10; i++) {
        final Integer a = i;
        System.out.println(myCache.get(i % 5, new Callable<String>() {
            @Override
            public String call() throws Exception {
                return productValue(a);
            }
        }));
    }
} catch (Exception e) {
    e.printStackTrace();
}

显式插入

使用cache.put(key, value)方法可以直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值。使用Cache.asMap()视图提供的任何方法也能修改缓 存。但请注意,asMap视图的任何方法都不能保证缓存项被原子地加载到缓存中。进一步说,asMap视图的原子运算在Guava Cache的原子加载范畴之外, 所以相比于Cache.asMap().putIfAbsent(K,V)Cache.get(K, Callable<V>) 应该总是优先

缓存回收

GuavaCache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。

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

public class MyGuavaSimpleCache {
    public static Cache<Integer, String> MY_CACHE = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .maximumWeight(10000)
            .weigher(new Weigher<Integer, String>() {
                @Override
                public int weigh(Integer key, String value) {
                    return key;
                }
            })
            .build();
}

注意:当有多个缓存回收条件时,可能会先触发其中一个或单个

定时回收(Timed Eviction)

expireAfterWriterefreshAfterWrite异同

refreshAfterWrite的特点是,在refresh的过程中,严格限制只有1个重新加载操作,而其他查询先返回旧值, 这样有效地可以减少等待和锁争用,所以refreshAfterWrite会比expireAfterWrite性能好。但是它也有一个缺点, 因为到达指定时间后,它不能严格保证所有的查询都获取到新值。了解过guava cache的定时失效(或刷新)原来的同学都知道, guava cache并没使用额外的线程去做定时清理和加载的功能,而是依赖于查询请求。在查询的时候去比对上次更新的时间, 如超过指定时间则进行加载或刷新。所以,如果使用refreshAfterWrite,在吞吐量很低的情况下,如很长一段时间内没有查询之后, 发生的查询有可能会得到一个旧值(这个旧值可能来自于很长时间之前),这将会引发问题。

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

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

显式清除

任何时候,你都可以显式地清除缓存项,而不是等到它被回收

移除监听器

public class MyCacheListener implements RemovalListener<Integer, String> {
    @Override
    public void onRemoval(RemovalNotification<Integer, String> notification) {
        System.out.println("key:" + notification.getKey());
        System.out.println("value:" + notification.getValue());
    }
}

警告:默认情况下,监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的,代价高昂的监听器方法在同步模式下会拖慢正常的 缓存请求。在这种情况下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把监听器装饰为异步操作。

清理什么时候发生?

刷新

//有些键不需要刷新,并且我们希望刷新是异步完成的
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相反,refreshAfterWrite通过定 时刷新可以让缓存项保持可用,但请注意:缓存项只有在被检索时才会真正刷新(如果CacheLoader.refresh实现为异步,那么检索不会被刷新拖慢)。 因此,如果你在缓存上同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为刷新盲目地定时重置,如果缓存项没有被检索,那刷新就不会 真的发生,缓存项在过期时间后也变得可以回收。

统计

CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回CacheStats对象以提供如下统计信息