最近正在重构公司平台中的一些组件,有一些涉及到缓存的处理。发现Guava的缓存还是非常不错的,所以把CachesExplained翻译了一下,供大家一起参考。
首先,看一下使用范例:
LoadingCache<Key,Graph> graphs =CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10,TimeUnit.MINUTES) .removalListener(MY_LISTENER) .build( newCacheLoader<Key,Graph>(){ publicGraph load(Key key)throwsAnyException{ return createExpensiveGraph(key); } });
适用性
缓存在很多情况下都是非常有用的。比如,我们需要多次根据给定的输入获取值,而且该值计算或者获取的开销是非常昂贵的。
缓存和ConcurrentMap是非常相像的,但是它们也不完全一样。最根本的区别就是,ConcurrentMap会持有所有添加的对象,直到被显示的移除。而缓存为了限制其内存的使用,通常都会配置成可以自动的将对象移除。在某些情况下即使不自动移除对象也是非常有用的,如LoadingCache它会自动加载缓存对象。
一般,Guava缓存适用于以下几种情况:
- 你愿意花费一些内存来换取性能提升;
- 你预测到某些键会多次进行查询;
-
你的缓存数据不超过内存(Guava缓存是单个应用中的本地缓存。它不会将数据存储到文件中,或者外部服务器。如果不适合你,可以考虑一下 Memcached)。
如果你的需要符合上面所说的每一条,那么选择Guava缓存绝对没错。
使用CacheBuilder的构建模式可以获取一个Cache,如上面的范例所示。但是如何进行定制才是比较有趣的。
注意:如果你不需要缓存的这些特性,那么使用ConcurrentHashMap会有更好的内存效率,但是如果想基于旧有的ConcurrentMap复制实现Cache的一些特性,那么可能是非常困难或者根本不可能。
加载
对于缓存首先需要明确的是:有没有一个方法可以通过给定的键来计算/加载相应的值?如果有,那么可以使用CacheLoader。如果没有这样的方法,或者你想复写缓存的加载方式,但你仍想保留“get-if-absent-compute”语义,你可以在调用get方法时传入一个Callable实例,来达到目的。缓存的对象可以通过Cache.put直接插入,但是自动加载是首选,因为自动加载可以更加容易的判断所有缓存信息的一致性。
From a CacheLoader
LoadingCache 缓存是通过一个CacheLoader来构建缓存。创建一个CacheLoader仅需要实现V load(K key) throws Exception方法即可。下面的范例就是如何创建一个LoadingCache:
LoadingCache<Key,Graph> graphs =CacheBuilder.newBuilder() .maximumSize(1000) .build( newCacheLoader<Key,Graph>(){ publicGraph load(Key key)throwsAnyException{ return createExpensiveGraph(key); } }); ... try{ return graphs.get(key); }catch(ExecutionException e){ thrownewOtherException(e.getCause()); }
通过方法get(K)可以对LoadingCache进行查询。该方法要不返回已缓存的值,要不通过CacheLoader来自动加载相应的值到缓存中。这里需要注意的是:CacheLoader可能会抛出Exception,LoaderCache.get(K)则可能会抛出ExecutionException。假如你定义的CacheLoader没有声明检查型异常,那么可以通过调用getUnchecked(K)来获取缓存值;但是一旦当CacheLoader中声明了检查型异常,则不可以调用getUnchecked。
LoadingCache<Key,Graph> graphs =CacheBuilder.newBuilder() .expireAfterAccess(10,TimeUnit.MINUTES) .build( newCacheLoader<Key,Graph>(){ publicGraph load(Key key){// no checked exception return createExpensiveGraph(key); } }); ... return graphs.getUnchecked(key);
批量查询可以使用getAll(Iterable<? extends K>)方法。缺省,getAll方法将循环每一个键调用CacheLoader.load方法获取缓存值。当缓存对象的批量获取比单独获取更有效时,可以通过复写CacheLoader.loadAll方法实现缓存对象的加载。此时当调用getAll(Iterable)方法时性能也会提升。
需要注意的是CacheLoader.loadAll的实现可以为没有明确要求的键加载缓存值。比如,当为某组中的一些键进行计算时,loadAll方法则可能会同时加载组中其余键的值。
From a Callable
所有Guava缓存,不论是否会自动加载,都支持get(K, Callable(V))方法。当给定键的缓存值已存在时则直接返回,否则通过指定的Callable方法进行计算并将值存放到缓存中。直到加载完成时,相应的缓存才会被更改。该方法简单实现了"if cached, return; otherwise create, cache and return"语义。
Cache<Key,Value> cache =CacheBuilder.newBuilder() .maximumSize(1000) .build();// look Ma, no CacheLoader ... try{ // If the key wasn't in the "easy to compute" group, we need to // do things the hard way. cache.get(key,newCallable<Value>(){ @Override publicValue call()throwsAnyException{ return doThingsTheHardWay(key); } }); }catch(ExecutionException e){ thrownewOtherException(e.getCause()); }
直接插入
使用cache.put(key, value)方法可以将值直接插入到缓存中,但这将会覆盖缓存中已存在的值。通过使用Cache.asMap()所导出的ConcurrentMap对象中的方法也可以对缓存进行修改。但是,请注意asMap中的任何方法都不能自动的将数据加载到缓存中。也就是说,asMap中的各方法是在缓存自动加载范围之外来运作。所以,当你使用CacheLoader或Callable来加载缓存时,应该优先使用Cache.get(K, Callable<V>),而不是Cache.asMap().putIfAbsent。
缓存回收
残酷的现实是我们可以肯定的说我们没有足够的内存来缓存一切。你必须来决定:什么时候缓存值不再值得保留?Guava提供了三种基本的缓存回收策略:基于容量回收策略,基于时间回收策略,基于引用回收策略。
基于容量回收策略
使用CacheBuilder.maximumSize(long)可以设置缓存的最大容量。缓存将会尝试回收最近没有使用,或者没有经常使用的缓存项。警告:缓存可能会在容量达到限制之前执行回收,通常是在缓存大小逼近限制大小时。
另外,如果不同的缓存项有不同的“权重”, 如,缓存项有不同的内存占用,此时你需要使用CacheBuilder.weigher(Weigher)指定一个权重计算函数,并使用CacheBuilder.maxmumWeight(long)设定总权重。和maximumSize同样需要注意的是缓存也是在逼近总权重的时候进行回收处理。此外,缓存项的权重是在创建时进行计算,此后不再改变。
LoadingCache<Key,Graph> graphs =CacheBuilder.newBuilder() .maximumWeight(100000) .weigher( newWeigher<Key,Graph>(){ publicint weigh(Key k,Graph g){ return g.vertices().size(); } }) .build( newCacheLoader<Key,Graph>(){ publicGraph load(Key key){// no checked exception return createExpensiveGraph(key); } });
基于时间回收策略
CacheBuilder为基于时间的回收提供了两种方式:
- expireAfterAccess(long, TimeUnit) 当缓存项在指定的时间段内没有被读或写就会被回收。这种回收策略类似于基于容量回收策略;
-
expireAfterWrite(long, TimeUnit) 当缓存项在指定的时间段内没有更新就会被回收。如果我们认为缓存数据在一段时间后数据不再可用,那么可以使用该种策略。
就如下面的讨论,定时过期回收会在写的过程中周期执行,偶尔也会读的过程中执行。
测试定时回收
测试定时回收其实不需要那么痛苦的,我们不必非得花费2秒来测试一个2秒的过期。在构建缓存时使用Ticker接口,并通过CacheBuilder.ticker(Ticker)方法指定时间源,这样我们就不用傻乎乎等系统时钟慢慢的走了。
基于引用回收策略
通过键或缓存值的弱引用(weak references),或者缓存值的软引用(soft references),Guava可以将缓存设置为允许垃圾回收。
- CacheBuilder.weakKeys() 使用弱引用存储键。当没有(强或软)引用到该键时,相应的缓存项将可以被垃圾回收。由于垃圾回收是依赖==进行判断,因此这样会导致整个缓存也会使用==来比较键的相等性,而不是使用equals();
-
CacheBuilder.weakValues() 使用弱引用存储缓存值。当没有(强或软)引用到该缓存项时,将可以被垃圾回收。由于垃圾回收是依赖==进行判断,因此这样会导致整个缓存也会使用==来比较缓存值的相等性,而不是使用equals();
-
CacheBuilder.softValues() 使用软引用存储缓存值。当响应需要时,软引用才会被垃圾回收通过最少使用原则回收掉。由于使用软引用造成性能上的影响,我们强烈建议使用可被预言的maximum cache size的策略来代替。同样使用softValues()缓存值的比较也是使用==,而不是equals()。
显示移除
在任何时候,你都可以可以通过下面的方法显式将无效的缓存移除,而不是被动等待被回收:
- 使用Cache.invalidate(key)单个移除;
- 使用Cache.invalidteAll(keys)批量移除;
-
使用Cache.invalidateAll()移除全部。
移除监听器
可以通过CacheBuilder.removalListener(RemovalListener)注册一个监听器,当缓存数据移除时可以执行一些处理。在RemovalListener中会接受到一个RemovalNotification对象,该对象中包含了RemovalCause(移除原因)、移除的键和移除的缓存值。
需要说明的是RemovalListener中所抛出的异常都会被记录下来(使用Logger),并被吞掉(swallowed,也就是说不会抛出去)。
CacheLoader<Key,DatabaseConnection> loader = newCacheLoader<Key,DatabaseConnection>(){ publicDatabaseConnection load(Key key)throwsException{ return openConnection(key); } }; RemovalListener<Key,DatabaseConnection> removalListener = newRemovalListener<Key,DatabaseConnection>(){ publicvoid onRemoval(RemovalNotification<Key,DatabaseConnection> removal){ DatabaseConnection conn = removal.getValue(); conn.close();// tear down properly } }; returnCacheBuilder.newBuilder() .expireAfterWrite(2,TimeUnit.MINUTES) .removalListener(removalListener) .build(loader);警告:移除监听器默认是同步进行执行的,考虑到缓存的处理也在同步执行,如果移除监听器所执行的处理非常耗时,那么会影响到缓存整体的性能。因此,如果执行一个非常耗时的移除监听器时,可以使用RemovalListeners.asynchronous(RemovalListener, Executor)将监听器包装成异步执行。
Cleanup什么时候会发生?
使用CacheBuilder构建的缓存不会执行清理和自动回收,也不会当缓存过期时马上进行回收,以及诸如此类的事情。取而代之的是,只是会在写过程,或者读操作(很少写的情况)时执行少量的维护处理。
这样做的原因是:如果我们想持续不断的缓存执行维护处理,我们必须为此创建一个线程,而且必须和用户操作竞争共享锁。另外,在有些环境下是不可以创建线程的,这样就会造成CacheBuilder不可用。
相反的,我们将选择权交给你。如果你的缓存是高吞吐量的,那么你可以不用关心缓存的回收过期条目等之类的维护处理。如果你的缓存只有偶尔写,并且不希望清理工作阻碍缓存的读操作,此时你可以创建一个自己的维护线程来定时调用Cache.cleanUp()即可。
如果你的缓存很少写,并且想周期对缓存进行维护,可使用ScheduledExecutorService。
刷新
缓存的刷新和移除还是有些不一样的。在LoadingCache.refresh(K)说明中,刷新会重新加载指定键的值,有可能以异步的方式。在刷新过程中,缓存查询时会返回原来的旧值,但移除是不同的,移除后查询需要等到新值加载完毕后才会返回。
如果刷新过程中抛出了异常,那么缓存会保留原来的旧值,记录该异常并吞掉(即,不会向上抛出)。
更好的方式是通过复写CacheLoader.reload(K, V)来实现刷新功能,这样在计算新值的时候,允许我们仍旧先使用旧值。
// Some keys don't need refreshing, and we want refreshes to be done asynchronously. LoadingCache<Key,Graph> graphs =CacheBuilder.newBuilder() .maximumSize(1000) .refreshAfterWrite(1,TimeUnit.MINUTES) .build( newCacheLoader<Key,Graph>(){ publicGraph load(Key key){// no checked exception return getGraphFromDatabase(key); } publicListenableFuture<Graph> reload(finalKey key,Graph prevGraph){ if(neverNeedsRefresh(key)){ returnFutures.immediateFuture(prevGraph); }else{ // asynchronous! ListenableFutureTask<Graph> task =ListenableFutureTask.create(newCallable<Graph>(){ publicGraph call(){ return getGraphFromDatabase(key); } }); executor.execute(task); return task; } } });通过CacheBuilder.refreshAfterWrite(long, TimeUnit)方法可以实现缓存的自动刷新。相对expiredAfterWrite来说,refreshAfterWrite方法可以让键在一段时间后进行刷新,但是需要注意的是,只有当该条目被访问后才会真正的去刷新(加入CacheLoader.refresh实现为异步的方式,那么你不用担心刷新会影响缓存查询的性能)。因此,你可以在缓存中同时实现refreshAfterWrite和expireAfterWrite,此时过期定时器是不会在缓存项刷新后盲目的重置,也就是说当一个缓存项在刷新后没有被访问过,那么它是允许被过期的。
其它特性
统计
CacheBuilder.recordStats()可以开启Guava缓存的统计信息的收集。Cache.status()方法将返回一个CacheStats对象,该对象提供了下述统计信息:
- hitRate(),提供了请求命中率;
- averageLoadZPenalty(),加载平均耗时,单位为纳秒;
-
evictionCount(),缓存移除总数
还有一些其它的统计信息。这些统计信息对缓存调整非常重要,因为我们建议在缓存要求较高的应用中多关注一下这些数据。
asMap
通过asMap视图你可以像ConcurrentMap来查看Cache,但这里需要解释一下asMap与Cache的交互机制:
- cache.asMap()包含了当前缓存中所加载的所有条目。也就是说,cache.asMap().keySet包含了当前缓存中已经加载的键。
- asMap().get(key)等同于cache.getIfPresent(key),不会调用到加载方法。这一点和Map的语义是一致的。
-
缓存的读写操作(包含了Cache.asMap().get(Object)和Cache.asMap().put(K,V))都会重置缓存的访问时间,但containsKey(Object)方法不会重置访问时间,Cache.asMap()方法也不会。因此,通过cache.entrySet()迭代访问是不会重置缓存条目的访问时间。
中断
加载方法(如:get)是不会抛出InterruptedException的。我们当然在设计时可以让这些方法抛出InterruptedException,但这不是一个完备方案,会迫使所有用户去处理它,但只有部分用户受益。接下来我们会详细进行讨论。
当get方法请求一个尚未缓存的值时,会有两种处理的可能:一个是自己加载这个值,另外就是等待正在加载值的其它线程加载完毕。这就造成我们需要使用不同的方式支持中断。对于等待其它线程加载情况比较简单,我们只需要进入中断等待状态即可。但对于自己加载的这种情况会变得比较棘手。因为,我们提供了用户自定义的CacheLoader,如果碰巧用户自定义的加载支持中断,那好我们可以支持,否则,我们无能为力。
那为何不干脆在CacheLoader中直接支持中断?在某种意义上,我们是支持(请看下面):如果CacheLoader抛出了InterruptedException,那么所有的get调用者将立刻返回(就像遇到其它异常一样)。需要说明的时,加载线程中的get就会进入中断状态。只不过此时InterruptedException被包装成ExecutionException。
原则上,我们是不会将此异常进行解包。这样,会强迫所有使用LoadingCache的用户,即使那些使用不会抛出该异常的CacheLoader实现的用户,也必须处理InterruptedException。当你考虑哪些所有非加载线程需要中断进行等待时,或许还有些价值。但对于那些单线程用户,他们也必须捕捉不可能的InterruptedException。而且,对于需要在多个线程中使用缓存的用户,也只会在碰巧第一个请求的线程才需要处理,并且这种情况是非常少见的。
在这个问题的决定上我们的指导原则是缓存的加载看起来就像是在其调用线程中。这个原则将很容易为那些原来每次调用都需要重新计算的代码中引入缓存。如果原来代码没有中断,那么新的代码也不会,反之也成立。
前面说过在某种意义上是支持中断的。另外一层意思就是,我们不支持,这会使LoadingCache成为一个有漏洞的抽象。如果加载线程并中断,我们把它处理的像其它异常一样。这在大多数情况是没有问题的,但对于有多个等待加载的get就是不对的了。尽管加载线程被中断,而其它线程则是不会。即使其它调用者都会收到一个被包装成ExcutionException的InterruptedException,但是也不应该加载失败啊。正确的方式就是在加载完毕后重新让其它线程重试。我们已经为此记录一个Bug报告。但,修改这个Bug是风险比较高的。因此,我们给出另外一个解决方案,将提供一个AsyncLoadingCache,该类将返回一个包含正确中断行为的Future对象。
相关推荐
在IT行业中,Google Guava库是一个非常强大的工具集,它为Java开发人员提供了一系列实用的集合、缓存、并发和I/O工具。本篇文章将详细探讨如何...在实际项目中,善用Guava缓存可以显著优化应用的响应速度和用户体验。
在这个名为 "Guava-Cache-Demo" 的项目中,我们将深入探讨如何利用 Guava Cache 来实现缓存机制,并通过一个实际示例——使用 Google Books API 获取 ISBN 对应图书详情——来展示其用法。 首先,我们需要理解 ...
在Spring Boot中使用Guava Cache,需要添加Guava依赖,并配置缓存管理器: ```xml <groupId>com.google.guava</groupId> <artifactId>guava <version>30.1-jre @Configuration @EnableCaching public class ...
Guava Cache的设计目标是简化缓存管理,并提供自动过期、统计、线程安全等特性。在《有了Redis缓存就高枕无忧了?可别忘了本地缓存!》这篇博客中,作者详细介绍了如何利用Guava Cache作为本地缓存来补充或增强...
2. **缓存机制**:Guava提供了LruCache(最近最少使用)缓存实现,可以方便地在应用程序中构建高效的缓存系统。 3. **函数式编程**:Guava支持函数式编程,包括Function、Predicate、Transformer等接口,便于编写...
Guava 23还优化了缓存,支持弱键和软引用键,以更好地管理内存资源。此外,还加强了对Java 8的兼容性,包括对Optional的更多支持和对日期时间API的扩展。 总的来说,Guava库随着时间的推移不断演进,每个新版本都...
- **缓存**: `LoadingCache`的性能提升和更丰富的API,使得内存缓存的管理更加灵活。 - **字符串处理**: 提供了更多处理字符串的方法,如`Joiner`和`Splitter`,便于进行复杂的字符串操作。 - **错误处理**: 引入了`...
在Java开发中,Guava库被广泛使用,因为它包含了大量的集合框架、并发支持、缓存机制、字符串处理、I/O操作等多个方面的功能。 标题中的"guava-19.0.jar"是Guava库的19.0版本的二进制文件,包含了编译后的Java类,...
- **缓存**:Guava 提供了一种高效、可配置的缓存实现,使得缓存数据变得简单,支持自动过期和大小限制。 - **函数式编程**:Guava 引入了 Function 和 Predicate 等接口,促进了函数式编程风格在 Java 中的应用。...
通过使用Maven这样的依赖管理工具,开发者可以很方便地将Guava库集成到项目中。在maven项目的pom.xml文件中添加依赖后,就可以在项目中使用Guava提供的所有工具和类库了。 总结来看,Guava库通过提供各种实用工具类...
Guava 的 Cache 接口提供了一种高效的缓存解决方案,可以自动管理数据的存储和过期。通过 LoadingCache,你可以轻松实现自动加载或计算缺失的缓存项。 3. **并发编程支持** Guava 提供了并发工具,如 ...
Guava的缓存功能允许开发者创建和管理本地缓存,从而提高应用程序性能。它支持基于引用的过期策略、大小限制以及预加载和加载函数,可以自定义缓存的行为。例如,你可以设置当某个条目在缓存中停留时间过长或者超过...
Guava Cache支持自动过期、预加载、监听器等特性,使得在Java应用中实现缓存管理变得简单。 一、Guava Cache的基本使用 1. 引入依赖 在项目中引入Guava库,如pom.xml文件中添加如下依赖: ```xml <groupId>...
4. **并发支持**:Guava提供了更高级的并发工具,如RateLimiter(限流器)、Service(服务管理)和ListenableFuture(监听型未来)等,使得在多线程环境中编写高效、安全的代码变得更加简单。 5. **字符串处理**:...
"Spring Cache 复合缓存管理器"指的是通过Spring Cache实现对多种缓存技术的集成和管理,例如 EhCache、Guava Cache 或者 Redis 等。这种复合缓存管理器允许开发者根据需求灵活选择和切换不同的缓存实现,同时提供了...
- 改进了Guava的缓存机制,提供更高效的内存管理和更灵活的配置选项。 - 集合框架的扩展,如Multimap、Table等,提供了更强大的数据结构,方便处理复杂的数据模型。 - 对并发编程的支持,如ListenableFuture、...
2. **缓存机制**:Guava的Cache模块允许开发者创建本地缓存,它可以自动管理资源,如基于引用的过期策略和大小限制。这有助于提高应用程序的性能,减少对远程服务的调用。 3. **并发工具**:Guava提供了强大的并发...
2. **缓存**:Guava的Cache模块允许开发者创建本地缓存,可以自动管理缓存项的过期和大小限制。 3. **函数式编程**:Guava提供了Function接口和其他相关类,支持函数式编程风格,可以方便地进行操作转换。 4. **...
Guava是Google开发的一个核心库,它为Java程序员提供了许多实用功能,如集合、缓存、并发工具、原生类型支持、字符串处理、I/O等。"guava-18.jar"是Guava库的一个特定版本,即版本18。这个JAR文件包含了Guava在18...
3. 统一的缓存管理:Guava Cache提供了统一的缓存管理机制,方便管理和维护缓存。 Guava LoadingCache的缺点 1. 依赖Guava库:Guava Cache依赖Guava库,需要在项目中添加Guava依赖项。 2. 内存占用高:Guava Cache...