创建LoadingCache
LoadingCache<Object, Object> cache = CacheBuilder.newBuilder()
.build(new CacheLoader<Object, Object>() { //若没有元素,则创建并且放入缓存
@Override
public Object load(Object key) throws Exception {
return key.hashCode();
}
});
初始化大小,最大个数
个数设置
LoadingCache<Object, Object> cache = CacheBuilder.newBuilder()
.initialCapacity(10)//初始化个数
.maximumSize(55)//设置最大个数
.build(new CacheLoader<Object, Object>() { //若没有元素,则创建并且放入缓存
@Override
public Object load(Object key) throws Exception {
return key.hashCode();
}
});
注:初始化如何太小,会导致扩容,比较浪费时间,太大浪费内存.
最大数太大,可能导致内存溢出.
重量设置
LoadingCache<Object, Object> cache = CacheBuilder.newBuilder()
.maximumWeight(1000) //设置重量,配合weigher使用
.weigher(new Weigher<Object, Object>() {
@Override
public int weigh(Object key, Object value) {
return 100;
}
})
.build(new CacheLoader<Object, Object>() { //若没有元素,则创建并且放入缓存
@Override
public Object load(Object key) throws Exception {
return key.hashCode();
}
});
weigher相当一杆秤,称每个元数多重,maximumWeight相当总重量.一般用的较少.
三种时间设置
过期时间
LoadingCache<Object, Object> cache = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.SECONDS) //多长时间未读写后过期
.expireAfterWrite(10, TimeUnit.SECONDS) //多长时间未写后过期
.build(new CacheLoader<Object, Object>() { //若没有元素,则创建并且放入缓存
@Override
public Object load(Object key) throws Exception {
return key.hashCode();
}
});
注:元数过期,guava并不会自动回收,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话。 这样做的原因在于:如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了。
相反,我们把选择权交到你手里。如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的 缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。ScheduledExecutorService可以帮助你很好地实现这样的定时调度。
刷新时间
public static void main(String[] args) throws ExecutionException, InterruptedException {
// guava线程池,用来产生ListenableFuture
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
LoadingCache<Object, Object> cache = CacheBuilder.newBuilder()
//指定时间内没有被创建/覆盖,则指定时间过后,再次访问时,会去刷新该缓存,在新值没有到来之前,始终返回旧值
.refreshAfterWrite(2, TimeUnit.SECONDS)
.build(new CacheLoader<Object, Object>() { //若没有元素,则创建并且放入缓存
@Override
public Object load(Object key) throws Exception {
return System.currentTimeMillis();
}
//重写reload,使其异步刷新数据
@Override
public ListenableFuture<Object> reload(Object key, Object oldValue) throws Exception {
System.out.println("......后台线程池异步刷新:" + key);
return service.submit(new Callable<Object>() { //模拟一个需要耗时2s的数据库查询任务
@Override
public Object call() throws Exception {
System.out.println("begin to mock query db...");
Thread.sleep(2000);
System.out.println("success to mock query db...");
return UUID.randomUUID().toString() + key;
}
});
}
});
}
分析:
- 使用expireAfterAccess或expireAfterWrite时,当缓存过期后,恰好有N个客户端发起请求,需要读取值。使用Guava Cache可以保证只让一个线程去加载数据(比如从数据库中),而其他线程则等待这个线程的返回结果.这样就能避免大量用户请求穿透缓存,但同时也降低了吞吐量.
- refreshAfterWrite: 当缓存数据过期的时候,真正去加载数据的线程会阻塞一段时间,其余线程立马返回过期的值,然这种处理方式更符合实际的使用场景。
- 真正加载数据的那个线程一定会阻塞,我们希望这个加载过程是异步的。这样就可以让所有线程立马返回旧值,在后台刷新缓存数据。refreshAfterWrite默认的刷新是同步的,会在调用者的线程中执行。我们可以改造成异步的,实现CacheLoader.reload()。上面的代码就将其实现.
基于引用的回收–强(strong)、软(soft)、弱(weak)
LoadingCache<Object, Object> cache = CacheBuilder.newBuilder()
.softValues()
.weakKeys()
.weakValues()
.build(new CacheLoader<Object, Object>() { //若没有元素,则创建并且放入缓存
@Override
public Object load(Object key) throws Exception {
return System.currentTimeMillis();
}
});
强(strong)、软(soft)、弱(weak)请参考: Guava—缓存之Reference
监听器
LoadingCache<Object, Object> cache = CacheBuilder.newBuilder()
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(RemovalNotification<Object, Object> notification) {
/**
*RemovalCause 枚举
* 标明是什么情况下 被移除的
*/
RemovalCause cause = notification.getCause();
if (notification.wasEvicted()) { //是否被移除(排除主动删除,和替换)
System.out.println(notification.getKey());
System.out.println(notification.getValue());
}
}
})
.build(new CacheLoader<Object, Object>() { //若没有元素,则创建并且放入缓存
@Override
public Object load(Object key) throws Exception {
return System.currentTimeMillis();
}
});
缓存命中统计
LoadingCache<Object, Object> cache = CacheBuilder.newBuilder()
.recordStats()//统计
.build(new CacheLoader<Object, Object>() { //若没有元素,则创建并且放入缓存
@Override
public Object load(Object key) throws Exception {
return System.currentTimeMillis();
}
});
CacheStats stats = cache.stats(); //不可变对象
stats.hitCount(); //命中次数
stats.hitRate(); //命中概率
stats.missCount();
stats.missRate();
完整参数配置
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
.initialCapacity(10)//初始化个数
.maximumSize(55)//设置最大个数
.maximumWeight(1000) //设置重量,配合weigher使用
.weigher(new Weigher<String, Object>() { //weigher相当一杆秤,称每个元数多重
@Override
public int weigh(String key, Object value) {
return 100;
}
})
.expireAfterAccess(10, TimeUnit.SECONDS) //多长时间未读写后过期
.expireAfterWrite(10, TimeUnit.SECONDS) //多长时间未写后过期
//指定时间内没有被创建/覆盖,则指定时间过后,再次访问时,会去刷新该缓存,在新值没有到来之前,始终返回旧值
.refreshAfterWrite(2, TimeUnit.SECONDS)
.concurrencyLevel(1) //写的并发数
.softValues() //软引用
.weakKeys() //弱引用
.weakValues() //弱引用
.recordStats() //统计的
.removalListener(new RemovalListener<String, Object>() {
@Override
public void onRemoval(RemovalNotification<String, Object> notification) {
/**
*RemovalCause 枚举
* 标明是什么情况下 被移除的
*/
RemovalCause cause = notification.getCause();
if (notification.wasEvicted()) { //是否被移除(排除主动删除,和替换)
System.out.println(notification.getKey() + notification.getValue());
}
}
})
.build(new CacheLoader<Object, Object>() { //若没有元素,则创建并且放入缓存
@Override
public Object load(Object key) throws Exception {
return System.currentTimeMillis();
}
@Override
public ListenableFuture<Object> reload(Object key, Object oldValue) throws Exception {
System.out.println("......后台线程池异步刷新:" + key);
return service.submit(new Callable<Object>() { //模拟一个需要耗时2s的数据库查询任务
@Override
public Object call() throws Exception {
System.out.println("begin to mock query db...");
Thread.sleep(2000);
System.out.println("success to mock query db...");
return UUID.randomUUID().toString() + key;
}
});
}
});
上述算是完整的LoadingCache, 按照实际业务自行配置参数
后续
Spring5放弃掉Guava Cache作为缓存机制,而改用Caffeine作为新的本地Cache的组件。
这个组件目前还没接触到,据说效率很高,待后续…