Ehcache
是现在最流行的纯Java
开源缓存框架,配置简单、结构清晰、功能强大,最初知道它,是从Hibernate的缓存开始的。网上中文的EhCache
材料以简单介绍和配置方法居多,对于API,官网上介绍已经非常清楚,请参见官网;但是很少见到特性说明和对实现原理的分析,因此在这篇文章里面,详细介绍和分析EhCache
的特性,加上一些自己的理解和思考,希望对缓存感兴趣的朋友有所收获。
过去几年,诸多测试表明Ehcache
是最快的Java缓存之一。
Ehcache
的线程机制是为大型高并发系统设计的。
大量性能测试用例保证Ehcache
在不同版本间性能表现得一致性。
很多用户都不知道他们正在用Ehcache
,因为不需要什么特别的配置。
API易于使用,这就很容易部署上线和运行。
很小的jar包,Ehcache 2.2.3才668kb。
最小的依赖:唯一的依赖就是SLF4J了。
缓存在内存和磁盘存储可以伸缩到数G
,Ehcache
为大数据存储做过优化。
大内存的情况下,所有进程可以支持数百G
的吞吐。
为高并发和大型多CPU
服务器做优化。
线程安全和性能总是一对矛盾,Ehcache
的线程机制设计采用了Doug Lea的想法来获得较高的性能。
单台虚拟机上支持多缓存管理器。
通过Terracotta服务器矩阵,可以伸缩到数百个节点。
Ehcache 1.2具备对象API接口和可序列化API接口。
不能序列化的对象可以使用除磁盘存储外Ehcache的所有功能。
除了元素的返回方法以外,API
都是统一的。只有这两个方法不一致:getObjectValue
和getKeyValue
。这就使得缓存对象、序列化对象来获取新的特性这个过程很简单。
支持基于Cache
和基于Element
的过期策略,每个Cache的存活时间都是可以设置和控制的。
提供了LRU、LFU和FIFO
缓存淘汰算法,Ehcache 1.2引入了最少使用和先进先出缓存淘汰算法,构成了完整的缓存淘汰算法。
提供内存和磁盘存储,Ehcache
和大多数缓存解决方案一样,提供高性能的内存和磁盘存储。
动态、运行时缓存配置,存活时间、空闲时间、内存和磁盘存放缓存的最大数目都是可以在运行时修改的。
Ehcache
提供了对JSR107 JCACHE API最完整的实现。因为JCACHE在发布以前,Ehcache
的实现(如net.sf.jsr107cache)已经发布了。
实现JCACHE API
有利于到未来其他缓存解决方案的可移植性。
Ehcache的维护者Greg Luck,正是JSR107的专家委员会委员。
监听器可以插件化。Ehcache 1.2提供了CacheManagerEventListener
和CacheEventListener
接口,实现可以插件化,并且可以在ehcache.xml
里配置。
节点发现,冗余器和监听器都可以插件化。
分布式缓存,从Ehcache 1.2开始引入,包含了一些权衡的选项。Ehcache
的团队相信没有什么是万能的配置。
实现者可以使用内建的机制或者完全自己实现,因为有完整的插件开发指南。
缓存的可扩展性可以插件化。创建你自己的缓存扩展,它可以持有一个缓存的引用,并且绑定在缓存的生命周期内。
缓存加载器可以插件化。创建你自己的缓存加载器,可以使用一些异步方法来加载数据到缓存里面。
缓存异常处理器可以插件化。创建一个异常处理器,在异常发生的时候,可以执行某些特定操作。
在VM重启后,持久化到磁盘的存储可以复原数据。
Ehcache
是第一个引入缓存数据持久化存储的开源Java缓存框架。缓存的数据可以在机器重启后从磁盘上重新获得。
根据需要将缓存刷到磁盘。将缓存条目刷到磁盘的操作可以通过cache.flush()
方法来执行,这大大方便了Ehcache
的使用
缓存管理器监听器。允许注册实现了CacheManagerEventListener
接口的监听器:
缓存事件监听器。允许注册实现了CacheEventListener
接口的监听器,它提供了许多对缓存事件发生后的处理机制:
notifyElementRemoved/Put/Updated/Expired
Ehcache
的JMX
功能是默认开启的,你可以监控和管理如下的MBean:
CacheManager、Cache、CacheConfiguration、CacheStatistics
从Ehcache 1.2开始,支持高性能的分布式缓存,兼具灵活性和扩展性。
分布式缓存的选项包括:
Terracotta
的缓存集群:设定和使用Terracotta
模式的Ehcache
缓存。缓存发现是自动完成的,并且有很多选项可以用来调试缓存行为和性能。RMI
、JGroups
或者JMS
来冗余缓存数据:节点可以通过多播或发现者手动配置。状态更新可以通过RMI连接来异步或者同步完成。Custom
:一个综合的插件机制,支持发现和复制的能力。TCP
的内建分发机制。为普通缓存场景和模式提供高质量的实现。
SelfPopulatingCache
在缓存一些开销昂贵操作时显得特别有用,它是一种针对读优化的缓存。它不需要调用者知道缓存元素怎样被返回,也支持在不阻塞读的情况下刷新缓存条目。CachingFilter
:一个抽象、可扩展的cache filter。SimplePageCachingFilter
:用于缓存基于request URI和Query String的页面。它可以根据HTTP request header的值来选择采用或者不采用gzip压缩方式将页面发到浏览器端。你可以用它来缓存整个Servlet页面,无论你采用的是JSP、velocity,或者其他的页面渲染技术。SimplePageFragmentCachingFilter
:缓存页面片段,基于request URI和Query String。在JSP中使用jsp:include标签包含。Cacheable
命令:这是一种老的命令行模式,支持异步行为、容错。Ehcache
的加载模块列表,他们都是独立的库,每个都为Ehcache
添加新的功能 :
ehcache-core
:API,标准缓存引擎,RMI复制和Hibernate支持ehcache
:分布式Ehcache,包括Ehcache的核心和Terracotta的库ehcache-monitor
:企业级监控和管理ehcache-web
:为Java Servlet Container提供缓存、gzip压缩支持的filtersehcache-jcache
:JSR107 JCACHE的实现ehcache-jgroupsreplication
:使用JGroup的复制ehcache-jmsreplication
:使用JMS的复制ehcache-openjpa
:OpenJPA插件ehcache-server
:war内部署或者单独部署的RESTful cache serverehcache-unlockedreadsview
:允许Terracotta cache的无锁读ehcache-debugger
:记录RMI分布式调用事件Ehcache for Ruby
:Jruby and Rails支持Ehcache的结构设计概览:
核心定义 :
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的配置方式,配置都是可以在运行时动态修改的:
Java代码
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
):工作方式类似于可重复读。但它不仅会锁定受影响的数据,还会锁定这个范围,这就阻止了新数据插入查询所涉及的范围。基于以上,再来对比思考下面的一致性模型:
强一致性模型
:系统中的某个数据被成功更新(事务成功返回)后,后续任何对该数据的读取操作都得到更新后的值。这是传统关系数据库提供的一致性模型,也是关系数据库深受人们喜爱的原因之一。强一致性模型下的性能消耗通常是最大的弱一致性模型
:系统中的某个数据被更新后,后续对该数据的读取操作得到的不一定是更新后的值,这种情况下通常有个不一致性时间窗口
存在:即数据更新完成后在经过这个时间窗口,后续读取操作就能够得到更新后的值。最终一致性模型
:属于弱一致性的一种,即某个数据被更新后,如果该数据后续没有被再次更新,那么最终所有的读取操作都会返回更新后的值。最终一致性模型包含如下几个必要属性,都比较好理解:
读写一致
:某线程A,更新某条数据以后,后续的访问全部都能取得更新后的数据会话内一致
:它本质上和上面那一条是一致的,某用户更改了数据,只要会话还存在,后续他取得的所有数据都必须是更改后的数据。单调读一致
:如果一个进程可以看到当前的值,那么后续的访问不能返回之前的值单调写一致
:对同一进程内的写行为必须是保序的,否则,写完毕的结果就是不可预期的了Bulk Load
:这种模型是基于批量加载数据到缓存里面的场景而优化的,没有引入锁和常规的淘汰算法这些降低性能的东西,它和最终一致性模型很像,但是有批量、高速写和弱一致性保证的机制。这样几个API也会影响到一致性的结果:
显式锁(Explicit Locking )
:如果我们本身就配置为强一致性,那么自然所有的缓存操作都具备事务性质。而如果我们配置成最终一致性时,再在外部使用显式锁API,也可以达到事务的效果。当然这样的锁可以控制得更细粒度,但是依然可能存在竞争和线程阻塞。无锁可读取视图(UnlockedReadsView)
:一个允许脏读的decorator
,它只能用在强一致性的配置下,它通过申请一个特殊的写锁来比完全的强一致性配置提升性能。xml
配置为强一致性模型:<cache name="myCache"
maxElementsInMemory="500"
eternal="false"
overflowToDisk="false"
<terracotta clustered="true" consistency="strong" />
</cache>
但是使用UnlockedReadsView:
Cache cache = cacheManager.getEhcache("myCache");
UnlockedReadsView unlockedReadsView = new UnlockedReadsView(cache, "myUnlockedCache");
原子方法(Atomic methods)
:方法执行是原子化的,即CAS操作(Compare and Swap)。CAS最终也实现了强一致性的效果,但不同的是,它是采用乐观锁而不是悲观锁来实现的。在乐观锁机制下,更新的操作可能不成功,因为在这过程中可能会有其他线程对同一条数据进行变更,那么在失败后需要重新执行更新操作。现代的CPU都支持CAS原语了。cache.putIfAbsent(Element element);
cache.replace(Element oldOne, Element newOne);
cache.remove(Element);
独立缓存(Standalone Ehcache)
:这样的缓存应用节点都是独立的,互相不通信。分布式缓存(Distributed Ehcache)
:数据存储在Terracotta
的服务器阵列(Terracotta Server Array,TSA)中,但是最近使用的数据,可以存储在各个应用节点中复制式缓存(Replicated Ehcache)
:缓存数据时同时存放在多个应用节点的,数据复制和失效的事件以同步或者异步的形式在各个集群节点间传播。上述事件到来时,会阻塞写线程的操作。在这种模式下,只有弱一致性模型。JGroup模式
:可以配置单播或者多播,协议栈和配置都非常灵活。<cacheManagerPeerProviderFactory
class="net.sf.ehcache.distribution.jgroups.JGroupsCacheManagerPeerProviderFactory"
properties="connect=UDP(mcast_addr=231.12.21.132;mcast_port=45566;):PING:
MERGE2:FD_SOCK:VERIFY_SUSPECT:pbcast.NAKACK:UNICAST:pbcast.STABLE:FRAG:pbcast.GMS"
propertySeparator="::"
/>
JMS模式:这种模式的核心就是一个消息队列,每个应用节点都订阅预先定义好的主题,同时,节点有元素更新时,也会发布更新元素到主题中去。JMS规范实现者上,Open MQ和Active MQ这两个,Ehcache的兼容性都已经测试过。
Cache Server模式:这种模式下存在主从节点,通信可以通过RESTful的API或者SOAP。
无论上面哪个模式,更新事件又可以分为updateViaCopy或updateViaInvalidate,后者只是发送一个过期消息,效率要高得多。
复制式缓存容易出现数据不一致的问题,如果这成为一个问题,可以考虑使用数据同步分发的机制。
即便不采用分布式缓存和复制式缓存,依然会出现一些不好的行为,比如:
缓存漂移(Cache Drift)
:每个应用节点只管理自己的缓存,在更新某个节点的时候,不会影响到其他的节点,这样数据之间可能就不同步了。这在web会话数据缓存中情况尤甚数据库瓶颈(Database Bottlenecks)
:对于单实例的应用来说,缓存可以保护数据库的读风暴;但是,在集群的环境下,每一个应用节点都要定期保持数据最新,节点越多,要维持这样的情况对数据库的开销也越大。存储方式:
OffHeapStore
)存储:被称为BigMemory
,只在企业版本的Ehcache
中提供,原理是利用nio
的DirectByteBuffers
实现,比存储到磁盘上快,而且完全不受GC的影响
,可以保证响应时间的稳定性;但是direct buffer
的在分配上的开销要比heap buffer
大,而且要求必须以字节数组方式
存储,因此对象必须在存储过程中进行序列化,读取则进行反序列化操作,它的速度大约比堆内存储慢一个数量级。direct buffer不受GC影响
,但是direct buffer
归属的JAVA
对象是在堆上且能够被GC
回收的,一旦它被回收,JVM
将释放direct buffer
的堆外空间)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中取得的。Copy Cache
的两个模式:CopyOnRead
和CopyOnWrite
CopyOnRead
指的是在读缓存数据的请求到达时,如果发现数据已经过期,需要重新从源处获取,发起的copy element
的操作(pull);CopyOnWrite
则是发生在真实数据写入缓存时,发起的更新其他节点的copy element
的操作(push)前者适合在不允许多个线程访问同一个element的时候使用,后者则允许你自由控制缓存更新通知的时机。
包括配置文件、声明式配置、编程式配置,甚至通过指定构造器的参数来完成配置,配置设计的原则包括:
它是提供了一种智能途径来控制缓存,调优性能。特性包括:
缓存数据的流转包括了这样几种行为:
Flush
:缓存条目向低层次移动。Fault
:从低层拷贝一个对象到高层。在获取缓存的过程中,某一层发现自己的该缓存条目已经失效,就触发了Fault行为。Eviction
:把缓存条目除去。Expiratio
n:失效状态。Pinning
:强制缓存条目保持在某一层。