暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

guava cache 的基本使用

dragon元 2019-04-01
714

Guava 是开源的 java 工具库,其中包含谷歌正在由他们很多项目使用的很多核心库。这个库是为了方便编码,并减少编码错误。这个库提供用于集合,缓存,支持原语,并发性,常见注解,字符串处理,I/O 和验证的实用方法。


缓存按照进程可以分为本地缓存和非本地缓存,本地缓存会将数据存储在进程的内存当中,本地缓存有 OsCache,GuavaCache。 而非本地缓存则是将数据存储在另一个进程的内存中,非本地缓存有 Redis,MemCache 之类的,非本地缓存会涉及到网络 IO。


本次学习的则是 GuavaCache,本章都是代码示例,学习 API 的调用,源码的话之后会讲解。


现在开始你的第一段 GuavaCache 代码吧。

public static void main(String[] args) {
//创建默认的 cache 对象
Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
//添加缓存
cache.put(1, 2);
//查询存在的缓存
System.out.println(cache.getIfPresent(1));//2
//查询不存在的缓存
System.out.println(cache.getIfPresent(2));//null
}


V get(K var1, Callable<? extends V> var2) 

获取指定缓存,不存在指定缓存则开辟新线程加载数据

public static void main(String[] args) throws ExecutionException {
Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
//添加缓存
cache.put(1, 2);
System.out.println(cache.get(1,() -> 5));//2
System.out.println(cache.get(2,() -> "213"));//213
}

从上面可以看出通过使用 get 方法,我们可以在没有查询到缓存数据的时候主动去加载数据。但是这样我产生了一个疑问,就是通过开辟新线程获取的数据是否会被缓存,如果不被缓存每次都要开辟线程去获取数据,是否会太浪费资源。

抱着疑问来测试一下结果。

public static void main(String[] args) throws ExecutionException {
Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
System.out.println(cache.get(2,new callableDemo()));
System.out.println(cache.get(2,new callableDemo()));
}


static class callableDemo implements Callable {
@Override
public Object call() throws Exception {
System.out.println("获取数据");
return "213";
}
}


控制台打印结果:


获取数据
213
213

通过上面的打印结果,我们可以看出来,新开辟线程获取的数据是会被保存到缓存中的。


void put(K var1, V var2) 

添加数据到缓存中

public static void main(String[] args) throws ExecutionException {
Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
cache.put(1,1);
cache.put(2,2);
cache.put(3,3);
System.out.println(cache.getIfPresent(2));//2
System.out.println(cache.getIfPresent(1));//1
}


V getIfPresent(@CompatibleWith("K") Object var1)

获取缓存中的数据,如果不存在则返回 null

public static void main(String[] args) throws ExecutionException {
Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
cache.put(1,1);
cache.put(2,2);
cache.put(3,3);
System.out.println(cache.getIfPresent(2));//2
System.out.println(cache.getIfPresent(1));//1
System.out.println(cache.getIfPresent(5));//null
}


ConcurrentMap<K, V> asMap();

将缓存中的数据转换为 map

public static void main(String[] args) throws ExecutionException {
Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
cache.put(1,1);
cache.put(2,2);
cache.put(3,3);
//转换为map
ConcurrentMap<Object, Object> map = cache.asMap();
    //并使用 map 的 entrySet 方法
for (Map.Entry<Object, Object> entry : map.entrySet()) {
System.out.print("key:" + entry.getKey() + " - ");
System.out.println("value:" + entry.getValue());
}
}


打印结果:
key:2 - value:2
key:1 - value:1
key:3 - value:3

 

void invalidate(@CompatibleWith("K") Object var1); 

通过 key 清除缓存

public static void main(String[] args) throws ExecutionException {
Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
cache.put(1,1);
cache.put(2,2);
cache.put(3,3);

    System.out.println(cache.getIfPresent(1));//1
System.out.println(cache.getIfPresent(2));//2
System.out.println(cache.getIfPresent(3));//3

cache.invalidate(1);
cache.invalidate(2);
cache.invalidate(3);


System.out.println(cache.getIfPresent(1));//null
System.out.println(cache.getIfPresent(2));//null
System.out.println(cache.getIfPresent(3));//null
}


void invalidateAll(Iterable<?> var1);

清除指定的缓存,可以指定多个 key

public static void main(String[] args) throws ExecutionException {
Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
cache.put(1,1);
cache.put(2,2);
cache.put(3,3);


List<Object> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

//清除指定的 key 集合
cache.invalidateAll(list::iterator);


System.out.println(cache.getIfPresent(1));//null
System.out.println(cache.getIfPresent(2));//null
System.out.println(cache.getIfPresent(3));//null
}


void invalidateAll();

清除所有缓存

public static void main(String[] args) throws ExecutionException {
Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
cache.put(1,1);
cache.put(2,2);
cache.put(3,3);


//清除所有缓存
cache.invalidateAll();


    System.out.println(cache.getIfPresent(1));//null
System.out.println(cache.getIfPresent(2));//null
System.out.println(cache.getIfPresent(3));//null
}


long size();

获取缓存的数量,通过查看源码,可以发现它的统计不是精确的,是个大概值,具体源码下回会解析的

public static void main(String[] args) throws ExecutionException {
Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
cache.put(1,1);
cache.put(2,2);
cache.put(3,3);


System.out.println(cache.size());//3
}


CacheStats stats(); 

获取 cache 的统计功能,要使用此功能的话需要在创建时调用 recordStats() 方法

public static void main(String[] args) throws ExecutionException {
Cache<Object, Object> cache = CacheBuilder.newBuilder()
.recordStats()
.build();
cache.put(1,1);
cache.put(2,2);
cache.put(3,3);


cache.getIfPresent(1);
cache.getIfPresent(2);
cache.getIfPresent(1);
cache.getIfPresent(4);


System.out.println(cache.stats());
}


打印结果:
CacheStats{hitCount=3, missCount=1, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=0}

stats 返回字段的意思:

hitCount:缓存命中率。   

missCount:击穿次数。  

loadSuccessCount:加载成功次数。 

loadExceptionCount:加载异常次数。    

totalLoadTime:总加载时间。  

evictionCount:缓存项被回收的总数,不包括显式清除。  



在上面我们学习的是 guavaCache 的 api,创建的 cache 对象的一些配置也是默认的,所以现在我们可以来学习一些个性化的配置,比如设置缓存数量,缓存大小,过期时间等等。


maximumSize 设置最大缓存数量      

public static void main(String[] args) {
Cache<Object, Object> cache = CacheBuilder.newBuilder()
.maximumSize(2)
.build();
cache.put(1,1);
cache.put(2,2);
cache.put(3,3);


System.out.println(cache.size()); //2
}

通过 size 方法我们获取到缓存得数量为2,看来是设置了最大缓存数量为2在起作用,那么我们可以思考一下,如果达到了最大缓存数,这时新增一个数据进入缓存,那么这个新增得数据到底会不会保存在我们的缓存中呢,这个数据是被抛弃还是替换了原有得缓存呢?我们查看一下 map 中的数据就一目了然了。

public static void main(String[] args) {
Cache<Object, Object> cache = CacheBuilder.newBuilder().maximumSize(2).build();
cache.put(1,1);
cache.put(2,2);
cache.put(3,3);


System.out.println("size:" + cache.size());


for (Map.Entry<Object, Object> entry : cache.asMap().entrySet()) {
System.out.print("key:" + entry.getKey() + " - ");
System.out.println("key:" + entry.getValue());
}
}


打印结果:
size:2
key:3 - key:3
key:2 - key:2

通过上述结果可以看出是被替换了,可以看到最开始加入缓存的被清除了,难道是根据最先添加进入缓存的删除吗?我们可以通过代码来测试一下

public static void main(String[] args) {
Cache<Object, Object> cache = CacheBuilder.newBuilder().maximumSize(2).build();
cache.put(1,1);
cache.put(2,2);


//在添加新数据进缓存前,先查询一下最早加入缓存的数据
cache.getIfPresent(1);


cache.put(3,3);


System.out.println("size:" + cache.size());


for (Map.Entry<Object, Object> entry : cache.asMap().entrySet()) {
System.out.print("key:" + entry.getKey() + " - ");
System.out.println("key:" + entry.getValue());
}
}


打印结果:
size:2
key:3 - key:3
key:1 - key:1

从打印结果来看,guava cache 并不是通过删除最早的缓存数据来增加新缓存的,是通过 LRU 算法(Least recently used,最近最少使用)来实现的,算法根据数据的历史访问记录来进行淘汰数据,其核心思想就是"如果数据被访问过,那么将来被访问的几率也高",本章不详细讲该算法在 guava cache 的实现,下回会详细说。


设置最大重量

weigher(Weigher<? super K1, ? super V1> weigher) //设置每个缓存值得重量     

maximumWeight(1000)//设置最大重量

public static void main(String[] args) {
Cache<Object, Object> cache = CacheBuilder.newBuilder()
.weigher(new Weigher<Object, Object>() {
@Override
public int weigh(Object key, Object value) {
return 300;
}
})
.maximumWeight(1000)
.build();
cache.put(1, 1);
cache.put(2, 2);
cache.put(3, 3);


System.out.println(cache.getIfPresent(1));
System.out.println(cache.getIfPresent(2));
System.out.println(cache.getIfPresent(3));
}


打印结果:
null
null
null

为什么要将两个方法放一起讲呢,因为它们是配合使用的,缺一不可。

打印结果出来之后会不会很诧异呢?明明设置了1000得总重量。而每个缓存得重量才设置为300,怎么一个缓存也添加不进去呢?说起来就是内部得缓存分配算法,因为篇幅限制和需要解析源码,所以会单独写一篇文章,等不及也可以自行查阅资料。


expireAfterAccess 

缓存项在给定时间内没有被读/写访问,则回收

public static void main(String[] args) {
Cache<Object, Object> cache = CacheBuilder.newBuilder()
//设置缓存的最大数量
.maximumSize(100)
//设置10秒钟过期缓存
.expireAfterAccess(5, TimeUnit.SECONDS)
.build();
cache.put(1, 1);
cache.put(2, 2);
cache.put(3, 3);


System.out.println(cache.getIfPresent(1));
System.out.println(cache.getIfPresent(2));
System.out.println(cache.getIfPresent(3));


try {
//睡眠五秒
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}


System.out.println(cache.getIfPresent(1));
System.out.println(cache.getIfPresent(2));
System.out.println(cache.getIfPresent(3));
}


打印结果:
1
2
3
null
null
null

可以看出在过了5秒之后,缓存的数据都被清空了,看到这我不禁有个疑问,是这些数据只能存活在我设置的时间之内吗,通过一段代码来看看是否是这样。

public static void main(String[] args) {
Cache<Object, Object> cache = CacheBuilder.newBuilder()
//设置缓存的最大数量
.maximumSize(100)
//设置5秒钟过期缓存
.expireAfterAccess(5, TimeUnit.SECONDS)
.build();
cache.put(1, 1);
cache.put(2, 2);
cache.put(3, 3);


System.out.println(cache.getIfPresent(1));
System.out.println(cache.getIfPresent(2));
System.out.println(cache.getIfPresent(3));


try {
//睡眠3秒
Thread.sleep(3000);
//获取 2号缓存
System.out.println("在睡眠时间内获取2号缓存:" + cache.getIfPresent(2));
//睡眠3秒
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}


System.out.println(cache.getIfPresent(1));
System.out.println(cache.getIfPresent(2));
System.out.println(cache.getIfPresent(3));
}


打印结果:
1
2
3
在睡眠时间内获取2号缓存:2
null
2
null

通过代码打印结果我们可以知道,存活时间存活在我们设置的时间段之内,只不过当它在生存期间之内被访问时,它的存活时间会被刷新。也就是如果我们设置了存活时间,那么它如果时热点数据,那么它将一直不会被回收。


expireAfterWrite 

缓存项在给定时间内没有被写访问(创建或覆盖),则回收

public static void main(String[] args) {
Cache<Object, Object> cache = CacheBuilder.newBuilder()
//设置缓存的最大数量
.maximumSize(100)
//设置10秒钟过期缓存
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();
cache.put(1, 1);
cache.put(2, 2);
cache.put(3, 3);


System.out.println(cache.getIfPresent(1));
System.out.println(cache.getIfPresent(2));
System.out.println(cache.getIfPresent(3));


try {
//睡眠3秒
Thread.sleep(3000);
//获取 2号缓存
System.out.println("在睡眠时间内写入2号缓存");
cache.put(2,2);
//睡眠3秒
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}


System.out.println(cache.getIfPresent(1));
System.out.println(cache.getIfPresent(2));
System.out.println(cache.getIfPresent(3));
}


打印结果:
1
2
3
在睡眠时间内获取2号缓存:2
null
2
null

在上面的代码中,我们使用了 expireAfterWrite  函数,该函数的效果是:缓存项如果在给定的时间内没有被写入则被回收。通过结果可以看出,没有在给定时间写入的缓存,都过期了。


removalListener(RemovalListener<? super K1, ? super V1> listener)

设置缓存监听,如果有缓存被删除则执行 removalListener 得 onRemoval 方法

public static void main(String[] args) {
Cache<Object, Object> cache = CacheBuilder.newBuilder()
//设置移除监听
.removalListener(new RemovalListenerDemo())
.build();
cache.put(1, 1);
cache.put(2, 2);
cache.put(3, 3);


//删除指定key
cache.invalidate(1);
}

//创建类继承 RemovalListener 并实现 onRemoval 方法
public class RemovalListenerDemo implements RemovalListener<Object,Object> {
@Override
public void onRemoval(RemovalNotification removalNotification) {
System.out.println("缓存发生删除操作,删除缓存 key:" + removalNotification.getKey());
}
}


打印结果:
缓存发生删除操作,删除缓存 key:1


build(CacheLoader<? super K1, V1> loader)

之前得例子都是使用无参得 build 方法。现在来使用一下有参得 build 方法,无参得 build 方法在获取缓存时,需要我们显示的使用 put 方法,将数据加入缓存,而有参的 build 方法则会通过我们传入的 cacheLoder 对象来加载数据并缓存。

public static void main(String[] args) throws ExecutionException {
LoadingCache<Integer, Person> cache = CacheBuilder.newBuilder()
.build(new CreatePersonLoader());

System.out.println("获取key为:" + 1 +" 的缓存,value为 :" + cache.get(1));
System.out.println("获取key为:" + 2 +" 的缓存,value为 :" + cache.get(2));
System.out.println("获取key为:" + 3 +" 的缓存,value为 :" + cache.get(3));
System.out.println("获取key为:" + 3 +" 的缓存,value为 :" + cache.get(3));
}


//继承 CacheLoader 结果,并实现 load 方法,该代码可以改成像数据库取数据
public class CreatePersonLoader extends CacheLoader<Integer, Person> {


int i = 1;


@Override
public Person load(Integer integer) throws Exception {
System.out.println("创建第"+ i +"个人开始");
Person person = new Person();
person.setId((long) i);
person.setAge(new Random().nextInt(124));
person.setName("张" + person.getAge());
i++;
return person;
}
}


//创建实体类
public class Person {


private Long id;


private String name;


private Integer age;


...
}


打印结果:
创建第1个人开始
获取key为:1 的缓存,value为 :Person{id=0, name='张71', age=71}
创建第2个人开始
获取key为:2 的缓存,value为 :Person{id=1, name='张123', age=123}
创建第3个人开始
获取key为:3 的缓存,value为 :Person{id=2, name='张82', age=82}
获取key为:3 的缓存,value为 :Person{id=2, name='张82', age=82}

可以看出来即使我们没有往缓存中先 put 数据,我们也可以获取到缓存值,这是我们实现的 CreatePersonLoader  类在起作用,并且加载的值也加入到缓存中了。      

对了这里会有个小坑要注意一下,就是想要使用自动加载的功能除了实现 CacheLoader 接口,对象类型也必须使用 LoadingCache 对象,不能使用 cache 对象。因为该 get 方法是 LoadingCache 独有的方法。父类对象调用不了子类的独有方法,代码就不演示了,有兴趣可以试试


以上便是 guavaCache 的部分 api 和 对缓存的一些控制,还有一些 api 会和源码解析的时候一起列出来。谢谢观看,有问题欢迎随时指出。

文章转载自dragon元,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论