guava翻译系列之Cache

简介: guava翻译系列

Guava Cache

在软件开发的过程,缓存是一个非常重要的话题。 在稍微复杂的开发过程中,我们基本上是不可能不使用到缓存的。 至少我们会使用Map去存储一些东西。 这其实就是一个最简单的缓存。 Guava给我们提供了比简单的使用HashMap更强大更灵活的功能,但是和专业的缓存工具相比,(EHCache,Memcached)功能还有些不足, 那么这一章,我们将覆盖Guava cache的下面几个方面:

-- 使用MapMaker类创建ConcurrentMap实例
-- 使用CacheBuilder的链式编程的方式创建LoadingCache和Cache实例
-- 使用CacheBuilderSpec类通过格式化的字符串创建CacheBuilder实例
-- 使用LoadingCache的实例CacheLoader通过指定的key获取对应的值
-- 使用CacheStats类查看Cache的使用状态
-- 使用RemovalListener类接受一个entry从map中删除的事件

下面让我们逐一介绍一下:

MapMaker

MapMaker类在com.google.common.collect包中,我们来看一下怎样使用MapMaker的链式编程快速创建一个ConcurrentHashMap。

ConcurrentMap<String,Book> books = new
MapMaker().concurrencyLevel(2)
.softValues()
.makeMap();

上面的例子中,我们创建了key为String类型,值为Book类型的ConcurrentHashMap. 其中concurrencyLevel()方法是为了指定同时可以用几个线程修改map的值。 softValues() 会将value包装成SoftReference对象,这样发生内存不够使用时,就会被垃圾回收自动回收。 还有其他的一些方法,比如weakKeys() weakValues(), 但是没有softKeys()这是为什么? 还要去看一下java的引用。 当我们使用weakReference或则SoftReference时,无论是key还是value被回收了,map中都会将整个entry移出,这样就不是出现key在value不在,或则value在key不在的情况。

Guava Caches

在我们使用和了解CacheBuilders之前,我们先了解一些关于Cache的背景知识, 在Guava中,我们有两个基本的接口 Cache 和LoadingCache,其中LoadingCache接口继承了Cache接口

Cache

Cache接口提供了基本的key到value的映射。另外也提供了一些比HashMap更好用的接口。 谈到Cache,我们一般的做法是给定一个key,cache会返回这个key对应的value,如果没有找到对应的value,那么将返回NULL. 如果我们想替换一个key/value值,我们可以调用如下方法:

put(key,value);

这里我们将key和value关联到cache或map中。 Cache接口中有传统的put方法,但是也有自己的特有的方法:

V value = cache.get(key, Callable<? Extends V> value);

上面的方法中我们将根据key获取值,如果值在缓存中,就直接返回,如果不在那么就调用Callable获取值,并将获取到的值和对应的key关联,并返回得到的值。 这个方法就完成下面这段代码的功能:

value = cache.get(key);
if(value == null){
value = someService.retrieveValue();
cache.put(key,value);
}

上面的那个方法的使用中,我们需要传入一个Callable对象,但是一般情况下,我们会使用一个匿名的内部类, 这里有个小技巧,如果我们不使用内部类,我们有其他方法吗? 我们可以使用Callables类, 在com.googele.common.util.concurrent包中, Callables中有一个方法可以方便的返回一个值:

Callable<String> value = Callables.returning("Foo");

上面的这条语句中,returning方法会创建返回一个Callable实例,并且当get方法调用时,会返回给定的'Foo'值。 这样的话,我们就可以替换上面例子的实现:

cache.get(key,Callables.returning(someService.retrieveValue());

这里我们要记住,如果值在缓存中,就直接返回, 如果我们期望 存在就返回,不存在就返回null. 那么可以使用getIfPresent(key) 方法。 Guava Cache中也有方法将值失效。
-- invalidate(key):调用这个方法可以将指定的key的值丢弃
-- invalidateAll(): 这个方法丢弃缓存中的所有值
-- invalidateAll(Iterable<?> keys):这个方法丢弃所有迭代中key对应的值

Loading Cache

LoadingCache继承了Cache实例,扩展了自己的方法,看一下下面的例子:

Book book = loadingCache.get(id);

在上面的例子中,如果book对象目前还不在缓存中,LoadingCache可以知道怎样去获取对象,存储对象并返回.

Loading values

LoadingCache的设计是线程安全的,针对同一个key的并发调用是会被block的,一旦value获取到了,这个value值就会返回给调用的get方法,但是如果调用get是多个不同的key那么我们就可以进行并发调用:

ImmutableMap<key,value> map = cache.getAll(Iterable<? Extends
key>);

getAll 返回了一个ImmutableMap 包含了指定的keys 和对应的values。返回的对象,有可能是直接从缓存中取得,有可能是新获取到的,有可能一部分是新获取的一部分是从缓存中取得。

Refreshing values in the cache

LoadingCache 提供了刷新缓存的方法:

refresh(key);

调用loadingcache的refresh方法,loadingcache会重新根据key获取值。 值得注意的地方是: 原来的值会一直等到新值回来才会被替换,在获取新值的过程如果出现异常,原来的值不会被丢弃。

CacheBuilder

CacheBuilder 提供通过建造者方式创建Cache和LoadingCache实例。 Cache实例提供了很多方法。 下面通过例子来感觉一下怎样使用Guava Cache。 第一个例子中我们展示了怎样从Cache中是一个entry失效。

LoadingCache<String,TradeAccount> tradeAccountCache =
CacheBuilder.newBuilder()
.expireAfterWrite(5L, TimeUnit.Minutes)
.maximumSize(5000L)
.removalListener(new
TradeAccountRemovalListener())
.ticker(Ticker.systemTicker())
.build(new CacheLoader<String, TradeAccount>() {
@Override
public TradeAccount load(String key) throws
Exception {
return
tradeAccountService.getTradeAccountById(key);
}
});

其中 TradeAccount的定义如下:

public class TradeAccount {
private String id;
private String owner;
private double balance;
}

下面我们分析一下上面的这个例子:

  1. 首先是expireAfterWrite方法,这个方法会自动在指定时间之后移出对应的entry。 在这个例子中是5分钟。
  2. maximumSize() 指定了缓存的最大的数量。
  3. 我们调用了RemovalListener增加了一个removalListener实例,当一个entry移出后,对调用removalListener方法。
  4. 通过ticker方法增加了一个Ticker实例,提供了纳秒级的服务
  5. 最后调用了build 方法,传入了一个CacheLoader实例,这样当一个key对应的value不在缓存中时,通过这个方法可以获取到对应的value。

下面的例子中,我们将了解基于最后访问时间使得entry失效:

LoadingCache<String,Book> bookCache = CacheBuilder.newBuilder()
.expireAfterAccess(20L,TimeUnit.MINUTES)
.softValues()
.removalListener(new BookRemovalListener())
.build(new CacheLoader<String, Book>() {
@Override
public Book load(String key) throws Exception
{
return bookService.getBookByIsbn(key);
}
});

在这个例子中,我们的做法有点不一样,下面我们来看一下这个例子。

  1. 首先我们调用expireAfterSeconds方法,当一个entry在20分钟没有被访问,就会自动从cache中移出。
  2. 使用jvm的softReference 软引用来代替我们限制cache的大小。 这样当memory不够用时,jvm会自动将entry从cache中移出。 jvm的移出规则是LRU (least-recently-used)算法。
  3. 最后我们加上了RemovalListener 来监听被移出的entry。

下面来看最后一个例子,看样子在guava中怎样自动的刷新缓存。

LoadingCache<String,TradeAccount> tradeAccountCache =
CacheBuilder.newBuilder()
.concurrencyLevel(10)
.refreshAfterWrite(5L,TimeUnit.SECONDS)
.ticker(Ticker.systemTicker())
.build(new CacheLoader<String,
TradeAccount>() {
@Override
public TradeAccount load(String key)
throws Exception {
return
tradeAccountService.getTradeAccountById(key);
}
});

最后一个例子中,我们同样做了一些改变,下面我们解释一下具体的改变:

  1. 首先指定了最多可以并发修改的线程数,我们这里指定了10,如果没有指定默认值是4.
  2. 我们使用在指定时间后自动刷新value的方法替代显示的调用刷新,触发自动更新的条件是这个value被访问并且已经超过指定的时间。
  3. tricker 了纳秒级的触发器
  4. 最后我们传递了一个CacheLoad实例用于获取key对应的value。

CacheBuilderSpec

CacheBuilderSpec 类可以通过指定一个配置的字符串创建一个CacheBuilder实例。 但是这样有个不好的地方,就是我们不能在编译期发现错误,只能在运行期发现错误。 下面是一个用于创建CacheBuilderSpec的字符串:

String configString = "concurrencyLevel=10,refreshAfterWrite=5s"

这个字符串指定了并发更新线程数是10,访问5s后自动刷新,指定时间的方式可以通过(s,m,h,d)分别代表秒,分,时,天。 暂时还没有办法设置毫秒及纳秒级别。 指定了配置的字符串后,我们就可以创建CacheBuilderSpec。 创建方式如下:

CacheBuilderSpec spec = CacheBuilderSpec.parse(configString);

接着我们可以使用CacheBuilder的form方法获取CacheBuilder实例。 完整的例子如下:

String spec =
"concurrencyLevel=10,expireAfterAccess=5m,softValues";
CacheBuilderSpec cacheBuilderSpec =
CacheBuilderSpec.parse(spec);
CacheBuilder cacheBuilder =
CacheBuilder.from(cacheBuilderSpec);
cacheBuilder.ticker(Ticker.systemTicker())
.removalListener(new TradeAccountRemovalListener())
.build(new CacheLoader<String, TradeAccount>() {
@Override
public TradeAccount load(String key) throws
Exception {
return
tradeAccountService.getTradeAccountById(key);
}
});

一般这种指定字符串的方式用在命令行或则配置文件中。

CacheLoader

我们在之前的例子中,已经看到了CacheLoader的使用方法,但是还有一些细节还没覆盖到。 CacheLoader 是一个抽象类,因为里面的load方法是一个抽象方法, 在CacheLoader中还有一个loadAll方法. loadAll方法接受一个Iterable参数,其实内部也是循环调用load方法。 CacheLoader中还有两个比较熟悉的静态方法.

  1. CacheLoader.from(Function func);
  2. CacheLoader.from(Supplier supplier);

其中CacheLoader.from(Function) 接受一个Function 这样输入的key做为Function的input,Function的返回作为返回的Value
CacheLoader.form(supplier) 这个方法其实没什么大的作用

CacheStats

到目前为止,我们已经知道了很多cache的算法,但是我们现在想知道这些cache算法的工作情况. 但是我们要知道收集cache情况会或多或少降低cache的性能。 要监控cache的性能信息,我们只需要在创建cache实例的时候加上recordStats()方法:

LoadingCache<String,TradeAccount> tradeAccountCache =
CacheBuilder.newBuilder()
.recordStats()

我们采用我们比较熟悉的链式编程的方式加上了对cache的监控。 获取监控数据我们只需要要调用stats()方法得到一个CacheStatus实例。 代码示例:

CacheStats cacheStats = cache.stats();

下面是可以从CacheStats中获取到的监控信息:
-- 每次获取新的值的时间
-- 缓存的命中率
-- 缓存失败率
-- 值被剔除的次数

还有很多其他的信息,大家可以看cacheStats的接口文档。

RemovalListener

在CacheBuilder的例子中我们已经看到怎样使用RemovalListener。 就想名字所描述的一样,通知发生在当一个entry没移出时。 和java里面的其他listener接口一样,RemovalListener也有一个onRemoval方法。这个方法接受一个RemovalNotification对象,RemovalListener接受参数化的配置 RemovalListener K和V是我们关心的key的类型和value的类型。 当对应类型的key和对应类型的value被移出时,会接受到对应的通知,如果我们想接受所有的通知,可以将K,V设置为Object。

RemovalNotification

RemovalNotification对象是RemovalListener接受的参数,RemovalNotification对象实现了Map.Entry接口,这样我们就可以获取到被删除的key和value的值,但是值得注意的是我们有可能获取的key或者value的值为空。 因为有可能这些值被jvm回收了。 为了获取被移出的原因我们可以调用getCause()方法。 下面是可能被回收的原因的枚举:

-- COLLECTED: 这个是key或者value被垃圾回收了
-- ECPRED: 这个是这个entry最后一次访问时间或则最后一次写入时间超时了
-- EXPLICIT: 这个表示是用户手动移出
-- REPLACED: 这个表明 这个entry没有被移出,但是 value被重新写过
-- SIZE: 这个表明因为cache的空间不足原因被移出

一般的,如果我们想在enrty被移出的时候 做一写操作,我们最好采取异步的方式、

RemovalListeners

RemovalListeners类是帮助我们异步处理移出后的动作。 使用方式如下:

RemovalListener<String,TradeAccount> myRemovalListener = new
RemovalListener<String, TradeAccount>() {
@Override
public void onRemoval(RemovalNotification<String,
TradeAccount> notification) {
//Do something here
}
};
RemovalListener<String,TradeAccount> removalListener =
RemovalListeners.asynchronous(myRemovalListener,executorService);

这里我们在asynchronous方法中传入了 RemovalListener 和 ExecutorService 参数。 这样就会调用executorService 帮助我们异步的处理 移出事件。 但是要注意的是,这个方法的要在我们调用 CacheBuilder.addRemovalListener之前。

总结

这一章中我们学习了GUAVA的使用方法。 我们学习

  1. MapMaker创建一个ConcurrentMap
  2. 我们学习的Cache和LoadingCache的一些常用方法
  3. 学习了CacheBuilder的一些常用配置
  4. CacheLoader 是LoadingCache的核心组件
  5. CacheStats 统计Cache的性能信息
  6. RemovalListener类 用来接受entry被移出事件
目录
相关文章
|
分布式计算 算法 调度
课3-详解隐私计算框架的架构和技术要点
隐语架构涵盖产品、算法、计算、资源和硬件五层,旨在实现互联互通和跨域管控。产品层包括SecretPad等,简化用户和集成商体验。算法层涉及PSI/PIR、SCQL和联邦学习,提供隐私保护的数据分析和学习。计算层如RayFed、SPU、HEU等,支持分布式计算和密态处理。资源层的KUSCIA用于跨机构任务编排,硬件层涉及FPGA等加速器。互联互通支持黑盒和白盒模式,确保不同平台协作。跨域管控则强调数据流转控制,保护数据权益。
|
Java 应用服务中间件 Spring
【Spring Boot 源码学习】@Conditional 条件注解
【1月更文挑战第8天】本篇介绍 @Conditional 条件注解及其衍生注解
605 3
【Spring Boot 源码学习】@Conditional 条件注解
|
移动开发
钉钉H5微应用配置IP,应用首页地址报错:app url exceeds max length limit,这个怎么处理?
钉钉H5微应用配置IP,应用首页地址报错:app url exceeds max length limit,这个怎么处理?
1841 0
element-ui 里 el-popover 位置发生偏移
element-ui 里 el-popover 位置发生偏移
3010 0
|
缓存 NoSQL Java
什么是缓存?如何在 Spring Boot 中使用缓存框架
什么是缓存?如何在 Spring Boot 中使用缓存框架
1182 0
|
域名解析 网络协议 Linux
localhost 和 127.0.0.1 到底有啥区别?
`localhost` 和 `127.0.0.1` 是开发中常用的本地测试工具,它们虽常互换使用,但存在本质区别。本文深入解析两者定义、特点及差异:`localhost` 为域名,需解析为 IP(如 `127.0.0.1` 或 IPv6 的 `::1`),更灵活;`127.0.0.1` 是固定 IPv4 回环地址,解析更快。文章还探讨了特殊场景下的表现差异,并给出开发中的选择建议,助你更好地理解与应用。
2931 0
|
缓存 NoSQL Java
Spring Boot中的分布式缓存方案
Spring Boot提供了简便的方式来集成和使用分布式缓存。通过Redis和Memcached等缓存方案,可以显著提升应用的性能和扩展性。合理配置和优化缓存策略,可以有效避免常见的缓存问题,保证系统的稳定性和高效运行。
590 3
|
安全 搜索推荐 前端开发
【https】如何让http升级成https
文章详细讲解了如何让HTTP到HTTPS的操作过程
2245 0
【https】如何让http升级成https
|
存储 Java 数据库
Spring Boot中的动态数据源切换
Spring Boot中的动态数据源切换
|
前端开发 JavaScript Java
Springboot框架整合jsp开发【干货满满】
该文介绍了如何在Spring Boot中集成JSP,需包含`spring-boot-starter-web`、`tomcat-embed-jasper`和`jstl`三个依赖。配置Spring Boot寻找JSP的位置,设置`spring.mvc.view.prefix`为`/WEB-INF/jsp/`,`spring.mvc.view.suffix`为`.jsp`。JSP文件应放在`src/main/webapp/WEB-INF/jsp/`下。
1465 0