Java Cache-EHCache系列之Store实现

简介:
写了那么多,终于到Store了。Store是EHCache中Element管理的核心,所有的Element都存放在Store中,也就是说Store用于所有和Element相关的处理。

EHCache中的Element
在EHCache中,它将所有的键值对抽象成一个Element,作为面向对象的设计原则,把数据和操作放在一起,Element除了包含key、value属性以外,它还加入了其他和一个Element相关的统计、配置信息以及操作:
public  class Element  implements Serializable, Cloneable {
     // the cache key. 从1.2以后不再强制要求Serializable,因为如果只是作为内存缓存,则不需要对它做序列化。IgnoreSizeOf注解表示在做SizeOf计算时key会被忽略。
    @IgnoreSizeOf
     private  final Object key;
     // the value. 从1.2以后不再强制要求Serializable,因为如果只是作为内存缓存,则不需要对它做序列化。
     private  final Object value;
     // version of the element. 这个属性只是作为纪录信息,EHCache实际代码中并没有用到,用户代码可以通过它来实现不同版本的处理问题。默认值是1。
    
// 如果net.sf.ehcache.element.version.auto系统属性设置为true,则当Element加入到Cache中时会被更新为当前系统时间。此时,用户设置的值会丢失。
     private  volatile  long version;
     // The number of times the element was hit.命中次数,在每次查找到一个Element会加1。
     private  volatile  long hitCount;
     // The amount of time for the element to live, in seconds. 0 indicates unlimited. 即一个Element自创建(CreationTime)以后可以存活的时间。
     private  volatile  int timeToLive = Integer.MIN_VALUE;
     // The amount of time for the element to idle, in seconds. 0 indicates unlimited. 即一个Element自最后一次被使用(min(CreationTime,LastAccessTime))以后可以存活的时间。
     private  volatile  int timeToIdle = Integer.MIN_VALUE;
     // Pluggable element eviction data instance,它存储这个Element的CreationTime、LastAccessTime等信息,窃以为这个抽取成一个单独的类没什么理由,而且这个类的名字也不好。
     private  transient  volatile ElementEvictionData elementEvictionData;
     // If there is an Element in the Cache and it is replaced with a new Element for the same key, 
    
// then both the version number and lastUpdateTime should be updated to reflect that. The creation time
    
// will be the creation time of the new Element, not the original one, so that TTL concepts still work. 在put和replace操作中该属性会被更新。
     private  volatile  long lastUpdateTime;
     // 如果timeToLive和timeToIdle没有手动设置,该值为true,此时在计算expired时使用CacheConfiguration中的timeTiLive、timeToIdle的值,否则使用Element自身的值。
     private  volatile  boolean cacheDefaultLifespan =  true;
     // 这个ID值用于EHCache内部,但是暂时不知道怎么用。
     private  volatile  long id = NOT_SET_ID;
     // 判断是否expired,这里如果timeToLive、timeToIdle都是Integer.MIN_VALUE时返回false,当他们都是0时,isEternal返回true
     public  boolean isExpired() {
         if (!isLifespanSet() || isEternal()) {
             return  false;
        }

         long now = System.currentTimeMillis();
         long expirationTime = getExpirationTime();

         return now > expirationTime;
    }
     // expirationTime算法:如果timeToIdle没有设置,或设置了,但是该Element还没有使用过,取timeToLive计算出的值;如果timeToLive没有设置,则取timeToIdle计算出的值,
    
// 否则,取他们的最小值。
     public  long getExpirationTime() {
         if (!isLifespanSet() || isEternal()) {
             return Long.MAX_VALUE;
        }

         long expirationTime = 0;
         long ttlExpiry = elementEvictionData.getCreationTime() + TimeUtil.toMillis(getTimeToLive());

         long mostRecentTime = Math.max(elementEvictionData.getCreationTime(), elementEvictionData.getLastAccessTime());
         long ttiExpiry = mostRecentTime + TimeUtil.toMillis(getTimeToIdle());

         if (getTimeToLive() != 0 && (getTimeToIdle() == 0 || elementEvictionData.getLastAccessTime() == 0)) {
            expirationTime = ttlExpiry;
        }  else  if (getTimeToLive() == 0) {
            expirationTime = ttiExpiry;
        }  else {
            expirationTime = Math.min(ttlExpiry, ttiExpiry);
        }
         return expirationTime;
    }
     // 在将Element加入到Cache中并且它的timeToLive和timeToIdle都没有设置时,它的timeToLive和timeToIdle会根据CacheConfiguration的值调用这个方法更新。
     protected  void setLifespanDefaults( int tti,  int ttl,  boolean eternal) {
         if (eternal) {
             this.timeToIdle = 0;
             this.timeToLive = 0;
        }  else  if (isEternal()) {
             this.timeToIdle = Integer.MIN_VALUE;
             this.timeToLive = Integer.MIN_VALUE;
        }  else {
            timeToIdle = tti;
            timeToLive = ttl;
        }
    }
}
public  class DefaultElementEvictionData  implements ElementEvictionData {
     private  long creationTime;
     private  long lastAccessTime;
}
public  class Cache  implements InternalEhcache, StoreListener {
     private  void applyDefaultsToElementWithoutLifespanSet(Element element) {
         if (!element.isLifespanSet()) {
            element.setLifespanDefaults(TimeUtil.convertTimeToInt(configuration.getTimeToIdleSeconds()),
                    TimeUtil.convertTimeToInt(configuration.getTimeToLiveSeconds()),
                    configuration.isEternal());
        }
    }
}

EHCache中的Store设计
Store是EHCache中用于存储、管理所有Element的仓库,它抽象出了所有对Element在内存中以及磁盘中的操作。基本的它可以向一个Store中添加Element(put、putAll、putWithWriter、putIfAbsent)、从一个Store获取一个或一些Element(get、getQuiet、getAll、getAllQuiet)、获取一个Store中所有key(getKeys)、从一个Store中移除一个或一些Element(remove、removeElement、removeAll、removeWithWriter)、替换一个Store中已存储的Element(replace)、pin或unpin一个Element(unpinAll、isPinned、setPinned)、添加或删除StoreListener(addStoreListener、removeStoreListener)、获取一个Store的Element数量(getSize、getInMemorySize、getOffHeapSize、getOnDiskSize、getTerracottaClusteredSize)、获取一个Store的Element以Byte为单位的大小(getInMemorySizeInBytes、getOffHeapSizeInBytes、getOnDiskSizeInBytes)、判断一个key的存在性(containsKey、containsKeyOnDisk、containsKeyOffHeap、containsKeyInMemory)、query操作(setAttributeExtractors、executeQuery、getSearchAttribute)、cluster相关操作(isCacheCoherent、isClusterCoherent、isNodeCoherent、setNodeCoherent、waitUtilClusterCoherent)、其他操作(dispose、getStatus、getMBean、hasAbortedSizeOf、expireElements、flush、bufferFull、getInMemoryEvictionPolicy、setInMemoryEvictionPolicy、getInternalContext、calculateSize)。

所谓Cache,就是将部分常用数据缓存在内存中,从而提升程序的效率,然而内存大小毕竟有限,因而有时候也需要有磁盘加以辅助,因而在EHCache中真正的Store实现就两种(不考虑分布式缓存的情况下):存储在内存中的MemoryStore和存储在磁盘中的DiskStore,而所有其他Store都是给予这两个Store的基础上来扩展Store的功能,如因为内存大小的限制,有些时候需要将内存中的暂时不用的Element写入到磁盘中,以腾出空间给其他更常用的Element,此时就需要MemoryStore和DiskStore共同来完成,这就是FrontCacheTier做的事情,所有可以结合FrontEndCacheTier一起使用的Store都要实现TierableStore接口(DiskStore、MemoryStore、NullStore);对于可控制Store占用空间大小做限制的Store还可以实现PoolableStore(DiskStore、MemoryStore);对于具有Terracotta特性的Store还实现了TerracottaStore接口(TransactionStore等)。

EHCache中Store的设计类结构图如下:

AbstractStore
几乎所有的Store实现都继承自AbstractStore,它实现了Query、Cluster等相关的接口,但没有涉及Element的管理,而且这部分现在也不了解,不详述。

MemoryStore和NotifyingMemoryStore
MemoryStore是EHCache中存储在内存中的Element的仓库。它使用SelectableConcurrentHashMap作为内部的存储结构,该类实现参考ConcurrentHashMap,只是它加入了pinned、evict等逻辑,不详述(注:它的setPinned方法中,对不存在的key,会使用一个DUMMY_PINNED_ELEMENT来创建一个节点,并将它添加到HashEntry的链中,这时为什么?窃以为这个应该是为了在以后这个key添加进来后,当前的pinned设置可以对它有影响,因为MemoryStore中并没有包含所有的Element,还有一部分Element是在DiskStore中)。而MemoryStore中的基本实现都代理给SelectableConcurrentHashMap,里面的其他细节在之前的文章中也有说明,不再赘述。 

而NotifyingMemoryStore继承自MemoryStore,它在Element evict和exipre时会调用注册的CacheEventListener。

DiskStore
DiskStore依然采用ConcurrentHashMap的实现思想,因而这部分逻辑不赘述。对DiskStore,当一个Element添加进来后,需要将其写入到磁盘中,这是接下来关注的重点。在DiskStore中,一个Element不再以Element本身而存在,而是以DiskSubstitute的实例而存在,DiskSubstitute有两个子类:PlaceHolder和DiskMarker,当一个Element初始被添加到DiskStore中时,它是以PlaceHolder的形式存在,当这个PlaceHolder被写入到磁盘中时,它会转换成DiskMarker。
     public  abstract  static  class DiskSubstitute {
         protected  transient  volatile  long onHeapSize;
        @IgnoreSizeOf
         private  transient  volatile DiskStorageFactory factory;
        DiskSubstitute(DiskStorageFactory factory) {
             this.factory = factory;
        }
         abstract Object getKey();
         abstract  long getHitCount();
         abstract  long getExpirationTime();
         abstract  void installed();
         public  final DiskStorageFactory getFactory() {
             return factory;
        }
    }
     final  class Placeholder  extends DiskSubstitute {
        @IgnoreSizeOf
         private  final Object key;
         private  final Element element;
         private  volatile  boolean failedToFlush;
        Placeholder(Element element) {
             super(DiskStorageFactory. this);
             this.key = element.getObjectKey();
             this.element = element;
        }
        @Override
         public  void installed() {
            DiskStorageFactory. this.schedule( new PersistentDiskWriteTask( this));
        }
    }
     public  static  class DiskMarker  extends DiskSubstitute  implements Serializable {
        @IgnoreSizeOf
         private  final Object key;
         private  final  long position;
         private  final  int size;
         private  volatile  long hitCount;
         private  volatile  long expiry;
        DiskMarker(DiskStorageFactory factory,  long position,  int size, Element element) {
             super(factory);
             this.position = position;
             this.size = size;

             this.key = element.getObjectKey();
             this.hitCount = element.getHitCount();
             this.expiry = element.getExpirationTime();
        }
        @Override
         public  void installed() {
             // no-op
        }
         void hit(Element e) {
            hitCount++;
            expiry = e.getExpirationTime();
        }
    }
当向DiskStore添加一个Element时,它会先创建一个PlaceHolder,并将该PlaceHolder添加到DiskStore中,并在添加完成后调用PlaceHolder的installed()方法,该方法会使用DiskStorageFactory schedule一个PersistentDiskWriteTask,将该PlaceHolder写入到磁盘(在DiskStorageFactory有一个DiskWriter线程会在一定的时候执行该Task)生成一个DiskMarker,释放PlaceHolder占用的内存。在从DiskStore移除一个Element时,它会先读取磁盘中的数据,将其解析成Element,然后释放这个Element占用的磁盘空间,并返回这个被移除的Element。在从DiskStore读取一个Element时,它需要找到DiskStore中的DiskSubstitute,对DiskMarker读取磁盘中的数据,解析成Element,然后返回。 

FrontEndCacheTier  
上述的MemoryStore和DiskStore,他们是各自独立的,然而Cache的一个重要特点是可以将部分内存中的数据evict出到磁盘,因为内存毕竟是有限的,所以需要有另一个Store可以将MemoryStore和DiskStore联系起来,这就是FrontEndCacheTier做的事情。FrontEndCacheTier有两个子类:DiskBackedMemoryStore和MemoryOnlyStore,这两个类的名字已经能很好的说明他们的用途了,DiskBackedMemoryStore可以将部分Element先evict出到磁盘,它也支持把磁盘文件作为persistent介质,在下一次读取时可以直接从磁盘中的文件直接读取并重新构建原来的缓存;而MemoryOnlyStore则只支持将Element存储在内存中。FrontEndCacheTier有两个Store属性:cache和authority,它将基本上所有的操作都直接同时代理给这两个Store,其中把authority作为主的存储Store,而将cache作为缓存的Store。在DiskBackedMemoryStore中,authority是DiskStore,而cache是MemoryStore,即DiskBackedMemoryStore将DiskStore作为主的存储Store,这刚开始让我很惊讶,不过仔细想想也是合理的,因为毕竟这里的Disk是作为persistent介质的;在MemoryOnlyStore中,authority是MemoryStore,而cache是NullStore。
FrontEndCacheTier在实现get方法时,添加了faults属性的ConcurrentHashMap,它是用于多个线程在同时读取同一key的Element时避免多次读取,每次之前将key和一个Fault新实例添加到faults中,这样第二个线程发现已经有另一个线程在读这个Element了,它就可以等待第一个线程读完直接拿第一个线程读取的结果即可,以提升性能。
所有作为FrontEndCacheTier的内部Store都必须实现TierableStore接口,其中fill、removeIfNotPinned、isTierPinned、getPresentPinnedKeys为cache store准备,而removeNoReturn、isPersistent为authority store准备。
public  interface TierableStore  extends Store {
     void fill(Element e);
     boolean removeIfNotPinned(Object key);
     void removeNoReturn(Object key);
     boolean isTierPinned();
    Set getPresentPinnedKeys();
     boolean isPersistent();
}

LruMemoryStore和LegacyStoreWrapper
这两个Store只是为了兼容而存在,其中LruMemoryStore使用LinkedHashMap作为其存储结构,他只支持一种Evict算法:LRU,这个Store的名字也因此而来,其他功能它类似MemoryStore,而LegacyStoreWrapper则类似FrontEndCacheTier。这两个Store的代码比较简单,而且他们也不应该再被使用,因而不细究。 

TerraccottaStore
对所有实现这个接口的Store都还不了解,看以后有没有时间回来了。。。。

相关文章
|
Java
Java 实现汉字按照首字母分组排序
Java 实现汉字按照首字母分组排序
570 0
|
分布式计算 Java Hadoop
Java实现单词计数MapReduce
本文分享实现单词计数MapReduce的方法
302 0
|
Java 数据安全/隐私保护
JAVA 实现上传图片添加水印(详细版)(上)
JAVA 实现上传图片添加水印(详细版)
956 0
JAVA 实现上传图片添加水印(详细版)(上)
|
存储 Java
Java实现图书管理系统
本篇文章是对目前Java专栏已有内容的一个总结练习,希望各位小主们在学习完面向对象的知识后,可以阅览本篇文章后,自己也动手实现一个这样的demo来加深总结应用已经学到知识并进行巩固。
376 0
Java实现图书管理系统
|
Java Windows Spring
java实现spring boot项目启动时,重启Windows进程
java实现spring boot项目启动时,重启Windows进程
476 0
|
数据可视化 Java
Java实现拼图小游戏(1)—— JFrame的认识及界面搭建
如果要在某一个界面里面添加功能的话,都在一个类中,会显得代码难以阅读,而且修改起来也会很困难,所以我们将游戏主界面、登录界面、以及注册界面都单独编成一个类,每一个类都继承JFrame父类,并且在类中创建方法来来实现页面
462 0
Java实现拼图小游戏(1)—— JFrame的认识及界面搭建
|
网络协议 Java
Java网络编程:UDP/TCP实现实时聊天、上传图片、下载资源等
ip地址的分类: 1、ipv4、ipv6 127.0.0.1:4个字节组成,0-255,42亿;30亿都在北美,亚洲就只有4亿 2011年就用尽了。
Java网络编程:UDP/TCP实现实时聊天、上传图片、下载资源等
|
数据可视化 Java 容器
Java实现拼图小游戏(7)—— 计步功能及菜单业务的实现
注意由于我们计步功能的步数要在重写方法中用到,所以不能将初始化语句写在方法体内,而是要写在成员位置。在其名字的时候也要做到“见名知意”,所以我们给它起名字为step
268 0
Java实现拼图小游戏(7)—— 计步功能及菜单业务的实现
|
Java
Java实现拼图小游戏(7)—— 作弊码和判断胜利
当我们好不容易把拼图复原了,但是一点提示也没有,完全看不出来是成功了,那么我们就需要有判断胜利的功能去弹出“成功”类的图片,以便于玩家选择是重新开始还是退出小游戏
240 0
Java实现拼图小游戏(7)—— 作弊码和判断胜利
|
Java
Java实现拼图小游戏(7)——查看完整图片(键盘监听实例2)
由于在移动和图片中我们已经添加了键盘监听,也继承了键盘监听的接口,那么我们只需要在重写方法内输入我们的代码即可
175 0