Java Cache-EHCache系列之Store实现

标签: java cache ehcache | 发表时间:2013-11-03 22:05 | 作者:DLevin
出处:http://www.blogjava.net/
写了那么多,终于到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都还不了解,看以后有没有时间回来了。。。。

DLevin 2013-11-03 22:05 发表评论

相关 [java cache ehcache] 推荐:

Java Cache-EHCache系列之Store实现

- - BlogJava-首页技术区
写了那么多,终于到Store了. Store是EHCache中Element管理的核心,所有的Element都存放在Store中,也就是说Store用于所有和Element相关的处理. EHCache中的Element. 在EHCache中,它将所有的键值对抽象成一个Element,作为面向对象的设计原则,把数据和操作放在一起,Element除了包含key、value属性以外,它还加入了其他和一个Element相关的统计、配置信息以及操作:.

Java Cache-EHCache系列之AA-Tree实现溢出到磁盘的数据管理

- - BlogJava-首页技术区
在EHCache中,如果设置了overflowToDisk属性,当Cache中的数据超过限制时,EHCache会根据配置的溢出算法(先进先出(FIFO)、最近最少使用算法(LRU)等),选择部分实例,将这些实例的数据写入到磁盘文件中临时存储,已减少内存的负担,当内存中的实例数减少(因为超时或手工移除)或某些实例被使用到时,又可以将这些写入磁盘的数据重新加载到内存中.

Java Cache-EHCache系列之计算实例占用的内存大小(SizeOf引擎)

- - BlogJava-首页技术区
计算一个实例内存占用大小思路. 在Java中,除了基本类型,其他所有通过字段包含其他实例的关系都是引用关系,因而我们不能直接计算该实例占用的内存大小,而是要递归的计算其所有字段占用的内存大小的和. 在Java中,我们可以将所有这些通过字段引用简单的看成一种树状结构,这样就可以遍历这棵树,计算每个节点占用的内存大小,所有这些节点占用的内存大小的总和就当前实例占用的内存大小,遍历的算法有:先序遍历、中序遍历、后序遍历、层级遍历等.

Java Cache-EHCache系列之AA-Tree实现溢出到磁盘的数据管理(2)

- - BlogJava-首页技术区
在上一篇《 Java Cache-EHCache系列之AA-Tree实现溢出到磁盘的数据管理(1)》已经详细讲解了EHCache中在AATreeSet中对AA Tree算法的实现,并且指出EHCache是采用它作为空闲磁盘管理数据结构,本文主要关注于EHCache是如何采用AATreeSet类来管理空闲磁盘的(这里的磁盘管理是只EHCache data文件中的空闲磁盘).

Java Cache系列之Guava Cache

- - BlogJava-首页技术区
然而作为工具库中的一部分,我们自然不能期待Guava对Cache有比较完善的实现. 因而Guava中的Cache只能用于一些把Cache作为一种辅助设计的项目或者在项目的前期为了实现简单而引入. 在Guava CacheBuilder的注释中给定Guava Cache以下的需求:. 对于这样的需求,如果要我们自己来实现,我们应该怎么设计.

从Java视角理解CPU缓存(CPU Cache)

- - 淘宝网通用产品团队博客
从Java视角理解系统结构连载, 关注我的微博( 链接)了解最新动态众所周知, CPU是计算机的大脑, 它负责执行程序的指令; 内存负责存数据, 包括程序自身数据. 同样大家都知道, 内存比CPU慢很多. 其实在30年前, CPU的频率和内存总线的频率在同一个级别, 访问内存只比访问CPU寄存器慢一点儿.

Guava cache

- - 孟飞阳的博客
Guava Cache是一个全内存的本地缓存实现,它提供了线程安全的实现机制. 整体上来说Guava cache 是本地缓存的不二之选,简单易用,性能好.    Guava Cache有两种创建方式:.   通过这两种方法创建的cache,和通常用map来缓存的做法比,不同在于,这两种方法都实现了一种逻辑——从缓存中取key X的值,如果该值已经缓存过了,则返回缓存中的值,如果没有缓存过,可以通过某个方法来获取这个值.

巧用query cache

- - OurMySQL
   收到一用户反馈其应用日志中狂报错误,获取连接超时:. 同时应用报错超出了数据库的最大连接数:max connections:. 这种情况很有可能是有慢sql占用了连接池中的连接没有释放,导致后续进来的请求迟迟获取不到连接池中的连接,导致请求报错,登录数据库排查发现如下sql出现执行非常的慢:.

在Spring、Hibernate中使用Ehcache缓存

- - BlogJava-首页技术区
前一篇 http://www.blogjava.net/hoojo/archive/2012/07/12/382852.html介绍了Ehcache整合Spring缓存,使用页面、对象缓存;这里将介绍在Hibernate中使用查询缓存、一级缓存、二级缓存,整合Spring在HibernateTemplate中使用查询缓存.

[转][转]Redis、Memcached、Guava、Ehcache中的算法

- - heiyeluren的blog(黑夜路人的开源世界)
缓存那些事,一是内存爆了要用LRU(最近最少使用)、LFU(最少访问次数)、FIFO的算法清理一些;二是设置了超时时间的键过期便要删除,用主动或惰性的方法. 今天看 Redis3.0的发行通告里说,LRU算法大幅提升了,就翻开源码来八卦一下,结果哭笑不得,这旧版的"近似LRU"算法,实在太简单,太偷懒,太Redis了.