Guava Cache

http://blog.youkuaiyun.com/zero__007/article/details/46756561

缓存统计

       使用recordStats()打开缓存统计功能:
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().recordStats().maximumSize(3).
        build(new CacheLoader<String, Integer>() {
            @Override
            public Integer load(String key) throws Exception {
                if (key.startsWith("a")) {
                    Thread.sleep(100);
                    throw new RuntimeException();
                } else {
                    Thread.sleep(200);
                    return key.hashCode();
                }
            }
        });
       调用cache的stats()会返回CacheStats,该类的缓存统计值有:
@GwtCompatible
public final class CacheStats {
  private final long hitCount;
  private final long missCount;
  private final long loadSuccessCount;
  private final long loadExceptionCount;
  private final long totalLoadTime;
  private final long evictionCount;
       当访问一个已经存在的key时,hitCount自增;当不存在该key时,会调用CacheLoader# load(),如果成功load,missCount和loadSuccessCount会自增,load()函数的耗时会记录在totalLoadTime(单位:纳秒)中;如果load中抛出异常,missCount和loadExceptionCount会自增,耗时也会记录在totalLoadTime中;evictionCount表示过期后被剔除key的数量。而手动清除缓存数据,或者是直接操作缓存底层数据,是不会影响统计信息。更详细的解释请看源码中注释。
       示例:
System.out.println(cache.stats());
//hitCount=0, missCount=0, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=0

cache.put("zero", 100);
cache.get("zero");
cache.get("zero001");
System.out.println(cache.stats());
//hitCount=1, missCount=1, loadSuccessCount=1, loadExceptionCount=0, totalLoadTime=xxx, evictionCount=0

cache.get("zero001");
try {
    cache.get("azero");
} catch (Exception e) {
}
System.out.println(cache.stats());
//hitCount=2, missCount=2, loadSuccessCount=1, loadExceptionCount=1, totalLoadTime=xxx, evictionCount=0

cache.asMap().get("zero");
System.out.println(cache.stats());
//hitCount=2, missCount=2, loadSuccessCount=1, loadExceptionCount=1, totalLoadTime=xxx, evictionCount=0

cache.invalidateAll();
System.out.println(cache.stats());
//hitCount=2, missCount=2, loadSuccessCount=1, loadExceptionCount=1, totalLoadTime=xxx, evictionCount=0

cache.stats().hitRate();

基于引用的回收策略

       Cache可以指定key或value的引用类型,采用如下方式:CacheBuilder.newBuilder().weakKeys()、CacheBuilder.newBuilder().weakValues()、CacheBuilder.newBuilder().softValues()。可以避免因为数据量多而导致的OOM。

移除监听器RemovalListener

       通过CacheBuilder.newBuilder().removalListener(RemovalListener)可以声明一个监听器,以便缓存项被移除时做一些额外操作。RemovalListener接口如下:
@GwtCompatible
public interface RemovalListener<K, V> {
  void onRemoval(RemovalNotification<K, V> notification);
}
       缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值。
       默认情况下,监听器的方法是被移除缓存的那个线程执行的,当然可以使用异步监听器:
RemovalListener<Object, Object> asyncRemovalListener = RemovalListeners.asynchronous(new RemovalListener(){
    @Override
    public void onRemoval(RemovalNotification notification) {
        //...
    }
}, Executors.newSingleThreadExecutor());
       然后在CacheBuilder.newBuilder().removalListener()中指定asyncRemovalListener即可。
       这里提及一下,监听器中抛出的任何异常,不会导致监听器执行线程挂掉:
Cache<Integer, Integer> cache = CacheBuilder.newBuilder()
        .removalListener(new RemovalListener() {
            @Override
            public void onRemoval(RemovalNotification notification) {
                System.out.println(notification.getCause());
                throw new RuntimeException();
            }
        })
        .build();
cache.put(1, 1);
cache.put(2, 2);

cache.invalidate(1);
cache.invalidate(2);

System.out.println(“over”);
       后面的”over”正常输出。

模拟流逝时间Ticker

       一般缓存都会设置过期时间,如果单元测试代码需要验证这个功能,可以使用Ticker来模拟时间流逝。
private static class TestTicker extends Ticker {
    private long start = Ticker.systemTicker().read();
    private long elapsedNano = 0;

    @Override
    public long read() {
        return start + elapsedNano;
    }

    public void addElapsedTime(long elapsedNano) {
        this.elapsedNano = elapsedNano;
    }
}
TestTicker testTicker = new TestTicker();

Cache<String, String> cache = CacheBuilder.newBuilder()
        .ticker(testTicker)
        .expireAfterAccess(1, TimeUnit.HOURS)
        .build();

cache.put("zero", "007");
System.out.println(cache.getIfPresent("zero")); //007

testTicker.addElapsedTime(TimeUnit.NANOSECONDS.convert(1, TimeUnit.HOURS));
System.out.println(cache.getIfPresent("zero")); //null

cleanUp执行缓存清理操作

       使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写或读操作时顺带做少量的维护工作。定时回收周期性地在写操作中执行,偶尔在读操作中执行。
       这样做的原因在于:如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了。因此到底是否cleanup或则何时cleanup,需要根据具体的业务决定。
       如果缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果缓存只会偶尔有写操作,而又不想清理工作阻碍了读操作,那么可以自定义一个维护线程,以固定的时间间隔调用Cache.cleanUp()。

刷新

       刷新表示为键加载新值,在刷新操作进行时,缓存仍然可以向其他线程返回旧值。
 LoadingCache<String, String> cache = CacheBuilder.newBuilder()
        .refreshAfterWrite (10, TimeUnit.SECONDS)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                //耗时操作
                return key;
            }
        });
       和expireAfterWrite相反,refreshAfterWrite通过定时刷新可以让缓存项保持可用,但缓存项只有在被检索时才会真正刷新(如果CacheLoader.refresh实现为异步,那么检索不会被刷新拖慢)。如果同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为刷新盲目地定时重置,如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收。
       真正加载数据的那个线程一定会阻塞,但是可以使加载过程是异步的。这样就可以让所有线程立马返回旧值,由其它的线程刷新缓存数据。refreshAfterWrite默认的刷新是同步的,会在调用者的线程中执行。改成异步的方式如下:
ExecutorService executor = Executors.newSingleThreadExecutor();
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
        .refreshAfterWrite(10, TimeUnit.SECONDS)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                //耗时操作
                return key;
            }


            @Override
            public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
                if (neverNeedsRefresh) {
                    return Futures.immediateFuture(oldValue);
                } else {
                    // asynchronous
                    ListenableFutureTask<String> task = ListenableFutureTask.create(new Callable<String>() {
                        @Override
                        public String call() throws Exception {
                            return load(key);
                        }
                    });
                    executor.execute(task);
                    return task;
                    
                    /* //或则这种方式
                    ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
                    return service.submit(new Callable<String>() {
                        @Override
                        public String call() throws Exception {
                            return load(key);
                        }
                    });*/
                }
            }
        });
       但是要注意,当缓存没有数据,导致一个线程去加载数据的时候,别的线程也都会阻塞了,因为没有旧值可以返回。所以一般系统启动的时候,需要将数据预先加载到缓存。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值