Guava Cache
背景
Guava Cache 是 Google 开源的一套开发工具集合,Guava Cache 是其中的一个专门用于处理本地缓存的轻量级框架,是全内存方式的本地缓存,而且是线程安全的。和 ConcurrentMap 相比,Guava Cache 可以限制内存的占用,并可设置缓存的过期时间,可以自动回收数据,而 ConcurrentMap 只能通过静态方式来控制缓存,移除数据元素需要显示的方式来移除。下面通过例子来演示Guava Cache 的使用方式:
使用 Guava Cache 的Maven 以来如下,Guava Cache 是 Google Guava 工具库中的一部分,不能单独应用,所以要把整个 Guava 库都引入进来,例如:
集成
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
缓存存放
在 Guava Cache 中需要通过CacheBuilder创建缓存对象,并通过 CacheLoader 和 Callable 两种方式定义原始数据的获取方式,CacheLoader 是在缓存对象声明时指定,Callable 是在获取数据时指定,相比于 CacheLoader ,Callable 更加灵活,当然也可以通过 cache.put(key,value) 的方式直接插入。
LoadingCache<Long, UserInfo> cache = CacheBuilder.newBuilder()
// 设置缓存个数
.maximumSize(1000).build(new CacheLoader<Long, UserInfo>() {
@Override
public UserInfo load(Long key) throws Exception {
return getUserInfoByUserId(key);
}
});
try {
// 读取缓存
UserInfo user = cache.get(100001L);
} catch (ExecutionException e) {
e.printStackTrace();
doSomethingForException();
}
下面是通过 CacheLoader 使用缓存的例子:
// 定义缓存
Cache<Object, Object> cache = CacheBuilder.newBuilder().maximumSize(1000).build();
try {
// 用户id
final Long userId = 100001L;
UserInfo user = (UserInfo) cache.get(userId, new Callable<UserInfo>() {
@Override
public UserInfo call() throws Exception {
return getUserInfoByUserId(userId);
}
});
} catch (ExecutionException e) {
doSomethingForException();
}
如果 key 为自定义对象,则需要重写 boolean equals(Object obj) 方法,Guava Cache 默认情况(使用弱引用保存key的情况除外,这种情况按照内存地址进行比较)下按照 equals 方法比较key是否相同。
缓存回收:
内存空间不是无限大的,当没有足够的内存情况下,必须要考虑缓存数据回收的问题,移除旧缓存数据为新的缓存数据腾出空间。
Guava Cache 支持四种回收方式,分别是基于容量回收(Size-based Eviction),基于时间回收(Timed Eviction)和基于引用类型的回收(Reference-based Eviction)和手动回收。
基于容量回收(Size-based Eviction)
这种方式可以将缓存数据的数目限制在一个最大值一下,再缓存数据数据接近这个最大值时,Guava Cache 会按照LRU原则回收最近没有使用或者很少使用缓存数据,再开吗中可以通过 CacheBuilder.maximumSize(long) 方法进行设置,但在使用前,你最好预估一下的数据所占用的内存空间,达到最大值时是否有可能造成内存溢出。
基于时间回收(Timed Eviction)
Guava Cache 定义了两种基于时间的缓存回收方式:
CacheBuilder.expireAfterAccess(duration,unit):缓存项在给定时间内没有给访问过(包括读写操作)则会被回收,回收方式按照最近最少使用的原则。代码例如:CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES),表示缓存项在10分钟之内没有读写过,则会被回收。这种方式在实际项目场景中很有用,它可以确保在缓存条目有限的情况下,缓存中保留的都是最近经常使用的热点数据,从而提高缓存的整体命中率。
CacheBuilder.expireAfterWrite (duration, unit),另外一种方式是在缓存项在给定时间内没有被更新(包括创建和覆盖),则可以回收。
基于引用类型的回收(Reference-based Eviction)
Java语言中有强引用,弱引用和软引用的概念,强引用就是最常见的应用方式,被变量赋值就是强引用,只要引用关系存在,应用的对象就不会被垃圾回收。相比于强引用,使用弱引用关联的对象,只要系统进行垃圾回收,弱应用对象就会被回收,而软应用的强度介于弱引用和强引用之间,在内存空间不足,系统内存溢出之前,使用软引用关联的对象才会被回收。
CacheBuilder.weakKeys():按照弱引用的方式存储键值,当没有引用(强应用或者软引用)指向某键值时,允许系统在进行垃圾回收时回收缓存项。因为垃圾回收仅仅依赖于恒等式(
) 来比较对象(对象地址比较),该方法使得缓存在比较键值时用恒等式(
)比较,而不是 equals() 方法。
CacheBuilder.weakValues():和 CacheBuilder.weakKeys() 方法类似,该方法按照弱引用方式来存储缓存项的值,允许系统垃圾回收时回收缓存项。
CacheBuilder.weakValues(),使用软引用方式来包装缓存值,只有在内存需要时(一般在接近内存溢出时),系统会按照全局LRU(least-recently-used)原则进行垃圾回收。考虑到垃圾回收的性能问题,推荐使用基于容量的回收方式。
手动回收方式:
除了上述三种自动回收方式以外,还可以通过如下方法进行
手动缓存回收
- 回收单个缓存项,Cache.invalidate(key)
- 批量回收,Cache.invalidateAll(keys)
- 全部回收Cache.invalidateAll()
实际上,Guava Cache 并不会自动清理缓存,而是在写操作时捎带完成清理工作,如果写操作如果过少的花,也会在部分读操作中进行清理。Guava Cache 之所以不单独启一个线程来实现缓存的自动清理,是为了降低维护成本,避免和用户线程之间产生竞争,而且某些场景下,可能并不能随意创建线程。如果你不想让清理工作影响你的缓存的读写操作,你可以自行维护一个清理线程,周期性调用 cache.cleanUp() 方法定期清理。
运维监控缓存
另外,Guava Cache 还提供了一些运维方法,帮助我们监控缓存运行状使用 CacheBuilder.recordStats()开启统计功能,cache.stats() 返回的 CacheStats 对象来查看运行的统计信息,可以监控好的信息有,主要的统计功能包括:
- hitRate(), 缓存命中率。 hitCount(),缓存命中次数。
- loadCount(),新值加载次数。
- requestCount(),缓存访问次数,是命中次数和非命中次数之和
- averageLoadPenalty(),加载新值时的平均耗时,以纳秒为单位。
- evictionCount(),除了手动清除意外的缓存回收总次数。
除此之外,还有其他很多统计信息,这些信息可以有效帮助我们优化缓存的设置。
并发级别设置:
类似于 ConcurrentHashMap 中的并发级别,Guava Cache 通过改值讲内部数据结构拆分成固定数量的段,缓存数据在段中存储,每个段有自己的写锁,段与段之间互不影响,所以并发级别越大,分的段越多,并发能力越强,如果你的服务QPS比较大的花,可以适当调大并发级别,用法如:
CacheBuilder.newBuilder().concurrencyLevel(20)
完整例子
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
/**
* {@linkplain Object}缓存。
*
* @author liuf *
*/
@Component
public class Cache
{
/** 缓存值的最大数 */
private int maximumSize = 1000;
/** 缓存过期分钟数 */
private int expireAfterAccessMinutes = 60 * 72;
private Cache<String,Object> _cache = null;
public Cache()
{
super();
}
public int getMaximumSize()
{
return maximumSize;
}
public void setMaximumSize(int maximumSize)
{
this.maximumSize = maximumSize;
}
public int getExpireAfterAccessMinutes()
{
return expireAfterAccessMinutes;
}
public void setExpireAfterAccessMinutes(int expireAfterAccessMinutes)
{
this.expireAfterAccessMinutes = expireAfterAccessMinutes;
}
/**
* 初始化。
*/
@PostConstruct
public void init() {
_cache = CacheBuilder.newBuilder().maximumSize(this.maximumSize).expireAfterAccess(this.expireAfterAccessMinutes * 60, TimeUnit.SECONDS).build();
}
/**
* 获取{@linkplain Object}。
*
* @param schemaId
* @param tableName
* @return 返回{@code null}表示没有缓存
*/
public Object get(String key)
{
return this._cache.getIfPresent(key);
}
/**
* 将{@linkplain Object}添加至缓存。
*
* @param key
* @param o
*/
public void put(String key,Object o)
{
this._cache.put(key, o);
}
/**
* 清除指定名称{@linkplain Object}缓存。
*
* @param key
* @param Object
*/
public void invalidate(String key)
{
this._cache.invalidate(key);
}
public static void main(String[] args) {
Cache cache = new Cache();
cache.init();
Object map = new Object();
map.setId("111111");
cache.put("1",map);
System.out.println(cache.get("1"));
}```