详细介绍高性能Java缓存库Caffeine
1、介绍
在本文中,我们来看看Caffeine—一个高性能的Java缓存库。
缓存和Map之间的一个根本区别在于缓存可以回收存储的item。
回收策略为在指定时间删除哪些对象。此策略直接影响缓存的命中率—缓存库的一个重要特征。
Caffeine因使用WindowTinyLfu回收策略,提供了一个近乎最佳的命中率。
2、依赖
我们需要在pom.xml中添加caffeine依赖:
com.github.ben-manes.caffeine caffeine 2.5.5
您可以在MavenCentral上找到最新版本的caffeine。
3、填充缓存
让我们来了解一下Caffeine的三种缓存填充策略:手动、同步加载和异步加载。
首先,我们为要缓存中存储的值类型写一个类:
classDataObject{ privatefinalStringdata; privatestaticintobjectCounter=0; //standardconstructors/getters publicstaticDataObjectget(Stringdata){ objectCounter++; returnnewDataObject(data); } }
3.1、手动填充
在此策略中,我们手动将值放入缓存之后再检索。
让我们初始化缓存:
Cachecache=Caffeine.newBuilder() .expireAfterWrite(1,TimeUnit.MINUTES) .maximumSize(100) .build();
现在,我们可以使用getIfPresent方法从缓存中获取一些值。如果缓存中不存在此值,则此方法将返回null:
Stringkey="A"; DataObjectdataObject=cache.getIfPresent(key); assertNull(dataObject);
我们可以使用put方法手动填充缓存:
cache.put(key,dataObject); dataObject=cache.getIfPresent(key); assertNotNull(dataObject);
我们也可以使用get方法获取值,该方法将一个参数为key的Function作为参数传入。如果缓存中不存在该键,则该函数将用于提供回退值,该值在计算后插入缓存中:
dataObject=cache .get(key,k->DataObject.get("DataforA")); assertNotNull(dataObject); assertEquals("DataforA",dataObject.getData());
get方法可以原子方式执行计算。这意味着您只进行一次计算—即使多个线程同时请求该值。这就是为什么使用get优于getIfPresent。
有时我们需要手动使一些缓存的值失效:
cache.invalidate(key); dataObject=cache.getIfPresent(key); assertNull(dataObject);
3.2、同步加载
这种加载缓存的方法使用了与用于初始化值的Function相似的手动策略的get方法。让我们看看如何使用它。
首先,我们需要初始化缓存:
LoadingCachecache=Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1,TimeUnit.MINUTES) .build(k->DataObject.get("Datafor"+k));
现在我们可以使用get方法检索值:
DataObjectdataObject=cache.get(key); assertNotNull(dataObject); assertEquals("Datafor"+key,dataObject.getData());
我们也可以使用getAll方法获取一组值:
MapdataObjectMap =cache.getAll(Arrays.asList("A","B","C")); assertEquals(3,dataObjectMap.size());
从传递给build方法的底层后端初始化函数检索值。这使得可以使用缓存作为访问值的主要门面(Facade)。
3.3、异步加载
此策略的作用与之前相同,但是以异步方式执行操作,并返回一个包含值的CompletableFuture:
AsyncLoadingCachecache=Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1,TimeUnit.MINUTES) .buildAsync(k->DataObject.get("Datafor"+k));
我们可以以相同的方式使用get和getAll方法,同时考虑到他们返回的是CompletableFuture:
Stringkey="A"; cache.get(key).thenAccept(dataObject->{ assertNotNull(dataObject); assertEquals("Datafor"+key,dataObject.getData()); }); cache.getAll(Arrays.asList("A","B","C")) .thenAccept(dataObjectMap->assertEquals(3,dataObjectMap.size()));
CompletableFuture有许多有用的API,您可以在此文中获取更多内容。
4、值回收
Caffeine有三个值回收策略:基于大小,基于时间和参考。
4.1、基于大小回收
这种回收方式假定当超过配置的缓存大小限制时会发生回收。获取大小有两种方法:缓存中计数对象,或获取权重。
让我们看看如何计算缓存中的对象。当缓存初始化时,其大小等于零:
LoadingCachecache=Caffeine.newBuilder() .maximumSize(1) .build(k->DataObject.get("Datafor"+k)); assertEquals(0,cache.estimatedSize());
当我们添加一个值时,大小明显增加:
cache.get("A"); assertEquals(1,cache.estimatedSize());
我们可以将第二个值添加到缓存中,这导致第一个值被删除:
cache.get("B"); cache.cleanUp(); assertEquals(1,cache.estimatedSize());
值得一提的是,在获取缓存大小之前,我们调用了cleanUp方法。这是因为缓存回收被异步执行,这种方法有助于等待回收的完成。
我们还可以传递一个weigherFunction来获取缓存的大小:
LoadingCachecache=Caffeine.newBuilder() .maximumWeight(10) .weigher((k,v)->5) .build(k->DataObject.get("Datafor"+k)); assertEquals(0,cache.estimatedSize()); cache.get("A"); assertEquals(1,cache.estimatedSize()); cache.get("B"); assertEquals(2,cache.estimatedSize());
当weight超过10时,值将从缓存中删除:
cache.get("C"); cache.cleanUp(); assertEquals(2,cache.estimatedSize());
4.2、基于时间回收
这种回收策略是基于条目的到期时间,有三种类型:
- 访问后到期—从上次读或写发生后,条目即过期。
- 写入后到期—从上次写入发生之后,条目即过期
- 自定义策略—到期时间由Expiry实现独自计算
让我们使用expireAfterAccess方法配置访问后过期策略:
LoadingCachecache=Caffeine.newBuilder() .expireAfterAccess(5,TimeUnit.MINUTES) .build(k->DataObject.get("Datafor"+k));
要配置写入后到期策略,我们使用expireAfterWrite方法:
cache=Caffeine.newBuilder() .expireAfterWrite(10,TimeUnit.SECONDS) .weakKeys() .weakValues() .build(k->DataObject.get("Datafor"+k));
要初始化自定义策略,我们需要实现Expiry接口:
cache=Caffeine.newBuilder().expireAfter(newExpiry(){ @Override publiclongexpireAfterCreate( Stringkey,DataObjectvalue,longcurrentTime){ returnvalue.getData().length()*1000; } @Override publiclongexpireAfterUpdate( Stringkey,DataObjectvalue,longcurrentTime,longcurrentDuration){ returncurrentDuration; } @Override publiclongexpireAfterRead( Stringkey,DataObjectvalue,longcurrentTime,longcurrentDuration){ returncurrentDuration; } }).build(k->DataObject.get("Datafor"+k));
4.3、基于引用回收
我们可以将缓存配置为启用缓存键值的垃圾回收。为此,我们将key和value配置为弱引用,并且我们可以仅配置软引用以进行垃圾回收。
当没有任何对对象的强引用时,使用WeakRefence可以启用对象的垃圾收回收。SoftReference允许对象根据JVM的全局最近最少使用(Least-Recently-Used)的策略进行垃圾回收。有关Java引用的更多详细信息,请参见此处。
我们应该使用Caffeine.weakKeys()、Caffeine.weakValues()和Caffeine.softValues()来启用每个选项:
LoadingCachecache=Caffeine.newBuilder() .expireAfterWrite(10,TimeUnit.SECONDS) .weakKeys() .weakValues() .build(k->DataObject.get("Datafor"+k)); cache=Caffeine.newBuilder() .expireAfterWrite(10,TimeUnit.SECONDS) .softValues() .build(k->DataObject.get("Datafor"+k));
5、刷新
可以将缓存配置为在定义的时间段后自动刷新条目。让我们看看如何使用refreshAfterWrite方法:
Caffeine.newBuilder() .refreshAfterWrite(1,TimeUnit.MINUTES) .build(k->DataObject.get("Datafor"+k));
这里我们应该要明白expireAfter和refreshAfter之间的区别。当请求过期条目时,执行将发生阻塞,直到buildFunction计算出新值为止。
但是,如果条目可以刷新,则缓存将返回一个旧值,并异步重新加载该值。
6、统计
Caffeine有一种记录缓存使用情况的统计方式:
LoadingCachecache=Caffeine.newBuilder() .maximumSize(100) .recordStats() .build(k->DataObject.get("Datafor"+k)); cache.get("A"); cache.get("A"); assertEquals(1,cache.stats().hitCount()); assertEquals(1,cache.stats().missCount());
我们也可能会传入recordStatssupplier,创建一个StatsCounter的实现。每次与统计相关的更改将推送此对象。
7、结论
在本文中,我们熟悉了Java的Caffeine缓存库。我们看到了如何配置和填充缓存,以及如何根据我们的需要选择适当的到期或刷新策略。
文中示例的源代码可以在Github上找到。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。