使用Guava构建本地缓存
一、什么是本地缓存
在我们的应用中,大部分的计算是昂贵的,而且是可复用的,并且计算结果不会经常发生改变。这时候我们就可以将这些昂贵的计算结果缓存到内存中,下次使用的时候直接取出即可,而不用重新计算。这样可以节省大量的cpu和内存资源,提高系统的吞吐量。
本地缓存作用就是提高系统的运行速度,是一种空间换时间的取舍。它实质上是一个做key-value查询的字典,但是相对于我们常用HashMap它又有以下特点:
- 并发性;由于目前的应用大都是多线程的,所以缓存需要支持并发的写入。
- 过期策略;在某些场景中,我们可能会希望缓存的数据有一定“保质期”,过期策略可以固定时间,例如缓存写入10分钟后过期。也可以是相对时间,例如10分钟内未访问则使缓存过期(类似于servlet中的session)。在java中甚至可以使用软引用,弱引用的过期策略。
- 淘汰策略;由于本地缓存是存放在内存中,我们往往需要设置一个容量上限和淘汰策略来防止出现内存溢出的情况。
二、缓存的最大容量与淘汰策略
由于本地缓存是将计算结果缓存到内存中,所以我们往往需要设置一个最大容量来防止出现内存溢出的情况。这个容量可以是缓存对象的数量,也可以是一个具体的内存大小。在Guava中仅支持设置缓存对象的数量。
当缓存数量逼近或大于我们所设置的最大容量时,为了将缓存数量控制在我们所设定的阈值内,就需要丢弃掉一些数据。由于缓存的最大容量恒定,为了提高缓存的命中率,我们需要尽量丢弃那些我们之后不再经常访问的数据,保留那些即将被访问的数据。为了达到以上目的,我们往往会制定一些缓存淘汰策略,常用的缓存淘汰策略有以下几种:
-
FIFO:First In First Out,先进先出。
一般采用队列的方式实现。这种淘汰策略仅仅是保证了缓存数量不超过我们所设置的阈值,而完全没有考虑缓存的命中率。所以在这种策略极少被使用。 -
LRU:Least Recently Used,最近最少使用;
该算法其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
所以该算法是淘汰最后一次使用时间离当前最久的缓存数据,保留最近访问的数据。所以该种算法非常适合缓存“热点数据”。
但是该算法在缓存周期性数据时,就会出现缓存污染,也就是淘汰了即将访问的数据,反而把不常用的数据读取到缓存中。
为了解决这个问题,后续也出现了如LRU-K,Two queues,Multi Queue等进阶算法。 -
LFU:Least Frequently Used,最不经常使用。
该算法的核心思想是“如果数据在以前被访问的次数最多,那么将来被访问的几率就会更高”。所以该算法淘汰的是历史访问次数最少的数据。
一般情况下,LFU效率要优于LRU,且能够避免周期性或者偶发性的操作导致缓存命中率下降的问题。但LFU需要记录数据的历史访问记录,一旦数据访问模式改变,LFU需要更长时间来适用新的访问模式,即:LFU存在历史数据影响将来数据的“缓存污染”效用。
后续出现LFU*,LFU-Aging,Window-LFU等改进算法。
合理的使用淘汰算法能够很明显的提升缓存命中率,但是也不应该一味的追求命中率,而是应在命中率和资源消耗中找到一个平衡。
在guava中默认使用LRU淘汰算法,而且在不修改源码的情况下也不支持自定义淘汰算法,这算是一种小小的遗憾吧。
三、Guava和它的cache
Guava是google开源的一个公共java库,类似于Apache Commons,它提供了集合,反射,缓存,科学计算,xml,io等一些工具类库。
cache只是其中的一个模块。使用Guava cache能够方便快速的构建本地缓存。
四、使用Guava构建第一个缓存
首先需要在maven项目中加入guava依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>
五、Cache与LoadingCache
使用CacheBuilder我们能构建出两种类型的cache,他们分别是Cache与LoadingCache。
- cache
Cache是通过CacheBuilder的build()方法构建,它是Gauva提供的最基本的缓存接口,并且它提供了一些常用的缓存
Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
// 放入或者覆盖一个缓存
cache.put("k1", "v1");
// 获取一个缓存,如果该缓存不存在则返回一个null值
Object value = cache.getIfPresent("k1");
// 获取缓存,当缓存不存在时,则通Callable进行加载并返回。该操作是原子
Object getValue = cache.get("k1", new Callable<Object>() {
@Override
public Object call() throws Exception {
return null;
}
});
- LoadingCache
LoadingCache继承自Cache,在构建LoadingCache时,需要通过CacheBuilder的build(CacheLoader<? super K1, V1> loader)方法构建:
//guava缓存
//声明静态的内存块
//initialCapacity为缓存的初始化容量
// maximumSize缓存的最大容量,当超过这个容量,guava的cache就会使用LRU算法
//expireAfterAccess缓存的有效期为12个小时
private static LoadingCache<String,String> localCache= CacheBuilder.newBuilder().initialCapacity(1000).maximumSize(10000).expireAfterAccess(12, TimeUnit.HOURS).build(new CacheLoader<String, String>() {
@Override
//默认的数据加载实现,当调用get取值的时候,如果key没有对应的值,就调用这个方法进行加载
public String load(String s) throws Exception {
return "null";
}
});
//把数据存入缓存
public static void setKey(String key,String value){
localCache.put(key, value);
}
//通过key取出缓存的value值
public static String getKey(String key){
String value=null;
try{
value=localCache.get(key);
if ("null".equals(value)){
return null;
}
return value;
}catch (Exception e){
logger.error("localCache get error",e);
}
return null;
}
然后可以通过例如以下方法给数据添加进缓存中
//设置UUID
String forgetToken= UUID.randomUUID().toString();
//把forgetToken利用guava放到本地cache中
TokenCache.setKey("token_"+username,forgetToken);
一部分内容摘抄自该博客https://my.oschina.net/u/2270476/blog/1805749,感谢该博主!