缓存原理

理论总结

它要解决什么样的问题?

数据的访问、存取、计算太慢、太不稳定、太消耗资源,同时,这样的操作存在重复性。因此希望有这样一种中间媒介,放置在其间,只保存自己关心的数据,而不关心具体数据逻辑内容,对于重复性的操作给出响应。对于数据和服务的使用者,它是透明的。

为哪些数据做缓存?

  • 模型对象,这在业务逻辑层面最常见。
  • 数据库查询结果集。
  • 页面缓存、页面片段缓存。
  • 运算结果集,尤其对于幂等性服务。
  • 外部接口查询结果。

缓存框架的核心

缓存生命周期管理,很多重要特性都是围绕它来展开的。

重要特性,这些特性不一定全部要具备,但是多数都要包含

  • 一致性选择。缓存框架的设计必须首先考虑这一点。通常我们见到的缓存框架都是最终一致性的,允许获取数据有一定的延迟窗口。一致性关系到缓存的生命周期,是缓存的核心理念之一。

  • 分级存储。也和缓存生命周期密切相关。至少应包括内存和磁盘两级存储,有些缓存框架包含组网内部节点的分级等等,允许用户管理缓存数据在不同级别存储中的跃迁。分级存储还包括对存储数据的管理,以提高数据获取的效率;包括跃迁策略,超时策略的定制,比如在某一级满足怎样的超时策略可以发生向下跃迁。

  • 规约配置,默认配置。可以支持XML、properties、DSL编程等等多种配置方式,但是最重要的是,要提供一个默认配置,允许用户在简单配置或者零配置的情况下使用缓存。

  • 集群、分布式,这意味着一定的伸缩性。包括内部通信协议选择,比如节点之间使用JMS、RMI或RESTful方式通信等等;包括节点热部署和节点发现能力,这通常都使用组播消息来实现;包括集群的方式,是Server-Client群、消息总线方式还是节点对等,等等。

  • 定制扩展性。尤其是淘汰算法、事件监听、持久化策略等等,都要允许用户方便地自定义。

相对较次要的特性:

统计能力。包括各级缓存命中情况统计,生命周期长度统计。
批量接口、异步接口。包括缓存分组能力。
缓存数据存储校验。
Web支持。特指Web容器中,对于页面存储的额外支持。
免锁数据处理。
缓存状态监控。
无侵入式拦截,注解编程支持。
运行时参数调整。

核心模型应该包括哪些

CacheManager:模型管理对象,可以是多实例的,也可以是单实例的。
Cache:通过CacheManager创建出来的缓存容器,内部包含了真正的缓存承载体,至少开放add/remove/flush等接口。
CacheMap:真正的缓存承载体,大致上都是一个Map,各种类型的Map。
CacheEntity:缓存条目,相当于CacheMap里面的每一条Entry。
CacheEvent:缓存事件,比如CacheEntity的创建、更新、删除等等。
CacheEventListener:缓存事件相应的监听器。
CacheEvictionAlgorithm:缓存淘汰算法,常见的有LRU、LFU、FIFO等等。

数据流阶段

  • 操作捕获
  • 缓存数据存储
  • 缓存数据读取
  • 缓存数据流动

OSCache分析

核心类和核心概念

  • cache factory:AbstractCacheAdministrator,生产Cache,同时管理用户配置的config,配置监听器列表和Cache的绑定。子类GeneralCacheAdministrator是通用实现,子类ServletCacheAdministrator关联了一个ServletContext,以实现在Web容器中的存储和传值(对于session的scope,持久化时,存放路径上会建立一个sessionID的dir,以避免存放冲突)。

  • cache proxy:Cache,是OSCache缓存管理的核心,也是cache map的存放场所。子类ServletCache引入了一个scope的概念,用以管理不同的可见性缓存,存在application级别、session级别;

  • cache map:AbstractConcurrentReadCache,缓存存储map。下面有基于它的子类,分别实现了LRU算法、FIFO算法和无限制缓存策略

  • cache entry:缓存条目,map中存储的每一项。其内部包含了缓存条目的创建、修改时间,存储的key、value等重要属性,此外,还有一个Set group,标识每个entry从属于哪些组。

值得说明的是,这张图虽然简单,却很有借鉴意义,再复杂的缓存框架,它往往都逃脱不出这样的最基本的设计。

调用示例代码如下:
ServletCacheAdministrator admin = ServletCacheAdministrator.getInstance(config.getServletContext());
Cache cache = admin.getCache(httpRequest, cacheScope);
cache.flushGroup(group);

cache map对cache entry的管理

EntryUpdateState是cache entry当前所处状态的表示,OSCache尽量避免了使用synchronize,引入了许多状态参量。状态变迁图示如下:

对于缓存超期的判定

官方推荐有两种方案,一种是“with fail over”的:

String myKey = "myKey";  
String myValue;  
int myRefreshPeriod = 1000;  
try {  
    // Get from the cache  
    myValue = (String) admin.getFromCache(myKey, myRefreshPeriod);  
} catch (NeedsRefreshException nre) {  
    try {  
        // Get the value (probably by calling an EJB)  
        myValue = "This is the content retrieved.";  
        // Store in the cache  
        admin.putInCache(myKey, myValue);  
    } catch (Exception ex) {  
        // We have the current content if we want fail-over.  
        myValue = (String) nre.getCacheContent();  
        // It is essential that cancelUpdate is called if the  
        // cached content is not rebuilt  
        admin.cancelUpdate(myKey);  
    }  
}  

一种是“without fail over”的:

String myKey = "myKey";  
String myValue;  
int myRefreshPeriod = 1000;  
try {  
    // Get from the cache  
    myValue = (String) admin.getFromCache(myKey, myRefreshPeriod);  
} catch (NeedsRefreshException nre) {  
    try {  
        // Get the value (probably by calling an EJB)  
        myValue = "This is the content retrieved.";  
        // Store in the cache  
        admin.putInCache(myKey, myValue);  
        updated = true;  
    } finally {  
        if (!updated) {  
            // It is essential that cancelUpdate is called if the  
            // cached content could not be rebuilt  
            admin.cancelUpdate(myKey);  
        }  
    }  
}  

这里出现了臭名昭著的NeedsRefreshException,它在缓存条目过期或者不存在的时候都会抛出。当这个异常出现,相应的cache map的key会被锁住,并且要访问它的所有其他线程都被block住了,所以,这个时候一定要调用putInCache或者cancelUpdate,千万不能遗漏,否则就是一个死锁。

  • 为什么要这样实现?
    首先,我们需要好好分析分析OSCache的核心,Cache类。Cache类的属性中,这个属性和这个问题最相关:

private Map updateStates = new HashMap();

其中,updateStates这个map,它的key是正在工作的缓存条目的key,value是EntryUpdateState,它是用来协调并发访问同一个缓存条目之用的。
当一条缓存条目正在被更新,那么有两种策略,根据配置项cache.blocking的配置,要么等待更新完成(阻塞策略),要么返回已经过时的缓存内容(非阻塞策略),选用哪种策略。
为了避免数据争用,cache map里面的值在某线程操作的过程中不能消失,因此updateStates实际的作用是显式引用计数(每一个updateState里面都有一个计数器),在所有线程都完成存取和更新以后,cache map的这条entry才能被清除掉。

再补充一个前提知识,缓存访问状态最终有三种:

  • HIT——表示命中;
  • MISS——表示未命中;
  • STALE_HIT——表示命中了一个失效的缓存(就是getFromCache整个过程初期还有效,等到getFromCache逻辑执行完已经过期了)。

淘汰算法

常见的有FIFO和LRU,FIFO比较简单
如果让你来做LRU算法(Least Recently Used最近最少使用算法),你会怎样实现?
如果是我的话,我会回答:利用JDK的LinkedHashMap预留的机制来实现。

先来看看JDK的HashMap的存储:

上图每一个矩形都是一个Entry,HashMap存放的时候,通过Entry[] table来管理hash之后的每一个桶的入口,在调用map的get方法的时候,先对key进行hash计算,找到桶,然后和桶内挨个entry的key进行equals操作,来寻找目标对象。

而LinkedHashMap,它的Entry继承自HashMap的Entry,比HashMap的Entry多了两个属性:before和after,这样就在HashMap的基础上,又单独维护了一个双向循环链表,同时LinkedHashMap保留了一个对这个链表head的引用

同时,LinkedHashMap引入一个accessOrder属性,用来控制access-order还是insertion-order,前者表示按照访问情况排序,后者表示按照插入情况排序。每次调用get方法时,进行一次recordAccess操作,如果是按照访问顺序排序的话,我需要在这次get访问后调整次序,即将刚访问的节点移到head节点之前(而每次要淘汰一个节点的时候,一定是淘汰header之后的那个节点)。

每次添加节点时都调用如下方法判断是否需要移除一个最近最少使用的节点:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {  
    return false;  
}  

而这个方法是protected方式扩展给子类实现的,我只要在我建立的子类LRUMap里面实现这个方法,判断当前cache map的size是否已经超出预设上限,超出则返回true即可。

好,我可以按照上面这个办法实现,但是这个办法的最大问题是,它不是线程安全的。

在AbstractConcurrentReadCache下有FIFOCache、LRUCache和UnlimitedCache三个子类。以LRUCache为例,它用一个LinkedHashSet,起到队列的作用,来存放所有的key,实现父类的回调方法进行这个队列的维护操作。

事件/监听模型

CacheEvent下面有如下几个子类,其中CacheMapAccessEvent对应的listener是CacheMapAccessEventListener,ScopeEvent对应的listener是ScopeEventListener,其余的几个event对应的listener都是CacheEntryEventListener,这三种类型的实现类中都有相应的一个Statistic监听实现类,做统计用:
CacheEntryEvent:对于cache entry发生add/remove/update/flush等操作时触发;
CacheGroupEvent:类似,只是对象变成了cache entry group,而不是cache entry;
CacheMapAccessEvent:访问cache map时触发;
CachePatternEvent:当cache匹配到某种模式(使用key.indexOf(pattern)判断是否匹配)时进行flush的时候触发;
CachewideEvent:当cache flushAll的时候触发;
ScopeEvent:仅在ServletCache出现flush时触发。

持久化

PersistenceListener接口定义了remove、retrieve和store等用于缓存持久化的方法,抽象实现类AbstractDiskPersistenceListener下,有两个子类:DiskPersistenceListener和HashDiskPersistenceListener,后者给文件名做了md5散列,并根据散列结果,将文件分散存储到多个文件夹内,以提高文件数量太大时,文件和文件夹访问的性能(操作系统文件夹内文件数量有限制):

对于集群节点下持久化文件的存储,可能造成名字冲突的问题,OSCache给出的解决办法是文件名组成上增加一个serverName。

集群

LifecycleAware接口提供了Cache的初始化和终结方法接口,AbstractBroadcastingListener在缓存flush发生时通知到其他节点,通知的方式由不同的子类实现。
最常用的是JGroupBroadcastingListener,使用JGroup通信(多播消息),但是JGroup并不是一个少惹麻烦的主,曾经有同事遇到过集群Cache过多导致JGroup通信时节点累积的NAKACK数量过大的问题,消耗大量内存,请在使用前考察清楚。

web支持

基于OSCache的web请求缓存方案:

每次目标请求到达,生成相应的key后,调用getFromCache尝试获取缓存信息:
如果成功取得缓存对象,从缓存中取得content并做一定修正后输出到response;
如果NeedsRefreshException抛出,缓存过期,这里用一点小技巧,给response包装一层,让后面逻辑写入response时,既写入原生HttpServletResponse中,也写入拟造出来的一个fake response流中,这样原生response可以顺利返回页面,而虚拟response则存放到CacheEntry中,甚至持久化到磁盘。

Ehcache 原理分析

特性

  • 1、快速轻量
    过去几年,诸多测试表明Ehcache是最快的Java缓存之一。
    Ehcache的线程机制是为大型高并发系统设计的。
    大量性能测试用例保证Ehcache在不同版本间性能表现得一致性。
    很多用户都不知道他们正在用Ehcache,因为不需要什么特别的配置。
    API易于使用,这就很容易部署上线和运行。
    很小的jar包,Ehcache 2.2.3才668kb。
    最小的依赖:唯一的依赖就是SLF4J了。

  • 2、伸缩性
    缓存在内存和磁盘存储可以伸缩到数G,Ehcache为大数据存储做过优化。
    大内存的情况下,所有进程可以支持数百G的吞吐。
    为高并发和大型多CPU服务器做优化。
    线程安全和性能总是一对矛盾,Ehcache的线程机制设计采用了Doug Lea的想法来获得较高的性能。
    单台虚拟机上支持多缓存管理器。
    通过Terracotta服务器矩阵,可以伸缩到数百个节点。

  • 3、灵活性
    Ehcache 1.2具备对象API接口和可序列化API接口。
    不能序列化的对象可以使用除磁盘存储外Ehcache的所有功能。
    除了元素的返回方法以外,API都是统一的。只有这两个方法不一致:getObjectValue和getKeyValue。这就使得缓存对象、序列化对象来获取新的特性这个过程很简单。
    支持基于Cache和基于Element的过期策略,每个Cache的存活时间都是可以设置和控制的。
    提供了LRU、LFU和FIFO缓存淘汰算法,Ehcache 1.2引入了最少使用和先进先出缓存淘汰算法,构成了完整的缓存淘汰算法。
    提供内存和磁盘存储,Ehcache和大多数缓存解决方案一样,提供高性能的内存和磁盘存储。
    动态、运行时缓存配置,存活时间、空闲时间、内存和磁盘存放缓存的最大数目都是可以在运行时修改的。

  • 4、标准支持
    Ehcache提供了对JSR107 JCACHE API最完整的实现。因为JCACHE在发布以前,Ehcache的实现(如net.sf.jsr107cache)已经发布了。
    实现JCACHE API有利于到未来其他缓存解决方案的可移植性。
    Ehcache的维护者Greg Luck,正是JSR107的专家委员会委员。

  • 5、可扩展性
    监听器可以插件化。Ehcache 1.2提供了CacheManagerEventListener和CacheEventListener接口,实现可以插件化,并且可以在ehcache.xml里配置。
    节点发现,冗余器和监听器都可以插件化。
    分布式缓存,从Ehcache 1.2开始引入,包含了一些权衡的选项。Ehcache的团队相信没有什么是万能的配置。
    实现者可以使用内建的机制或者完全自己实现,因为有完整的插件开发指南。
    缓存的可扩展性可以插件化。创建你自己的缓存扩展,它可以持有一个缓存的引用,并且绑定在缓存的生命周期内。
    缓存加载器可以插件化。创建你自己的缓存加载器,可以使用一些异步方法来加载数据到缓存里面。
    缓存异常处理器可以插件化。创建一个异常处理器,在异常发生的时候,可以执行某些特定操作。

  • 6、应用持久化
    在VM重启后,持久化到磁盘的存储可以复原数据。
    Ehcache是第一个引入缓存数据持久化存储的开源Java缓存框架。缓存的数据可以在机器重启后从磁盘上重新获得。
    根据需要将缓存刷到磁盘。将缓存条目刷到磁盘的操作可以通过cache.flush()方法来执行,这大大方便了Ehcache的使用。

  • 7、监听器
    缓存管理器监听器。允许注册实现了CacheManagerEventListener接口的监听器:
    notifyCacheAdded()
    notifyCacheRemoved()
    缓存事件监听器。允许注册实现了CacheEventListener接口的监听器,它提供了许多对缓存事件发生后的处理机制:
    notifyElementRemoved/Put/Updated/Expired

  • 8 分布式缓存
    从Ehcache 1.2开始,支持高性能的分布式缓存,兼具灵活性和扩展性。
    分布式缓存的选项包括:
    通过Terracotta的缓存集群:设定和使用Terracotta模式的Ehcache缓存。缓存发现是自动完成的,并且有很多选项可以用来调试缓存行为和性能。
    使用RMI、JGroups或者JMS来冗余缓存数据:节点可以通过多播或发现者手动配置。状态更新可以通过RMI连接来异步或者同步完成。
    Custom:一个综合的插件机制,支持发现和复制的能力。
    可用的缓存复制选项。支持的通过RMI、JGroups或JMS进行的异步或同步的缓存复制。
    可靠的分发:使用TCP的内建分发机制。
    节点发现:节点可以手动配置或者使用多播自动发现,并且可以自动添加和移除节点。对于多播阻塞的情况下,手动配置可以很好地控制。
    分布式缓存可以任意时间加入或者离开集群。缓存可以配置在初始化的时候执行引导程序员。
    BootstrapCacheLoaderFactory抽象工厂,实现了BootstrapCacheLoader接口(RMI实现)。
    缓存服务端。Ehcache提供了一个Cache Server,一个war包,为绝大多数web容器或者是独立的服务器提供支持。
    缓存服务端有两组API:面向资源的RESTful,还有就是SOAP。客户端没有实现语言的限制。
    RESTful缓存服务器:Ehcached的实现严格遵循RESTful面向资源的架构风格。
    SOAP缓存服务端:Ehcache RESTFul Web Services API暴露了单例的CacheManager,他能在ehcache.xml或者IoC容器里面配置。

  • 9、Java EE和应用缓存
    为普通缓存场景和模式提供高质量的实现。
    阻塞缓存:它的机制避免了复制进程并发操作的问题。
    SelfPopulatingCache在缓存一些开销昂贵操作时显得特别有用,它是一种针对读优化的缓存。它不需要调用者知道缓存元素怎样被返回,也支持在不阻塞读的情况下刷新缓存条目。
    CachingFilter:一个抽象、可扩展的cache filter。
    SimplePageCachingFilter:用于缓存基于request URI和Query String的页面。它可以根据HTTP request header的值来选择采用或者不采用gzip压缩方式将页面发到浏览器端。你可以用它来缓存整个Servlet页面,无论你采用的是JSP、velocity,或者其他的页面渲染技术。
    SimplePageFragmentCachingFilter:缓存页面片段,基于request URI和Query String。在JSP中使用jsp:include标签包含。
    已经使用Orion和Tomcat测试过,兼容Servlet 2.3、Servlet 2.4规范。
    Cacheable命令:这是一种老的命令行模式,支持异步行为、容错。
    兼容Hibernate,兼容Google App Engine。
    基于JTA的事务支持,支持事务资源管理,二阶段提交和回滚,以及本地事务。

  • 10、开源协议
    Apache 2.0 license

Ehcache的加载模块列表

他们都是独立的库,每个都为Ehcache添加新的功能
ehcache-core:API,标准缓存引擎,RMI复制和Hibernate支持
ehcache:分布式Ehcache,包括Ehcache的核心和Terracotta的库
ehcache-monitor:企业级监控和管理
ehcache-web:为Java Servlet Container提供缓存、gzip压缩支持的filters
ehcache-jcache:JSR107 JCACHE的实现
ehcache-jgroupsreplication:使用JGroup的复制
ehcache-jmsreplication:使用JMS的复制
ehcache-openjpa:OpenJPA插件
ehcache-server:war内部署或者单独部署的RESTful cache server
ehcache-unlockedreadsview:允许Terracotta cache的无锁读
ehcache-debugger:记录RMI分布式调用事件
Ehcache for Ruby:Jruby and Rails支持

结构模块

核心定义

cache manager:缓存管理器,以前是只允许单例的,不过现在也可以多实例了
cache:缓存管理器内可以放置若干cache,存放数据的实质,所有cache都实现了Ehcache接口
element:单条缓存数据的组成单位
system of record(SOR):可以取到真实数据的组件,可以是真正的业务逻辑、外部接口调用、存放真实数据的数据库等等,缓存就是从SOR中读取或者写入到SOR中去的。

代码示例:

CacheManager manager = CacheManager.newInstance("src/config/ehcache.xml");  
manager.addCache("testCache");  
Cache test = singletonManager.getCache("testCache");  
test.put(new Element("key1", "value1"));  
manager.shutdown(); 

也支持这种类似DSL的配置方式,配置都是可以在运行时动态修改的:

Cache testCache = new Cache(  
  new CacheConfiguration("testCache", maxElements)  
    .memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LFU)  
    .overflowToDisk(true)  
    .eternal(false)  
    .timeToLiveSeconds(60)  
    .timeToIdleSeconds(30)  
    .diskPersistent(false)  
    .diskExpiryThreadIntervalSeconds(0));

事务的例子:

Ehcache cache = cacheManager.getEhcache("xaCache");  
transactionManager.begin();  
try {  
    Element e = cache.get(key);  
    Object result = complexService.doStuff(element.getValue());  
    cache.put(new Element(key, result));  
    complexService.doMoreStuff(result);  
    transactionManager.commit();  
} catch (Exception e) {  
    transactionManager.rollback();  
}  

一致性模型:

说到一致性,数据库的一致性是怎样的?不妨先来回顾一下数据库的几个隔离级别:

  • 未提交读(Read Uncommitted):在读数据时不会检查或使用任何锁。因此,在这种隔离级别中可能读取到没有提交的数据。会出现脏读、不可重复读、幻象读。
  • 已提交读(Read Committed):只读取提交的数据并等待其他事务释放排他锁。读数据的共享锁在读操作完成后立即释放。已提交读是数据库的默认隔离级别。会出现不可重复读、幻象读。
  • 可重复读(Repeatable Read):像已提交读级别那样读数据,但会保持共享锁直到事务结束。会出现幻象读。
  • 可序列化(Serializable):工作方式类似于可重复读。但它不仅会锁定受影响的数据,还会锁定这个范围,这就阻止了新数据插入查询所涉及的范围。

基于以上,再来对比思考下面的一致性模型:

  • 1、强一致性模型:系统中的某个数据被成功更新(事务成功返回)后,后续任何对该数据的读取操作都得到更新后的值。这是传统关系数据库提供的一致性模型,也是关系数据库深受人们喜爱的原因之一。强一致性模型下的性能消耗通常是最大的。

  • 2、弱一致性模型:系统中的某个数据被更新后,后续对该数据的读取操作得到的不一定是更新后的值,这种情况下通常有个“不一致性时间窗口”存在:即数据更新完成后在经过这个时间窗口,后续读取操作就能够得到更新后的值。

  • 3、最终一致性模型:属于弱一致性的一种,即某个数据被更新后,如果该数据后续没有被再次更新,那么最终所有的读取操作都会返回更新后的值。

最终一致性模型包含如下几个必要属性,都比较好理解:

  • 读写一致:某线程A,更新某条数据以后,后续的访问全部都能取得更新后的数据。
  • 会话内一致:它本质上和上面那一条是一致的,某用户更改了数据,只要会话还存在,后续他取得的所有数据都必须是更改后的数据。
  • 单调读一致:如果一个进程可以看到当前的值,那么后续的访问不能返回之前的值。
  • 单调写一致:对同一进程内的写行为必须是保序的,否则,写完毕的结果就是不可预期的了。

这样几个API也会影响到一致性的结果:

1、显式锁(Explicit Locking):如果我们本身就配置为强一致性,那么自然所有的缓存操作都具备事务性质。而如果我们配置成最终一致性时,再在外部使用显式锁API,也可以达到事务的效果。当然这样的锁可以控制得更细粒度,但是依然可能存在竞争和线程阻塞。

2、无锁可读取视图(UnlockedReadsView):一个允许脏读的decorator,它只能用在强一致性的配置下,它通过申请一个特殊的写锁来比完全的强一致性配置提升性能。
使用UnlockedReadsView:

Cache cache = cacheManager.getEhcache("myCache");  
UnlockedReadsView unlockedReadsView = new UnlockedReadsView(cache, "myUnlockedCache");

3、原子方法(Atomic methods):方法执行是原子化的,即CAS操作(Compare and Swap)。CAS最终也实现了强一致性的效果,但不同的是,它是采用乐观锁而不是悲观锁来实现的。在乐观锁机制下,更新的操作可能不成功,因为在这过程中可能会有其他线程对同一条数据进行变更,那么在失败后需要重新执行更新操作。现代的CPU都支持CAS原语了。

cache.putIfAbsent(Element element);  
cache.replace(Element oldOne, Element newOne);  
cache.remove(Element);  

缓存拓扑类型:

  • 1、独立缓存(Standalone Ehcache):这样的缓存应用节点都是独立的,互相不通信。
  • 2、分布式缓存(Distributed Ehcache):数据存储在Terracotta的服务器阵列(Terracotta Server Array,TSA)中,但是最近使用的数据,可以存储在各个应用节点中。
    逻辑视角:

L1缓存就在各个应用节点上,而L2缓存则放在Cache Server阵列中。

组网视角:

模型存储视角:

L1级缓存是没有持久化存储的。另外,从缓存数据量上看,server端远大于应用节点。

复制式缓存(Replicated Ehcache)

缓存数据时同时存放在多个应用节点的,数据复制和失效的事件以同步或者异步的形式在各个集群节点间传播。上述事件到来时,会阻塞写线程的操作。在这种模式下,只有弱一致性模型。

它有如下几种事件传播机制:RMI、JGroups、JMS和Cache Server

  • RMI模式下,所有节点全部对等:
  • JGroup模式:可以配置单播或者多播,协议栈和配置都非常灵活。
  • JMS模式:这种模式的核心就是一个消息队列,每个应用节点都订阅预先定义好的主题,同时,节点有元素更新时,也会发布更新元素到主题中去。JMS规范实现者上,Open MQ和Active MQ这两个,Ehcache的兼容性都已经测试过。
  • Cache Server模式:这种模式下存在主从节点,通信可以通过RESTful的API或者SOAP。

无论上面哪个模式,更新事件又可以分为updateViaCopy或updateViaInvalidate,后者只是发送一个过期消息,效率要高得多。
复制式缓存容易出现数据不一致的问题,如果这成为一个问题,可以考虑使用数据同步分发的机制。
即便不采用分布式缓存和复制式缓存,依然会出现一些不好的行为,比如:

  • 缓存漂移(Cache Drift):每个应用节点只管理自己的缓存,在更新某个节点的时候,不会影响到其他的节点,这样数据之间可能就不同步了。这在web会话数据缓存中情况尤甚。

  • 数据库瓶颈(Database Bottlenecks ):对于单实例的应用来说,缓存可以保护数据库的读风暴;但是,在集群的环境下,每一个应用节点都要定期保持数据最新,节点越多,要维持这样的情况对数据库的开销也越大。

存储方式

  • 1、堆内存储:速度快,但是容量有限。

  • 2、堆外(OffHeapStore)存储:被称为BigMemory,只在企业版本的Ehcache中提供,原理是利用nio的DirectByteBuffers实现,比存储到磁盘上快,而且完全不受GC的影响,可以保证响应时间的稳定性;但是direct buffer的在分配上的开销要比heap buffer大,而且要求必须以字节数组方式存储,因此对象必须在存储过程中进行序列化,读取则进行反序列化操作,它的速度大约比堆内存储慢一个数量级。
    (注:direct buffer不受GC影响,但是direct buffer归属的的JAVA对象是在堆上且能够被GC回收的,一旦它被回收,JVM将释放direct buffer的堆外空间。)

  • 3、磁盘存储,配备SSD。

缓存使用模式

  • cache-aside:直接操作。先询问cache某条缓存数据是否存在,存在的话直接从cache中返回数据,绕过SOR;如果不存在,从SOR中取得数据,然后再放入cache中。
public V readSomeData(K key)   
{  
   Element element;  
   if ((element = cache.get(key)) != null) {  
       return element.getValue();  
   }  
   if (value = readDataFromDataStore(key)) != null) {  
       cache.put(new Element(key, value));  
   }   
   return value;  
}  
  • cache-as-sor:结合了read-through、write-through或write-behind操作,通过给SOR增加了一层代理,对外部应用访问来说,它不用区别数据是从缓存中还是从SOR中取得的。
    read-through。
    write-through。
    write-behind(write-back):既将写的过程变为异步的,又进一步延迟写入数据的过程。

  • Copy Cache的两个模式:CopyOnRead和CopyOnWrite。

    • CopyOnRead指的是在读缓存数据的请求到达时,如果发现数据已经过期,需要重新从源处获取,发起的copy element的操作(pull);
    • CopyOnWrite则是发生在真实数据写入缓存时,发起的更新其他节点的copy element的操作(push)。

前者适合在不允许多个线程访问同一个element的时候使用,后者则允许你自由控制缓存更新通知的时机。

多种配置方式

包括配置文件、声明式配置、编程式配置,甚至通过指定构造器的参数来完成配置,配置设计的原则包括:
所有配置要放到一起
缓存的配置可以很容易在开发阶段、运行时修改
错误的配置能够在程序启动时发现,在运行时修改出错则需要抛出运行时异常
提供默认配置,几乎所有的配置都是可选的,都有默认值

自动资源控制(Automatic Resource Control,ARC)

内存内缓存对象大小的控制,避免OOM出现
池化(cache manager级别)的缓存大小获取,避免单独计算缓存大小的消耗
灵活的独立基于层的大小计算能力,下图中可以看到,不同层的大小都是可以单独控制的
可以统计字节大小、缓存条目数和百分比
优化高命中数据的获取,以提升性能,参见下面对缓存数据在不同层之间的流转的介绍

数据流转生命周期

缓存数据的流转包括了这样几种行为:
Flush:缓存条目向低层次移动。
Fault:从低层拷贝一个对象到高层。在获取缓存的过程中,某一层发现自己的该缓存条目已经失效,就触发了Fault行为。
Eviction:把缓存条目除去。
Expiration:失效状态。
Pinning:强制缓存条目保持在某一层。
下面的图反映了数据在各个层之间的流转,也反映了数据的生命周期

监控功能

每个应用节点部署一个监控探针,通过TCP协议与监控服务器联系,最终将数据提供给富文本客户端或者监控操作服务器。


广域网复制

缓存数据复制方面,Ehcache允许两个地理位置各异的节点在广域网下维持数据一致性,同时它提供了这样几种方案

  • 第一种方案:Terracotta Active/Mirror Replication。

这种方案下,服务端包含一个活跃节点,一个备份节点;各个应用节点全部靠该活跃节点提供读写服务。这种方式最简单,管理容易;但是,需要寄希望于理想的网络状况,服务器之间和客户端到服务器之间都存在走WAN的情况,这样的方案其实最不稳定。

  • 第二种方案:Transactional Cache Manager Replication。

这种方案下,数据读取不需要经过WAN,写入数据时写入两份,分别由两个cache manager处理,一份在本地Server,一份到其他Server去。这种方案下读的吞吐量较高而且延迟较低;但是需要引入一个XA事务管理器,两个cache manager写两份数据导致写开销较大,而且过WAN的写延迟依然可能导致系统响应的瓶颈。

  • 第三种方案:Messaging based (AMQ) replication。

这种方案下,引入了批量处理和队列,用以减缓WAN的瓶颈出现,同时,把处理读请求和复制逻辑从Server Array物理上就剥离开,避免了WAN情况恶化对节点读取业务的影响。这种方案要较高的吞吐量和较低的延迟,读/复制的分离保证了可以提供完备的消息分发保证、冲突处理等特性;但是它较为复杂,而且还需要一个消息总线。

Ehcache的性能比对

the time taken for 10,000 puts, gets and removes, for 10,000 cache items. 下面这张图来自Ehcache的创始人Greg Luck的blog

put/get上Ehcache要500-1000倍快过Memcached。原因何在?他自己分析道:“In-process caching and asynchronous replication are a clear performance winner”。有关它详细的内容还是请参阅他的blog吧。

cache元素的属性

name:缓存名称

maxElementsInMemory:内存中最大缓存对象数

maxElementsOnDisk:硬盘中最大缓存对象数,若是0表示无穷大

eternal:true表示对象永不过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false

overflowToDisk:true表示当内存缓存的对象数目达到了maxElementsInMemory界限后,会把溢出的对象写到硬盘缓存中。注意:如果缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。

diskSpoolBufferSizeMB:磁盘缓存区大小,默认为30MB。每个Cache都应该有自己的一个缓存区。

diskPersistent:是否缓存虚拟机重启期数据,是否持久化磁盘缓存,当这个属性的值为true时,系统在初始化时会在磁盘中查找文件名 为cache名称,后缀名为index的文件,这个文件中存放了已经持久化在磁盘中的cache的index,找到后会把cache加载到内存,要想把 cache真正持久化到磁盘,写程序时注意执行net.sf.ehcache.Cache.put(Element element)后要调用flush()方法。

diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认为120秒

timeToIdleSeconds: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,如果处于空闲状态的时间超过了timeToIdleSeconds属性 值,这个对象就会过期,EHCache将把它从缓存中清空。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限 期地处于空闲状态

timeToLiveSeconds:设定对象允许存在于缓存中的最长时间,以秒为单位。当对象自从被存放到缓存中后,如果处于缓存中的时间超过了 timeToLiveSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清除。只有当eternal属性为false,该属性才有 效。如果该属性值为0,则表示对象可以无限期地存在于缓存中。timeToLiveSeconds必须大于timeToIdleSeconds属性,才有 意义

memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。

Ref:
http://raychase.iteye.com/blog/1549570

推荐阅读更多精彩内容

  • 记录是一种精神,是加深理解最好的方式之一。 最近看了下Mybatis的源码,分析了二级缓存的实现方式,在这里把他记...
    曹金桂阅读 6,756评论 8 30
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 89,600评论 13 124
  • 一、简介 Ehcache是一个用Java实现的使用简单,高速,实现线程安全的缓存管理类库,ehcache提供了用内...
    小程故事多阅读 31,931评论 11 52
  • Ehcache是现在最流行的纯Java开源缓存框架,配置简单、结构清晰、功能强大,最初知道它,是从Hibernat...
    安易学车阅读 1,303评论 0 10
  • 原文连接:https://my.oschina.net/coolfire368/blog/123377 ehcac...
    晴天哥_374阅读 505评论 0 1