当前位置: 首页 > 后端技术 > Java

Guava缓存过期方案实践

时间:2023-04-02 10:04:25 Java

过期机制只要是缓存,就一定有过期机制。Guava缓存过期分为以下三种:expireAfterAccess:指定时间内没有被访问(读取或写入)的数据为过期数据,当没有数据或读取过期数据时,只允许一个线程更新新的数据,其他线程被阻塞等待线程的更新完成,然后去取最新的数据。构造函数:publicCacheBuilderexpireAfterAccess(longduration,TimeUnitunit){...this.expireAfterAccessNanos=unit.toNanos(duration);returnthis;}构造函数设置变量expireAfterAccessNanos的值。expireAfterWrite:如果数据在指定时间内没有更新(写入),则为过期数据。当没有数据或读取到过期数据时,只允许一个线程更新新数据,其他线程阻塞等待该线程的更新完成。最新数据。构造函数:publicCacheBuilderexpireAfterWrite(longduration,TimeUnitunit){...this.expireAfterWriteNanos=unit.toNanos(duration);returnthis;}构造函数设置变量expireAfterWriteNanos的值。refreshAfterWrite:如果数据在指定时间内没有更新(写入),则为过期数据。当一个线程正在更新(写入)新数据时,其他线程会返回旧数据。构造函数:publicCacheBuilderrefreshAfterWrite(longduration,TimeUnitunit){...this.refreshNanos=unit.toNanos(duration);returnthis;}构造函数设置变量refreshNanos的值。问题expireAfterAccess和expireAfterWrite:当数据到达过期时间后,只有一个线程可以进行数据刷新,其他请求阻塞等待刷新操作完成,会造成性能损失。refreshAfterWrite:数据过期时,只有一个线程可以加载新值,其他线程返回旧值(也可以设置异步获取新值,所有线程返回旧值)。这有效地减少了等待和锁争用,所以refreshAfterWrite会比expireAfterWrite表现得更好。但是还是会有一个线程需要执行刷新任务,guavacache是??支持异步刷新的。如果启用了异步刷新,线程在提交异步刷新任务后会返回旧值,性能更好。但是由于guavacache没有定时清理(主动)的功能,所以在查询数据的时候会做过期检查和清理(被动)。那么就会出现以下问题:如果经过很长一段时间后才去查询数据,得到的旧值可能来自很久以前,就会出现问题,对时效性要求高的场景可能会造成非常大的错误。当缓存中没有要访问的数据时,无论设置哪种模式,所有线程都会被阻塞,只有一个线程会通过锁控制来加载数据。原理首先要了解guava缓存过期的原理。1.整体方法get方法:classLocalCacheextendsAbstractMapimplementsConcurrentMap{Vget(Kkey,inthash,CacheLoaderloader)throwsExecutionException{checkNotNull(键);checkNotNull(装载机);try{if(count!=0){//read-volatile//不要调用getLiveEntry,这会忽略加载值ReferenceEntrye=getEntry(key,hash);if(e!=null){longnow=map.ticker.read();V值=getLiveValue(e,现在);如果(值!=null){recordRead(e,现在);statsCounter.recordHits(1);返回scheduleRefresh(e,键,散列,值,现在,加载器);}ValueReferencevalueReference=e.getValueReference();如果(valueReference.isLoading()){返回waitForLoadingValue(e,key,valueReference);}}}//此时e为null或已过期;返回lockedGetOrLoad(key,hash,loader);}catch(ExecutionExceptionee){Throwablecause=ee.getCause();if(causeinstanceofError){thrownewExecutionError((Error)原因);}elseif(causeinstanceofRuntimeException){thrownewUncheckedExecutionException(cause);}扔ee;}最后{postReadCleanup();核心数据结构基于ConcurrentHashMap2.简化方法这里将方法简化为与本题相关的几个关键步骤:if(count!=0){//当前缓存是否有数据ReferenceEntrye=getEntry(key,hash);//获取数据节点if(e!=null){Vvalue=getLiveValue(e,now);//判断是否过期,过滤过期数据,只判断expireAfterAccess或expireAfterWrite方式设置的时间if(value!=null){returnscheduleRefresh(e,key,hash,value,now,loader);//是否刷新数据,仅在refreshAfterWrite方式下有效}ValueReferencevalueReference=e.getValueReference();if(valueReference.isLoading()){//如果其他线程正在加载/刷新数据returnwaitForLoadingValue(e,key,valueReference);//等待其他线程完成加载/刷新数据}}}returnlockedGetOrLoad(key,hash,loader);//Load/refreshdatacount是缓存的一个属性,由volatile(volatileintcount)修饰,保存的是当前缓存的个数。如果count==0(无缓存)或者根据key获取不到Hash节点,则锁定并加载缓存(lockedGetOrLoad)。如果获取到Hash节点,则判断是否过期(getLiveValue),过滤掉过期数据。3.getLiveValueVgetLiveValue(ReferenceEntryentry,longnow){if(entry.getKey()==null){tryDrainReferenceQueues();}返回空值;}V值=entry.getValueReference().get();if(value==null){tryDrainReferenceQueues();返回空值;}if(map.isExpired(entry,now)){tryExpireEntries(now);返回空值;}returnvalue;}使用isExpired判断当前节点是否过期:booleanisExpired(ReferenceEntryentry,longnow){checkNotNull(entry);if(expiresAfterAccess()&&(now-entry.getAccessTime()>=expireAfterAccessNanos)){returntrue;}if(expiresAfterWrite()&&(now-entry.getWriteTime()>=expireAfterWriteNanos)){returntrue;}returnfalse;}isExpired只判断了expireAfterAccessNanos和expireAfterWriteNanos的两次,结合expireAfterAccess、expireAfterWrite、refreshAfterWrite三个方法的构造函数,可以看到这个方法并不关心refreshAfterWrite设置的时间,即如果expireAfterAccess和expireAfterWrite设置的时间已经过去,数据过期,否则没有过期。如果发现数据已经过期,则会检查是否还有其他过期数据(懒删除):voidtryExpireEntries(longnow){if(tryLock()){try{expireEntries(now);}最后{解锁();//不要调用postWriteCleanup,因为我们正在读取}}}voidexpireEntries(longnow){drainRecencyQueue();ReferenceEntrye;while((e=writeQueue.peek())!=null&&map.isExpired(e,now)){if(!removeEntry(e,e.getHash(),RemovalCause.EXPIRED)){thrownewAssertionError();}}while((e=accessQueue.peek())!=null&&map.isExpired(e,now)){if(!removeEntry(e,e.getHash(),RemovalCause.EXPIRED)){thrownewAssertionError();}}}voiddrainRecencyQueue(){ReferenceEntrye;while((e=recencyQueue.poll())!=null){if(accessQueue.contains(e)){accessQueue.add(e);}}}获取最新的范围并写入数据,一个检查是否过期。4.scheduleRefreshVvalue=getLiveValue(e,now);if(value!=null){returnscheduleRefresh(e,key,hash,value,now,loader);}getLiveValue后,如果结果不为null,则表示expireAfterAccessexpireAfterWrite的两种模式都没有过期(或者这两种模式的时间没有设置),但是不代表数据不会刷新,因为getLiveValue不判断refreshAfterWrite的过期时间,而是判断它在scheduleRefresh方法中。VscheduleRefresh(ReferenceEntryentry,Kkey,inthash,VoldValue,longnow,CacheLoader加载器){if(map.refreshes()&&(now-entry.getWriteTime()>map.refreshNanos)&&!entry.getValueReference().isLoading()){VnewValue=refresh(key,hash,loader,true);如果(新值!=null){返回新值;}}returnoldValue;}满足以下条件,才会刷新数据(同步刷新方式刷新线程返回新值,异步刷新方式可能返回旧值),否则旧值直接返回:refreshAfterWrite时间refreshNanos设置。当前数据已过时。没有其他线程正在刷新数据(!entry.getValueReference().isLoading())。5.如果waitForLoadingValue没有设置refreshAfterWrite,数据已经过期:如果其他线程正在刷新,则阻塞等待(被future.get()阻塞)。如果没有其他线程正在刷新,则锁定并刷新数据。ValueReferencevalueReference=e.getValueReference();if(valueReference.isLoading()){returnwaitForLoadingValue(e,key,valueReference);}VwaitForLoadingValue(ReferenceEntrye,Kkey,ValueReferencevalueReference)throwsExecutionException{if(!valueReference.isLoading()){thrownewAssertionError();}}checkState(!Thread.holdsLock(e),"递归加载:%s",key);//不要考虑过期,因为我们正在加载try{Vvalue=valueReference.waitForValue();if(value==null){thrownewInvalidCacheLoadException("CacheLoader为key"+key+"返回null。");}//加载完成后重新读取自动收报机longnow=map.ticker.read();记录读取(e,现在);返回值;}最后{statsCounter.recordMisses(1);}}publicVwaitForValue()throwsExecutionException{returngetUninterruptibly(futureValue);}publicstaticVgetUninterruptibly(Futurefuture)抛出ExecutionException{布尔中断=false;尝试{while(true){尝试{返回未来。得到();}catch(InterruptedExceptione){interrupted=true;}}}最后{如果(中断){线程。当前线程().中断();}}}6。加载数据加载数据,最终要么调用lockedGetOrLoad方法,要么调用scheduleRefresh中的refresh方法,最后调用CacheLoader的load/reload方法。当缓存中没有要访问的数据时,无论设置哪种模式,都会进入lockedGetOrLoad方法:通过锁竞争获得加载数据的权利。抓取锁定数据,设置节点状态为loading,加载数据。对于没有加锁的数据,进入和上一步一样的waitForLoadingValue方法,阻塞直到数据加载完成。lock();try{LoadingValueReferenceloadingValueReference=newLoadingValueReference(valueReference);e.setValueReference(loadingValueReference);如果(createNewEntry){loadingValueReference=newLoadingValueReference();if(e==null){e=newEntry(key,hash,first);e.setValueReference(loadingValueReference);table.set(index,e);}else{e.setValueReference(loadingValueReference);}}}最后{unlock();postWriteCleanup();}if(createNewEntry){try{//在检测到递归加载时同步条目以允许快速失败。这在复制条目时可能会被规避,但大多数时候会很快失败。synchronized(e){returnloadSync(key,hash,loadingValueReference,loader);}}最后{statsCounter.recordMisses(1);}}else{//该条目已经存在。等待加载。返回waitForLoadingValue(e,key,valueReference);}解决方案通过上面的分析我们可以知道,在判断缓存是否过期的时候,guava缓存会分为两个独立的判断:判断expireAfterAccess和expireAfterWrite判断refreshAfterWrite。回到问题“虽然refreshAfterWrite提高了性能,但除了同步加载模式下执行刷新的线程外,其他线程可能会访问过时已久的数据”。我们可以结合expireAfterWrite和refreshAfterWrite来解决:在设置refreshAfterWrite的过期时间的同时,还可以设置expireAfterWrite/expireAfterAccess的过期时间,expireAfterWrite/expireAfterAccess的时间大于refreshAfterWrite的时间。比如refreshAfterWrite的时间是5分钟,expireAfterWrite的时间是30分钟。访问过期数据时:如果过期时间小于30分钟,会进入scheduleRefresh方法,刷新线程以外的线程会直接返回旧值。如果缓存数据长时间未被访问,过期时间超过30分钟,则在getLiveValue方法中会过滤掉该数据,除刷新线程外其他线程会阻塞等待。