CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛

学习使用本地缓存Caffeine-源码交易平台丞旭猿

认识缓存组件Caffeine

Caffeine 是基于Java 8,基于进程内存的的高性能缓存库。

Caffeine的内部实现机制可以参考相关文档,我们现在学习如何使用,其实在仔细阅读Caffeine暴露的API文档后,也能大概猜测出它是怎么实现了。

首先仔细观察Caffeine类的说明,这是一个工厂类,提供以不同构建策略生成Cache容器的方法。

Caffine 类

一个组合了以下特性的Cache、LoadingCache、AsyncCache和AsyncLoadingCache实例的构造器(Builder)。

  • 自动将缓存项(entries)加载到缓存中,视需要异步加载
  • 基于缓存项数量的回收策略
  • 基于缓存项过期的回收策略
  • 当请求一个过期的缓存项时进行异步刷新
  • 键自动封装在弱引用中
  • 值自动封装在弱引用或软引用中
  • 写操作可以广播到外部资源
  • 缓存项被淘汰或被删除时的通知机制
  • 缓存积累访问信息的统计

这些功能都是可选的,可以都使用它们或者都不使用它们来创建缓存。默认情况下,Caffeine创建的缓存实例不会执行任何类型的回收。

应用案例:

LoadingCache<Key,Graph>graphs=Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(Duration.ofMinutes(10)).removalListener((Keykey,Graphgraph,RemovalCausecause)->System.out.printf("Key %s was removed (%s)%n",key,cause)).build(key->createExpensiveGraph(key));

返回的Cache实例被实现为一个哈希表,具有与ConcurrentHashMap相似的性能特征。asMap视图(及其集合视图)具有弱一致的迭代器。这意味着并发使用它们是安全的,但是如果其他线程在迭代器创建之后修改缓存,那么这些修改中有哪些会在迭代器中反映出来(如果有的话)是不确定的。这些迭代器永远不会抛出ConcurrentModificationException。

注意:默认情况下,返回的缓存使用相等比较(equals方法)来确定键或值的相等。但是,如果指定了weakKeys,则缓存对键使用identity(==)比较。同样,如果指定了weakValues或softValues,缓存将对值进行标识比较。

当maximumSize、maximumWeight、expireAfter、expireAfterWrite、expireAfterAccess、weakKeys、weakValues或softvalue被应用时,缓存项在符合条件时将自动地从缓存中删除。

如果maximumSize或maximumWeight被应用,那么在每次缓存修改时,缓存项可能会被清除。

如果expireAfter、expireAfterWrite或expireAfterAccess被应用,那么在每次缓存修改、偶尔的缓存访问或调用Cache.cleanUp时,缓存项可能会被清除。可以指定调度程序(Scheduler)来及时删除过期缓存项,而不是等待Caffeine触发定期维护。过期的缓存项会通过Cache.estimatedSize()计数,但是对读写操作来说是不可见的。

如果使用弱键、弱值或软值,那么缓存中存在的键或值可能会被垃圾收集器回收。带有回收键或值的缓存项可能会在每次缓存修改、偶尔的缓存访问或调用cache . cleanup时从缓存中删除;这样的缓存项可以在Cache.estimatedSize()中计算,但是对读写操作来说是不可见的。

某些缓存配置将导致定期维护任务的积累,这些任务将在写操作期间执行,或者在没有写操作的情况下,偶尔的读操作也可能导致定期维护任务的执行。Cache实例的Cache.cleanUp方法也将执行维护,如果要实现高吞吐量的缓存,就没有必要调用它。只有使用maximumSize、maximumWeight、expireAfter、expireAfterWrite、expireAfterAccess、weakKeys、weakvalue或softvalue构建的缓存才能进行定期维护。

Caffeine生成的缓存是可序列化的,反序列化的缓存保留原始缓存的所有配置属性。请注意,序列化表单不包括缓存内容,只包括配置。

开发者建议:不要依赖缓存过期时间来实现业务逻辑,就像JVM的垃圾回收机制的触发具有不确定一样,Caffeine的回收策略也是复杂且不确定的,可以假定设置了淘汰机制的缓存项将在未来的某一段时间将会过期,但是不能确定究竟是在什么时间点、什么操作发生前被回收,除非主动的操作删除。

Caffeine 方法

newBuilder构造一个新的带有默认设置的Caffeine实例,包括强键、强值,并且不进行任何类型的自动清除。请注意,虽然返回类型是Caffeine,但构建方法上的类型参数允许您创建所需的任何类型键和值的缓存。

initialCapacity设置内部数据结构的最小容量。在构建时提供足够的预加载数量以免之后进行昂贵的调整操作,但是将这个值设置为不必要的大小会浪费内存。

maximumSize指定缓存可以包含的最大缓存项数量。请注意,缓存项可能会在超过此限制之前被删除,或者在删除时临时超过阈值。当缓存大小增长到接近最大值时,缓存会清除不太可能再次使用的条目(根据淘汰算法)。当size为0时,元素被加载到缓存后将立即被清除,这在测试中很有用,或者在不更改代码的情况下临时禁用缓存。此功能不能与maximumWeight一起使用。

expireAfterWrite缓存项在创建后一段时间内若没有被写入,应自动从缓存中删除这个缓存项。如果时间段可以表示为Duration对象,优先使用带Duration参数的重载方法。

expireAfterAccess缓存项在创建后一段时间内若没有被写入或者读取,应自动从缓存中删除这个缓存项。读操作不包括对Cache.asMap的集合视图的操作。如果时间段可以表示为Duration对象,优先使用带Duration参数的重载方法。

refreshAfterWrite缓存项在创建后一段时间内若没有被写入,重新构建(refresh)这个缓存项。构建的算法在LoadingCache中指定。当出现第一个陈旧缓存项的读请求时,将直接返回旧值,然后对CacheLoader进行异步调用重新加载数据。注意:刷新期间抛出的所有异常将被记录下来,然后被吞下。异步加载缓存可以解决缓存穿透等问题。

build构建缓存容器,除非提供映射函数,否则在键请求时不会自动加载值。注意,多个线程可以同时加载不同键的值。如果可行的话,考虑使用build(CacheLoader)重载。此方法不会改变此Caffeine实例的状态,因此可以再次调用它来创建多个独立缓存容器。

removalListener指定一个监听器实例,该监听器实例在每次出于任何原因删除缓存项时接受通知。这个构建器创建的每个缓存都将调用这个监听器。警告:在调用此方法后,不要继续使用此缓存构建器引用,相反,使用该方法返回的引用。为了获得最佳效果,请使用实例代码中的链式表达式习惯用法,在一条语句中配置构造器并构建缓存。警告:任何由监听器抛出的异常都不会传播给缓存用户,只通过日志记录器记录。

writer指定写入器实例,当显式地创建或修改或出于任何原因删除缓存项时,缓存器会通知该写入器实例。当加载或计算缓存项时,不会通知写入者。这个构建器创建的每个缓存都将调用这个写入器,作为修改缓存的原子操作的一部分。警告:在调用此方法后,不要继续使用此缓存构建器引用;相反,使用该方法返回的引用。在运行时,它们指向同一个实例,但是只有返回的引用具有正确的泛型类型信息,从而确保类型安全。

weakKeys指定存储在缓存中的每个键(不是值)应该被包装在WeakReference中(默认情况下,使用强引用)。警告:当使用此方法时,产生的缓存容器将使用identity(==)比较来确定键的相等性。这个特性不能与writer一起使用。

weakValue指定存储在缓存中的每个值(不是键)应该被包装到WeakReference中(默认情况下,使用强引用)。弱值一旦是弱可达对象将被垃圾收集。这使得它们不适合用于缓存;考虑softValues代替。注意:当使用此方法时,产生的缓存将使用identity(==)比较来确定值的相等性。此功能不能与buildAsync一起使用。

Cache 容器接口

Cache

键到值的半持久化映射。使用get(Object, Function) 或 put(Object, Object)手动添加缓存项,并存储在缓存中,直到清除或手动使其失效为止。这个接口的实现应该是线程安全的,并且可以被多个并发线程安全地访问。

LoadingCache

键到值的半持久化映射。值由缓存自动加载,并存储在缓存中,直到删除或手动使其失效为止。这个接口的实现应该是线程安全的,并且可以被多个并发线程安全地访问。

AsyncCache

键到值的半持久化映射。使用get(Object, Function)或put(Object, CompletableFuture)手动添加缓存项,并存储在缓存中,直到清除或手动使其失效为止。这个接口的实现应该是线程安全的,并且可以被多个并发线程安全地访问。

AsyncLoadingCache

键到值的半持久化映射。缓存自动异步加载值,并存储在缓存中,直到删除或手动使其失效为止。这个接口的实现应该是线程安全的,并且可以被多个并发线程安全地访问。

CacheLoader

根据键返回用于填充LoadingCache或AsyncLoadingCache的值。大多数实现只需要实现load方法,可以根据需要重写其他方法。使用示例:

CacheLoader loader = key -> createExpensiveGraph(key); LoadingCache cache = Caffeine.newBuilder().build(loader);

Cache 容器方法

get返回与该缓存中的键相关联的值,必要时从CacheLoader.load(Object)获取该值。如果其他线程调用get获取正在加载中的该键的值,则该线程只能阻塞直到原线程完成并返回其加载的值。注意,多个线程可以同时加载不同键的值。如果指定的键尚未与某个值相关联,则尝试计算该值并将其输入到此缓存中,除非为空。整个方法调用是原子地执行的,因此每个键最多应用函数一次。当load方法正在进行时,其他线程试图对这个缓存进行的一些更新操作可能会被阻塞,因此load方法应该短而简单,并且不能试图更新这个缓存的任何其他映射。

getIfPresent返回与此缓存中的键关联的值,如果没有键的缓存值,则返回null

put将该值与此缓存中的键关联。如果缓存先前包含与键关联的值,则旧值将被新值替换。如果选择使用旁路缓存模式(Cache Aside),推荐使用get(Object, Function) 。

invalidate丢弃键的任何缓存值。如果当前正在加载缓存项,此操作的行为是未定义的。

cleanUp执行任何当前有必要执行的维护操作。具体执行哪些活动——如果有的话——是依赖于实现的。

测试Caffeine

在Spring boot maven项目中,引入依赖:

org.springframework.bootspring-boot-starter-cachecom.github.ben-manes.caffeinecaffeine

根据Caffeine的API,编写测试案例:

packagecom.sunday.mytalk;importcom.github.benmanes.caffeine.cache.*;importorg.checkerframework.checker.nullness.qual.NonNull;importorg.checkerframework.checker.nullness.qual.Nullable;importjava.util.concurrent.TimeUnit;publicclassCacheTest{publicstaticvoidmain(String[]args){Cache<String,String>cache=Caffeine.newBuilder().initialCapacity(0).maximumSize(10).expireAfterWrite(1,TimeUnit.DAYS).writer(newCacheWriter<>(){@Overridepublicvoidwrite(@NonNullObjectkey,@NonNullObjectvalue){System.out.println("write:"+key);}@Overridepublicvoiddelete(@NonNullObjectkey,@NullableObjectvalue,@NonNullRemovalCausecause){System.out.println("delete:"+key);}}).build();normalTest(cache);}publicstaticvoidnormalTest(Cache<String,String>cache){intentryCount=15;intreadCount=entryCount*10;intthreadCount=3;longt=System.currentTimeMillis();//writefor(inti=0;i<entryCount;i++){cache.put("key-"+i,"value-"+i);}//cache.cleanUp();System.out.println("write passing:"+(System.currentTimeMillis()-t));//readwhile(threadCount-->0){newThread(()->{finallongtime=System.currentTimeMillis();intexistCount=0;for(inti=0;i<readCount;i++){intindex=i%entryCount;Stringvalue=cache.getIfPresent("key-"+index);if(value!=null){existCount++;}}System.out.println("read passing:"+(System.currentTimeMillis()-time)+"  existCount:"+existCount);}).start();}}}

运行结果如下:

我们发现,设置了最大缓存数量后,居然不是写入时删除而是读取时删除的,而且读请求按理应该命中100次(因为我们对maximumSize=10的缓存容器按序遍历了10遍),但结果是每个线程命中数都略大于100,这是因为Caffeine是通过定期维护任务来执行清理的,而定期维护任务又是由读写操作触发的。加上cache.cleanUp(),可以看到稳定的命中数,但是不推荐。

增大读写数量,在我的机器上读并发是2000W每秒。

使用Caffeine

CacheTest中演示了Caffeine的使用,但是需要将Cache容器实例设计为全局的资源,才能在程序进程间使用。可以设计单例模式来返回Cache的唯一实例,而在Spring框架下,使用Bean注入更方便。示例代码如下:

@ConfigurationpublicclassCacheConfig{@Bean("coreCache")//通用核心缓存,键不自动加载publicLoadingCache<String,String>coreCache(){returnCaffeine.newBuilder().initialCapacity(10).maximumSize(10000).expireAfterWrite(Duration.ofMinutes(1)).build(key->null);}@Bean("talkContentCache")@Lazy//讲话内容缓存publicCache<String,String>talkContentCache(){returnCaffeine.newBuilder().initialCapacity(1).maximumSize(100).expireAfterWrite(Duration.ofSeconds(1)).build();}}

在@Configuration类中,声明了@Bean注解的方法将返回一个实例,这个实例将视为组件注入到Spring容器中,可以通过@Autowired注解引入这个组件。设置1秒钟的缓存看似很短,但仍将高并发下连续的数据库请求降低为离散的间断性请求,将穿透访问数降低了一个维度。

@RestControllerpublicclassTalkController{privatefinalLoggerLOG=LoggerFactory.getLogger(this.getClass());@AutowiredTalkContenttalkContent;@AutowiredLoadingCache<String,String>coreCache;@AutowiredCache<String,String>talkContentCache;@GetMapping("/hello")publicStringhello(){if(coreCache.get("hello")==null){coreCache.put("hello","hello, current time is:"+System.currentTimeMillis());}returncoreCache.get("hello");}@PostMapping("/talk")publicStringpostTalkContent(Stringcontent){ResultMsgmsg=talkContent.insertTalk(content);LOG.info("receive a talk:"+content);returnmsg.getRtnMsg();}@GetMapping("/talk")publicStringqueryTalkContent(){returntalkContentCache.get("talkContent",s->{List<String>list=talkContent.queryTalks();StringBuilderstringBuilder=newStringBuilder();stringBuilder.append("last updated:"+System.currentTimeMillis()).append("
"
);for(Stringstr:list){stringBuilder.append("").append(str.replace("<","<")).append("").append("
"
);}returnstringBuilder.toString();});}}

测试请求:http://localhost:8080/hello

返回:hello, current time is:1608619932016

返回结果每分钟变化一次,看出缓存确实起到了效果。

进程级缓存只是解决高并发问题的手段之一,各个进程间缓存不共享,分布式情况下还需结合Redis。

声明:本文部分素材转载自互联网,如有侵权立即删除 。

© 版权声明
THE END
喜欢就支持一下吧
点赞0赞赏 分享
相关推荐
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容