概述
缓存的目的是使用空间来换时间,比较常见的使用场景是,将经过复杂计算(或者跨网络socket调用)的结果缓存到本地,以便下次使用时直接利用上次结果,对于读多写少的数据,缓存可以有效的减少请求处理的端到端延迟,以及对后端的基础资源有效的保护。
分布式缓存,一般选择redis等分布式缓存组件来实现,而本地内存缓存涉及到线程安全、缓存失效时间控制等问题。对于本地缓存可以使用Guava Cache,Guava Cache是一款优秀的内存缓存组件。
maven依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
使用示例
创建缓存
LoadingCache<Object, Object> cache = CacheBuilder.newBuilder()
.build(new CacheLoader<Object, Object>() { //若没有元素,则创建并且放入缓存
@Override
public Object load(Object key) throws Exception {
return key.hashCode();
}
});
具体示例:
private LoadingCache<String, CResp> deviceCache = CacheBuilder.newBuilder().expireAfterWrite(dTimeout, TimeUnit.MINUTES).maximumSize(5000)
.build(
new CacheLoader<String, CResp>() {
@Override
public CResp load(String key) throws Exception {
CResp cResp = cService.getInfo(key);
return cResp;
}
});
初始化大小、最大个数
个数设置
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可以帮助你很好地实现这样的定时调度。
回收策略
expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读、写访问,则回收。
expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。该种方式主要适用于无法保证本地缓存和依赖的数据的一致性的情况下,强制只缓存一段时间。
我们也可以显式地清除缓存项,而不是等到它被回收:
个别清除:invalidate(key)
批量清除:invalidateAll(keys)
清除所有缓存项:invalidateAll()
刷新时间
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, 按照实际业务自行配置参数。
参考
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/100244.html