Ehcache原理详细解读

2022-05-11  本文已影响0人  上善若泪

1 Ehcache

Ehcache是现在最流行的纯Java开源缓存框架,配置简单、结构清晰、功能强大,最初知道它,是从Hibernate的缓存开始的。网上中文的EhCache材料以简单介绍和配置方法居多,对于API,官网上介绍已经非常清楚,请参见官网;但是很少见到特性说明和对实现原理的分析,因此在这篇文章里面,详细介绍和分析EhCache的特性,加上一些自己的理解和思考,希望对缓存感兴趣的朋友有所收获。

1.1 特性

1.1.1 快速轻量

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

1.1.2 伸缩性

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

1.1.3 灵活性

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

1.1.4 标准支持

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

1.1.5 可扩展性

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

1.1.6 应用持久化

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

1.1.7 监听器

缓存管理器监听器。允许注册实现了CacheManagerEventListener接口的监听器:

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

1.1.8 开启JMX

EhcacheJMX功能是默认开启的,你可以监控和管理如下的MBean:
CacheManager、Cache、CacheConfiguration、CacheStatistics

1.1.9 分布式缓存

从Ehcache 1.2开始,支持高性能的分布式缓存,兼具灵活性和扩展性。
分布式缓存的选项包括:

1.1.10 Java EE和应用缓存

为普通缓存场景和模式提供高质量的实现。

1.2 模块列表

Ehcache的加载模块列表,他们都是独立的库,每个都为Ehcache添加新的功能 :

Ehcache的结构设计概览:


image.png

1.3 核心定义

核心定义 :

代码示例:

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();  
}  

1.4 一致性模型

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

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

  1. 强一致性模型:系统中的某个数据被成功更新(事务成功返回)后,后续任何对该数据的读取操作都得到更新后的值。这是传统关系数据库提供的一致性模型,也是关系数据库深受人们喜爱的原因之一。强一致性模型下的性能消耗通常是最大的
  2. 弱一致性模型:系统中的某个数据被更新后,后续对该数据的读取操作得到的不一定是更新后的值,这种情况下通常有个不一致性时间窗口存在:即数据更新完成后在经过这个时间窗口,后续读取操作就能够得到更新后的值。
  3. 最终一致性模型:属于弱一致性的一种,即某个数据被更新后,如果该数据后续没有被再次更新,那么最终所有的读取操作都会返回更新后的值。

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

  1. Bulk Load:这种模型是基于批量加载数据到缓存里面的场景而优化的,没有引入锁和常规的淘汰算法这些降低性能的东西,它和最终一致性模型很像,但是有批量、高速写和弱一致性保证的机制。

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

  1. 显式锁(Explicit Locking ):如果我们本身就配置为强一致性,那么自然所有的缓存操作都具备事务性质。而如果我们配置成最终一致性时,再在外部使用显式锁API,也可以达到事务的效果。当然这样的锁可以控制得更细粒度,但是依然可能存在竞争和线程阻塞。
  2. 无锁可读取视图(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");  
  1. 原子方法(Atomic methods):方法执行是原子化的,即CAS操作(Compare and Swap)。CAS最终也实现了强一致性的效果,但不同的是,它是采用乐观锁而不是悲观锁来实现的。在乐观锁机制下,更新的操作可能不成功,因为在这过程中可能会有其他线程对同一条数据进行变更,那么在失败后需要重新执行更新操作。现代的CPU都支持CAS原语了。
cache.putIfAbsent(Element element);  
cache.replace(Element oldOne, Element newOne);  
cache.remove(Element);  

1.5 缓存拓扑类型

  1. 独立缓存(Standalone Ehcache):这样的缓存应用节点都是独立的,互相不通信。
  2. 分布式缓存(Distributed Ehcache):数据存储在Terracotta的服务器阵列(Terracotta Server Array,TSA)中,但是最近使用的数据,可以存储在各个应用节点中
  3. 复制式缓存(Replicated Ehcache):缓存数据时同时存放在多个应用节点的,数据复制和失效的事件以同步或者异步的形式在各个集群节点间传播。上述事件到来时,会阻塞写线程的操作。在这种模式下,只有弱一致性模型。
    它有如下几种事件传播机制:RMI、JGroups、JMS和Cache Server。
    RMI模式下,所有节点全部对等:
    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,后者只是发送一个过期消息,效率要高得多。
复制式缓存容易出现数据不一致的问题,如果这成为一个问题,可以考虑使用数据同步分发的机制。

即便不采用分布式缓存和复制式缓存,依然会出现一些不好的行为,比如:

1.6 存储方式

存储方式:

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

1.7 缓存使用模式

  1. 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;  
}  
  1. cache-as-sor:结合了read-through、write-through或write-behind操作,通过给SOR增加了一层代理,对外部应用访问来说,它不用区别数据是从缓存中还是从SOR中取得的。
  2. read-through
  3. write-through
  4. write-behind(write-back):既将写的过程变为异步的,又进一步延迟写入数据的过程。

Copy Cache的两个模式:CopyOnReadCopyOnWrite

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

1.8 多种配置方式

包括配置文件、声明式配置、编程式配置,甚至通过指定构造器的参数来完成配置,配置设计的原则包括:

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

它是提供了一种智能途径来控制缓存,调优性能。特性包括:

缓存数据的流转包括了这样几种行为:

上一篇 下一篇

猜你喜欢

热点阅读