http://blog.youkuaiyun.com/zero__007/article/details/46756561
示例:
默认情况下,监听器的方法是被移除缓存的那个线程执行的,当然可以使用异步监听器:
这里提及一下,监听器中抛出的任何异常,不会导致监听器执行线程挂掉:
这样做的原因在于:如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了。因此到底是否cleanup或则何时cleanup,需要根据具体的业务决定。
如果缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果缓存只会偶尔有写操作,而又不想清理工作阻碍了读操作,那么可以自定义一个维护线程,以固定的时间间隔调用Cache.cleanUp()。
真正加载数据的那个线程一定会阻塞,但是可以使加载过程是异步的。这样就可以让所有线程立马返回旧值,由其它的线程刷新缓存数据。refreshAfterWrite默认的刷新是同步的,会在调用者的线程中执行。改成异步的方式如下:
缓存统计
使用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);
}
});*/
}
}
});
但是要注意,当缓存没有数据,导致一个线程去加载数据的时候,别的线程也都会阻塞了,因为没有旧值可以返回。所以一般系统启动的时候,需要将数据预先加载到缓存。