Java缓存的使用前面已经介绍过了。对于我们来说,如果有人总结出一些缓存使用模式/模板,我们在使用的时候直接按照模式来写就可以了。其实已经有总结出来的模型,主要分为两类:Cache-Aside和Cache-As-SoR(Read-through、Write-through、Write-behind)。首先,同步两个名词。SoR(system-of-record):记录系统,也可以称为数据源,即实际存储原始数据的系统。Cache:Cache是??SoR的快照数据。Cache的访问速度比SoR快。放在Cache中的目的是提高访问速度,减少回源到SoR的次数。回源:即回到数据的源头获取数据。当Cache不活跃时,需要从SoR中读取数据。这叫回源。本文主要以GuavaCache和Ehcache3.x作为实用框架进行讲解。1、Cache-AsideCache-Aside是指业务代码围绕Cache编写,由业务代码直接维护缓存。示例代码如下。读取场景,先从缓存中获取数据,如果没有***,则回源给SoR,将源数据放入缓存中,供下次读取。//1。先从缓存中获取数据value=myCache.getIfPresent(key);if(value==null){//2.1。如果缓存中没有***,则返回SoR获取源数据value=loadFromSoR(key);//2.2、将数据放入缓存中,下次myCache时可以从缓存中获取数据.put(key,value);}写场景,先将数据写入SoR,写入成功后立即同步数据写入缓存。//1。先将数据写入SoRwriteToSoR(key,value);//2.执行成功后立即同步写入缓存myCache.put(key,value);或者先把数据写入SoR,然后写入缓存数据过期,下次读取时再加载缓存。//1。先将数据写入SoRwriteToSoR(key,value);//2.使缓存失效,下次读取时再加载缓存myCache.invalidate(key);Cache-Aside适合AOP方式实现,可以参考作者博客《Spring Cache抽象详解》实现。对于Cache-Aside,可能存在并发更新,即如果多个应用实例同时更新,那么缓存该怎么办呢?如果是用户维度的数据(比如订单数据、用户数据),出现这种情况的几率很小,因为并发的情况很少,可以忽略这个问题,可以加上过期时间来解决。●对于商品等基础数据,可以考虑使用canal订阅binlog增量更新分布式缓存,这样不会出现缓存数据不一致的情况,但是缓存更新会有延迟。本地缓存根据不一致容忍度设置合理的过期时间。●对于读服务场景,可以考虑使用一致性哈希将相同操作的负载均衡到同一个实例上,从而降低并发几率。或者设置一个比较短的过期时间,请参考《第十七章京东商品详情页服务闭环实践》。2.Cache-As-SoRCache-As-SoR把Cache当成SoR,所有的操作都在Cache上进行,然后Cache委托SoR进行真正的读写。即业务代码中只能看到Cache的操作,看不到SoR相关的代码。有三种实现方式:read-through、write-through、write-behind。1.Read-ThroughRead-Through,业务代码先调用Cache,如果不能保证Cache,则Cache返回source给SoR而不是业务代码(即Cache读取SoR)。要使用Read-Through模式,您需要配置一个CacheLoader组件以将源数据加载回SoR。GuavaCache和Ehcache3.x都支持这种模式。GuavaCache实现LoadingCache>getCache=CacheBuilder.newBuilder().softValues().maximumSize(5000).expireAfterWrite(2,TimeUnit.MINUTES).build(newCacheLoader>(){@OverridepublicResultload(finalIntegersortId)throwsException{returncategoryService.get(sortId);}});在构建Cache时,传入一个CacheLoader来加载缓存。操作过程如下。应用业务代码直接调用getCache.get(sortId)。先查询Cache,如果缓存中有,则直接返回缓存数据。如果缓存中没有缓存,则委托给CacheLoader,CacheLoader返回SoR查询源数据(返回值不能为null,可以包装成Null对象),然后写入进入缓存。使用CacheLoader后有几个好处。●应用业务代码更简洁,无需像Cache-Aside模式那样将缓存查询代码和SoR代码交织在一起。如果缓存使用逻辑分散在多个地方,使用这种方法可以轻松消除重复代码。●解决Dog-pile效应,即当某个缓存失效时,有大量相同的请求没有被缓存,导致请求同时发送到后端,导致对后端的压力过大后端。此时只能限制一个请求。.if(firstCreateNewEntry){//第一个请求加载缓存的线程去SoR加载源数据1);}}else{//其他并发线程等待“***线程”加载的数据returnwaitForLoadingValue(e,key,valueReference);}GuavaCache也支持get(Kkey,CallablevalueLoader)方法,通过在一个Callable中,例如,当缓存为空时,会调用Callable#call来查询SoR以加载源数据。Ehcache3.x实现CacheManagercacheManager=CacheManagerBuilder.newCacheManagerBuilder().build(true);org.ehcache.CachemyCache=cacheManager.createCache("myCache",CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class,String.class,ResourcePoolsBuilder.newResourcePoolsBuilder().heap(100,MemoryUnit.MB)).withDispatcherConcurrency(4).withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS))).withLoaderWriter(newDefaultCacheLoaderWriter(){@OverridepublicStringload(Stringkey)throwsException{returnreadDB(key);}@OverridepublicMaploadAll(Iterablekeys)throwsBulkCacheLoadingException,Exception{returnull;}}));Ehcache3.x使用CacheLoaderWriter实现,通过load(Kkey)和loadAll(Iterablekeys)分别加载单个KEY和批量KEY。Ehcache3.1本身并没有解决Dog-pile的问题。2.Write-ThroughWrite-Through简称直写模式/write-through模式。业务代码首先调用Cache写入(添加/修改)数据,然后由Cache代替业务代码负责写入缓存和SoR。使用Write-Through模式需要配置一个CacheWriter组件来回写到SoR。GuavaCache不提供支持。Ehcache3.x支持这种模式。Ehcache需要配置一个CacheLoaderWriter,CacheLoaderWriter知道怎么写SoR。当Cache需要写入(添加/修改)数据时,首先调用CacheLoaderWriter同步(立即)到SoR,成功后更新缓存。CacheManagercacheManager=CacheManagerBuilder.newCacheManagerBuilder().build(true);org.ehcache.CachemyCache=cacheManager.createCache("myCache",CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class,String.class,ResourcePoolsBuilder.newResourcePoolsBuilder().heap(100,MemoryUnit.MB)).withDispatcherConcurrency(4).withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS))).withLoaderWriter(newDefaultCacheLoaderWriter(){@Overridepublicvoidwrite(Stringkey,Stringvalue)throwsException{//write}@OverridepublicvoidwriteAll(Iterable>entries)throwsBulkCacheWritingException,Exception{for(Objectentry:entries){//批量写入}}@Overridepublicvoiddelete(Stringkey)throwsException{//delete}@OverridepublicvoiddeleteAll(Iterablekeys)throwsBulkCacheWritingException,Exception{for(Objectkey:keys){//批量删除}}})。建造());Ehcache3.x仍然使用CacheLoaderWriter来实现,通过write(Stringkey,Stringvalue),writeAll(Iterable>entries)和delete(Stringkey),deleteAll(Iterablekeys)分别支持单写,批量写,单删除,和批量删除操作。操作流程如下。当我们调用myCache.put("e","123")或myCache.putAll(map)时,缓存被写入。首先Cache会立即将写操作委托给CacheLoaderWriter#write和#writeAll,然后CacheLoaderWriter立即负责写入SoR。SoR写入成功后,写入Cache。3.Write-BehindWrite-Behind,也叫Write-Back,简称回写方式。与Write-Through同步写入SoR和Cache不同,Write-Behind异步写入。异步后,可以实现批量写入、合并写入、延时和限流。(1)异常写入CacheManagercacheManager=CacheManagerBuilder.newCacheManagerBuilder().using(PooledExecutionServiceConfigurationBuilder.newPooledExecutionServiceConfigurationBuilder().pool("writeBehindPool",1,5).build()).build(true);org.ehcache.CachemyCache=cacheManager.createCache("myCache",CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class,String.class,ResourcePoolsBuilder.newResourcePoolsBuilder().heap(100,MemoryUnit.MB)).withDispatcherConcurrency(4).withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS))).withLoaderWriter(newDefaultCacheLoaderWriter(){@Overridepublicvoidwrite(Stringkey,Stringvalue)throwsException{//write}@Overridepublicvoiddelete(Stringkey)throwsException{//delete}}).add(WriteBehindConfigurationBuilder.newUnBatchedWriteBehindConfiguration().queueSize(5).concurrencyLevel(2.useThreadPool("writeBehindPool").build()));几个重要配置如下。hreadPool:使用PooledExecutionServiceConfigurationBuilder配置线程池;然后WriteBehindConfigurationBuilder通过useThreadPool配置使用哪个线程池;WriteBehindConfigurationBuilder:配置WriteBehind策略;CacheLoaderWriter:配置WriteBehind如何操作SoR。WriteBehindConfigurationBuilder将执行以下配置。newUnBatchedWriteBehindConfiguration:表示不进行批处理,那么所有的批操作都会转化为单次操作,即CacheLoaderWriter只需要实现write和delete。queueSize(intsize):因为操作是异步回写到SoR,需要先将操作放入写操作等待队列。因此,使用队列大小来定义写操作等待队列的最大大小,即线程池队列大小。内部使用NonBatchingLocalHeapWriteBehindQueue。concurrencyLevel(intconcurrency):配置用于WriteBehind的并发线程数和队列数。由于我们只传入了一个线程池,那么这个模式是如何实现的呢?首先看下面的代码片段。for(inti=0;i(executionService,defaultThreadPool,config,cacheLoaderWriter));}else{this.stripes.add(newBatchingLocalHeapWriteBehindQueue(executionService,defaultThreadPool,config,cacheLoaderWriter));}}可以看到会创建一个concurrencyLevel队列NonBatchingLocalHeapWriteBehindQueue,它通过以下代码片段。this.executorQueue=newLinkedBlockingQueue(config.getMaxQueueSize());if(config.getThreadPoolAlias()==null){this.executor=executionService.getOrderedExecutor(defaultThreadPool,executorQueue);}else{this.executor=executionService。getOrderedExecutor(config.getThreadPoolAlias(),executorQueue);}CacheLoaderWriter:这里我们只配置write和delete,writeAll和deleteAll会委托批量操作进行write和delete。PooledExecutionService#getOrderedExecutor方法将创建一个PartitionedOrderedExecutor实例。PartitionedOrderedExecutor(BlockingQueuequeue,ExecutorServiceexecutor){this.delegate=newPartitionedUnorderedExecutor(queue,executor,1);}它使用maxWorkers=1创建PartitionedUnorderedExecutor,然后PartitionedUnorderedExecutor由this.runnerPermit=newSemaphore(maxWorkers)控制,即就是,maxWorkers=1达到了并发。因此,Ehcache实际能够写入的最大队列大小为并发级别*队列大小。因为内部使用了线程池写,实现了异步写,同时因为使用了队列,控制了总的吞吐量(这里注意根据实际场景给线程池配置RejectedPolicy),然后下面看看如何实现批量写入。(2)批量写入.withLoaderWriter(newDefaultCacheLoaderWriter(){@OverridepublicvoidwriteAll(Iterable>entries)throwsBulkCacheWritingException,Exception{for(Objectentry:entries){//batchwrite}}@OverridepublicvoiddeleteAll(Iterablekeys)throwsBulkCacheWritingException,Exception{for(Objectkey:keys){//batchdelete}}}).add(WriteBehindConfigurationBuilder.newBatchedWriteBehindConfiguration(3,TimeUnit.SECONDS,2).queueSize(5)).concurrencyLevel(1).enableCoalescing().useThreadPool("writeBehindPool").build()));与上例不同的是,使用了newBatchedWriteBehindConfiguration进行批量配置。●newBatchedWriteBehindConfiguration(longmaxDelay,TimeUnitmaxDelayUnit,intbatchSize):设置批量大小和最大延迟。batchSize用于定义批量大小。当写操作的数量等于批处理大小时,这批数据将被发送到CacheLoaderWriter进行处理。Ehcache使用BatchingLocalHeapWriteBehindQueue实现批队列,批处理代码如下。if(openBatch.add(operation)){//向batch添加操作,当添加的个数等于batchsize时submit(openBatch);//异步提交batch操作openBatch=null;}所以Ehcache其实可以这样写***队列大小为并发级别*队列大小*批处理大小。maxDelay用于配置未完成批处理的最大延迟。比如我们设置batchsize为3,但是实际上我们只写了两条数据。当写入第三条数据时,会触发提交批量操作。但是,如果我们不写第三个,就会导致这两个数据一直等待下去。我们可以设置maxDelay,这两个数据会在超时的时候提交批处理。●enableCoalescing:是否需要合并,即同一个Key只记录最后一条数据。●CacheLoaderWriter:write和delete会转换为writeAll和deleteAll,即批处理。3.CopyPatternCopyPattern有两种,Copy-On-Read(读时复制)和Copy-On-Write(写时复制),因为GuavaCache和Ehcache中的堆缓存都是基于引用的,所以如果有人如果你获取缓存的数据并修改它,可能会出现不可预知的问题。我见过这种情况导致的数据错误。GuavaCache不提供支持,Ehcache3.x提供支持。publicinterfaceCopier{TcopyForRead(Tobj);//Copy-On-Read,如myCache.get()TcopyForWrite(Tobj);//Copy-On-Write,如myCache.put()}ConfiguretheKeyby以下方法和Value'sCopier。CacheConfigurationBuilder.withKeyCopier()CacheConfigurationBuilder.withValueCopier()【本文为专栏作者张凯涛原创文章,作者微信公众号:凯涛的博客(kaitao-1234567)】点此阅读作者更多好文