Ehcache原理詳細解讀

1 Ehcache

Ehcache是現(xiàn)在最流行的純Java開源緩存框架,配置簡單、結(jié)構(gòu)清晰、功能強大,最初知道它,是從Hibernate的緩存開始的。網(wǎng)上中文的EhCache材料以簡單介紹和配置方法居多,對于API,官網(wǎng)上介紹已經(jīng)非常清楚,請參見官網(wǎng);但是很少見到特性說明和對實現(xiàn)原理的分析,因此在這篇文章里面,詳細介紹和分析EhCache的特性,加上一些自己的理解和思考,希望對緩存感興趣的朋友有所收獲。

1.1 特性

1.1.1 快速輕量

過去幾年,諸多測試表明Ehcache是最快的Java緩存之一。
Ehcache的線程機制是為大型高并發(fā)系統(tǒng)設計的。
大量性能測試用例保證Ehcache在不同版本間性能表現(xiàn)得一致性。
很多用戶都不知道他們正在用Ehcache,因為不需要什么特別的配置。
API易于使用,這就很容易部署上線和運行。
很小的jar包,Ehcache 2.2.3才668kb。
最小的依賴:唯一的依賴就是SLF4J了。

1.1.2 伸縮性

緩存在內(nèi)存和磁盤存儲可以伸縮到數(shù)G,Ehcache為大數(shù)據(jù)存儲做過優(yōu)化。
大內(nèi)存的情況下,所有進程可以支持數(shù)百G的吞吐。
為高并發(fā)和大型多CPU服務器做優(yōu)化。
線程安全和性能總是一對矛盾,Ehcache的線程機制設計采用了Doug Lea的想法來獲得較高的性能。
單臺虛擬機上支持多緩存管理器。
通過Terracotta服務器矩陣,可以伸縮到數(shù)百個節(jié)點。

1.1.3 靈活性

Ehcache 1.2具備對象API接口和可序列化API接口。
不能序列化的對象可以使用除磁盤存儲外Ehcache的所有功能。
除了元素的返回方法以外,API都是統(tǒng)一的。只有這兩個方法不一致:getObjectValuegetKeyValue。這就使得緩存對象、序列化對象來獲取新的特性這個過程很簡單。
支持基于Cache和基于Element的過期策略,每個Cache的存活時間都是可以設置和控制的。
提供了LRU、LFU和FIFO緩存淘汰算法,Ehcache 1.2引入了最少使用和先進先出緩存淘汰算法,構(gòu)成了完整的緩存淘汰算法。
提供內(nèi)存和磁盤存儲,Ehcache和大多數(shù)緩存解決方案一樣,提供高性能的內(nèi)存和磁盤存儲。
動態(tài)、運行時緩存配置,存活時間、空閑時間、內(nèi)存和磁盤存放緩存的最大數(shù)目都是可以在運行時修改的。

1.1.4 標準支持

Ehcache提供了對JSR107 JCACHE API最完整的實現(xiàn)。因為JCACHE在發(fā)布以前,Ehcache的實現(xiàn)(如net.sf.jsr107cache)已經(jīng)發(fā)布了。
實現(xiàn)JCACHE API有利于到未來其他緩存解決方案的可移植性。
Ehcache的維護者Greg Luck,正是JSR107的專家委員會委員。

1.1.5 可擴展性

監(jiān)聽器可以插件化。Ehcache 1.2提供了CacheManagerEventListenerCacheEventListener接口,實現(xiàn)可以插件化,并且可以在ehcache.xml里配置。
節(jié)點發(fā)現(xiàn),冗余器和監(jiān)聽器都可以插件化。
分布式緩存,從Ehcache 1.2開始引入,包含了一些權(quán)衡的選項。Ehcache的團隊相信沒有什么是萬能的配置。
實現(xiàn)者可以使用內(nèi)建的機制或者完全自己實現(xiàn),因為有完整的插件開發(fā)指南。
緩存的可擴展性可以插件化。創(chuàng)建你自己的緩存擴展,它可以持有一個緩存的引用,并且綁定在緩存的生命周期內(nèi)。
緩存加載器可以插件化。創(chuàng)建你自己的緩存加載器,可以使用一些異步方法來加載數(shù)據(jù)到緩存里面。
緩存異常處理器可以插件化。創(chuàng)建一個異常處理器,在異常發(fā)生的時候,可以執(zhí)行某些特定操作。

1.1.6 應用持久化

在VM重啟后,持久化到磁盤的存儲可以復原數(shù)據(jù)。
Ehcache是第一個引入緩存數(shù)據(jù)持久化存儲的開源Java緩存框架。緩存的數(shù)據(jù)可以在機器重啟后從磁盤上重新獲得。
根據(jù)需要將緩存刷到磁盤。將緩存條目刷到磁盤的操作可以通過cache.flush()方法來執(zhí)行,這大大方便了Ehcache的使用

1.1.7 監(jiān)聽器

緩存管理器監(jiān)聽器。允許注冊實現(xiàn)了CacheManagerEventListener接口的監(jiān)聽器:

  • notifyCacheAdded()
  • notifyCacheRemoved()

緩存事件監(jiān)聽器。允許注冊實現(xiàn)了CacheEventListener接口的監(jiān)聽器,它提供了許多對緩存事件發(fā)生后的處理機制:
notifyElementRemoved/Put/Updated/Expired

1.1.8 開啟JMX

EhcacheJMX功能是默認開啟的,你可以監(jiān)控和管理如下的MBean:
CacheManager、Cache、CacheConfiguration、CacheStatistics

1.1.9 分布式緩存

從Ehcache 1.2開始,支持高性能的分布式緩存,兼具靈活性和擴展性。
分布式緩存的選項包括:

  • 通過Terracotta的緩存集群:設定和使用Terracotta模式的Ehcache緩存。緩存發(fā)現(xiàn)是自動完成的,并且有很多選項可以用來調(diào)試緩存行為和性能。
    使用RMI、JGroups或者JMS來冗余緩存數(shù)據(jù):節(jié)點可以通過多播或發(fā)現(xiàn)者手動配置。狀態(tài)更新可以通過RMI連接來異步或者同步完成。
  • Custom:一個綜合的插件機制,支持發(fā)現(xiàn)和復制的能力。
    可用的緩存復制選項。支持的通過RMI、JGroups或JMS進行的異步或同步的緩存復制。
  • 可靠的分發(fā):使用TCP的內(nèi)建分發(fā)機制。
  • 節(jié)點發(fā)現(xiàn):節(jié)點可以手動配置或者使用多播自動發(fā)現(xiàn),并且可以自動添加和移除節(jié)點。對于多播阻塞的情況下,手動配置可以很好地控制。
    分布式緩存可以任意時間加入或者離開集群。緩存可以配置在初始化的時候執(zhí)行引導程序員。
    BootstrapCacheLoaderFactory抽象工廠,實現(xiàn)了BootstrapCacheLoader接口(RMI實現(xiàn))。
    緩存服務端。Ehcache提供了一個Cache Server,一個war包,為絕大多數(shù)web容器或者是獨立的服務器提供支持。
  • 緩存服務端有兩組API:面向資源的RESTful,還有就是SOAP??蛻舳藳]有實現(xiàn)語言的限制。
  • RESTful緩存服務器:Ehcached的實現(xiàn)嚴格遵循RESTful面向資源的架構(gòu)風格。
  • SOAP緩存服務端:Ehcache RESTFul Web Services API暴露了單例的CacheManager,他能在ehcache.xml或者IoC容器里面配置。
    標準服務端包含了內(nèi)嵌的Glassfish web容器。它被打成了war包,可以任意部署到支持Servlet 2.5的web容器內(nèi)。Glassfish V2/3、Tomcat 6和Jetty 6都已經(jīng)經(jīng)過了測試。

1.1.10 Java EE和應用緩存

為普通緩存場景和模式提供高質(zhì)量的實現(xiàn)。

  • 阻塞緩存:它的機制避免了復制進程并發(fā)操作的問題。
    SelfPopulatingCache在緩存一些開銷昂貴操作時顯得特別有用,它是一種針對讀優(yōu)化的緩存。它不需要調(diào)用者知道緩存元素怎樣被返回,也支持在不阻塞讀的情況下刷新緩存條目。
  • CachingFilter:一個抽象、可擴展的cache filter。
  • SimplePageCachingFilter:用于緩存基于request URI和Query String的頁面。它可以根據(jù)HTTP request header的值來選擇采用或者不采用gzip壓縮方式將頁面發(fā)到瀏覽器端。你可以用它來緩存整個Servlet頁面,無論你采用的是JSP、velocity,或者其他的頁面渲染技術(shù)。
  • SimplePageFragmentCachingFilter:緩存頁面片段,基于request URI和Query String。在JSP中使用jsp:include標簽包含。
    已經(jīng)使用Orion和Tomcat測試過,兼容Servlet 2.3、Servlet 2.4規(guī)范。
  • Cacheable命令:這是一種老的命令行模式,支持異步行為、容錯。
    兼容Hibernate,兼容Google App Engine。
    基于JTA的事務支持,支持事務資源管理,二階段提交和回滾,以及本地事務

1.2 模塊列表

Ehcache的加載模塊列表,他們都是獨立的庫,每個都為Ehcache添加新的功能 :

  • ehcache-core:API,標準緩存引擎,RMI復制和Hibernate支持
  • ehcache:分布式Ehcache,包括Ehcache的核心和Terracotta的庫
  • ehcache-monitor:企業(yè)級監(jiān)控和管理
  • ehcache-web:為Java Servlet Container提供緩存、gzip壓縮支持的filters
  • ehcache-jcache:JSR107 JCACHE的實現(xiàn)
  • ehcache-jgroupsreplication:使用JGroup的復制
  • ehcache-jmsreplication:使用JMS的復制
  • ehcache-openjpa:OpenJPA插件
  • ehcache-server:war內(nèi)部署或者單獨部署的RESTful cache server
  • ehcache-unlockedreadsview:允許Terracotta cache的無鎖讀
  • ehcache-debugger:記錄RMI分布式調(diào)用事件
  • Ehcache for Ruby:Jruby and Rails支持

Ehcache的結(jié)構(gòu)設計概覽:


image.png

1.3 核心定義

核心定義 :

  • cache manager:緩存管理器,以前是只允許單例的,不過現(xiàn)在也可以多實例了
  • cache:緩存管理器內(nèi)可以放置若干cache,存放數(shù)據(jù)的實質(zhì),所有cache都實現(xiàn)了Ehcache接口
  • element:單條緩存數(shù)據(jù)的組成單位
  • system of record(SOR):可以取到真實數(shù)據(jù)的組件,可以是真正的業(yè)務邏輯、外部接口調(diào)用、存放真實數(shù)據(jù)的數(shù)據(jù)庫等等,緩存就是從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的配置方式,配置都是可以在運行時動態(tài)修改的:
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 一致性模型

說到一致性,數(shù)據(jù)庫的一致性是怎樣的?不妨先來回顧一下數(shù)據(jù)庫的幾個隔離級別:

  • 未提交讀(Read Uncommitted):在讀數(shù)據(jù)時不會檢查或使用任何鎖。因此,在這種隔離級別中可能讀取到?jīng)]有提交的數(shù)據(jù)。會出現(xiàn)臟讀、不可重復讀、幻象讀。
  • 已提交讀(Read Committed):只讀取提交的數(shù)據(jù)并等待其他事務釋放排他鎖。讀數(shù)據(jù)的共享鎖在讀操作完成后立即釋放。已提交讀是數(shù)據(jù)庫的默認隔離級別。會出現(xiàn)不可重復讀、幻象讀。
  • 可重復讀(Repeatable Read):像已提交讀級別那樣讀數(shù)據(jù),但會保持共享鎖直到事務結(jié)束。會出現(xiàn)幻象讀。
  • 可序列化(Serializable):工作方式類似于可重復讀。但它不僅會鎖定受影響的數(shù)據(jù),還會鎖定這個范圍,這就阻止了新數(shù)據(jù)插入查詢所涉及的范圍。

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

  1. 強一致性模型:系統(tǒng)中的某個數(shù)據(jù)被成功更新(事務成功返回)后,后續(xù)任何對該數(shù)據(jù)的讀取操作都得到更新后的值。這是傳統(tǒng)關系數(shù)據(jù)庫提供的一致性模型,也是關系數(shù)據(jù)庫深受人們喜愛的原因之一。強一致性模型下的性能消耗通常是最大的
  2. 弱一致性模型:系統(tǒng)中的某個數(shù)據(jù)被更新后,后續(xù)對該數(shù)據(jù)的讀取操作得到的不一定是更新后的值,這種情況下通常有個不一致性時間窗口存在:即數(shù)據(jù)更新完成后在經(jīng)過這個時間窗口,后續(xù)讀取操作就能夠得到更新后的值。
  3. 最終一致性模型:屬于弱一致性的一種,即某個數(shù)據(jù)被更新后,如果該數(shù)據(jù)后續(xù)沒有被再次更新,那么最終所有的讀取操作都會返回更新后的值。

最終一致性模型包含如下幾個必要屬性,都比較好理解:

  • 讀寫一致:某線程A,更新某條數(shù)據(jù)以后,后續(xù)的訪問全部都能取得更新后的數(shù)據(jù)
  • 會話內(nèi)一致:它本質(zhì)上和上面那一條是一致的,某用戶更改了數(shù)據(jù),只要會話還存在,后續(xù)他取得的所有數(shù)據(jù)都必須是更改后的數(shù)據(jù)。
  • 單調(diào)讀一致:如果一個進程可以看到當前的值,那么后續(xù)的訪問不能返回之前的值
  • 單調(diào)寫一致:對同一進程內(nèi)的寫行為必須是保序的,否則,寫完畢的結(jié)果就是不可預期的了
  1. Bulk Load:這種模型是基于批量加載數(shù)據(jù)到緩存里面的場景而優(yōu)化的,沒有引入鎖和常規(guī)的淘汰算法這些降低性能的東西,它和最終一致性模型很像,但是有批量、高速寫和弱一致性保證的機制。

這樣幾個API也會影響到一致性的結(jié)果:

  1. 顯式鎖(Explicit Locking ):如果我們本身就配置為強一致性,那么自然所有的緩存操作都具備事務性質(zhì)。而如果我們配置成最終一致性時,再在外部使用顯式鎖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):方法執(zhí)行是原子化的,即CAS操作(Compare and Swap)。CAS最終也實現(xiàn)了強一致性的效果,但不同的是,它是采用樂觀鎖而不是悲觀鎖來實現(xiàn)的。在樂觀鎖機制下,更新的操作可能不成功,因為在這過程中可能會有其他線程對同一條數(shù)據(jù)進行變更,那么在失敗后需要重新執(zhí)行更新操作。現(xiàn)代的CPU都支持CAS原語了。
cache.putIfAbsent(Element element);  
cache.replace(Element oldOne, Element newOne);  
cache.remove(Element);  

1.5 緩存拓撲類型

  1. 獨立緩存(Standalone Ehcache):這樣的緩存應用節(jié)點都是獨立的,互相不通信。
  2. 分布式緩存(Distributed Ehcache):數(shù)據(jù)存儲在Terracotta的服務器陣列(Terracotta Server Array,TSA)中,但是最近使用的數(shù)據(jù),可以存儲在各個應用節(jié)點中
  3. 復制式緩存(Replicated Ehcache):緩存數(shù)據(jù)時同時存放在多個應用節(jié)點的,數(shù)據(jù)復制和失效的事件以同步或者異步的形式在各個集群節(jié)點間傳播。上述事件到來時,會阻塞寫線程的操作。在這種模式下,只有弱一致性模型。
    它有如下幾種事件傳播機制:RMI、JGroups、JMS和Cache Server。
    RMI模式下,所有節(jié)點全部對等:
    JGroup模式:可以配置單播或者多播,協(xié)議棧和配置都非常靈活。
<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模式:這種模式的核心就是一個消息隊列,每個應用節(jié)點都訂閱預先定義好的主題,同時,節(jié)點有元素更新時,也會發(fā)布更新元素到主題中去。JMS規(guī)范實現(xiàn)者上,Open MQ和Active MQ這兩個,Ehcache的兼容性都已經(jīng)測試過。

Cache Server模式:這種模式下存在主從節(jié)點,通信可以通過RESTful的API或者SOAP。

無論上面哪個模式,更新事件又可以分為updateViaCopy或updateViaInvalidate,后者只是發(fā)送一個過期消息,效率要高得多。
復制式緩存容易出現(xiàn)數(shù)據(jù)不一致的問題,如果這成為一個問題,可以考慮使用數(shù)據(jù)同步分發(fā)的機制。

即便不采用分布式緩存和復制式緩存,依然會出現(xiàn)一些不好的行為,比如:

  • 緩存漂移(Cache Drift):每個應用節(jié)點只管理自己的緩存,在更新某個節(jié)點的時候,不會影響到其他的節(jié)點,這樣數(shù)據(jù)之間可能就不同步了。這在web會話數(shù)據(jù)緩存中情況尤甚
  • 數(shù)據(jù)庫瓶頸(Database Bottlenecks):對于單實例的應用來說,緩存可以保護數(shù)據(jù)庫的讀風暴;但是,在集群的環(huán)境下,每一個應用節(jié)點都要定期保持數(shù)據(jù)最新,節(jié)點越多,要維持這樣的情況對數(shù)據(jù)庫的開銷也越大。

1.6 存儲方式

存儲方式:

  1. 堆內(nèi)存儲:速度快,但是容量有限。
  2. 堆外(OffHeapStore)存儲:被稱為BigMemory,只在企業(yè)版本的Ehcache中提供,原理是利用nioDirectByteBuffers實現(xiàn),比存儲到磁盤上快,而且完全不受GC的影響,可以保證響應時間的穩(wěn)定性;但是direct buffer的在分配上的開銷要比heap buffer大,而且要求必須以字節(jié)數(shù)組方式存儲,因此對象必須在存儲過程中進行序列化,讀取則進行反序列化操作,它的速度大約比堆內(nèi)存儲慢一個數(shù)量級。
    (注:direct buffer不受GC影響,但是direct buffer歸屬的JAVA對象是在堆上且能夠被GC回收的,一旦它被回收,JVM將釋放direct buffer的堆外空間)
  3. 磁盤存儲

1.7 緩存使用模式

  1. cache-aside:直接操作。先詢問cache某條緩存數(shù)據(jù)是否存在,存在的話直接從cache中返回數(shù)據(jù),繞過SOR;如果不存在,從SOR中取得數(shù)據(jù),然后再放入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:結(jié)合了read-through、write-through或write-behind操作,通過給SOR增加了一層代理,對外部應用訪問來說,它不用區(qū)別數(shù)據(jù)是從緩存中還是從SOR中取得的。
  2. read-through
  3. write-through
  4. write-behind(write-back):既將寫的過程變?yōu)楫惒降?,又進一步延遲寫入數(shù)據(jù)的過程。

Copy Cache的兩個模式:CopyOnReadCopyOnWrite

  • CopyOnRead指的是在讀緩存數(shù)據(jù)的請求到達時,如果發(fā)現(xiàn)數(shù)據(jù)已經(jīng)過期,需要重新從源處獲取,發(fā)起的copy element的操作(pull);
  • CopyOnWrite則是發(fā)生在真實數(shù)據(jù)寫入緩存時,發(fā)起的更新其他節(jié)點的copy element的操作(push)

前者適合在不允許多個線程訪問同一個element的時候使用,后者則允許你自由控制緩存更新通知的時機。

1.8 多種配置方式

包括配置文件、聲明式配置、編程式配置,甚至通過指定構(gòu)造器的參數(shù)來完成配置,配置設計的原則包括:

  • 所有配置要放到一起
  • 緩存的配置可以很容易在開發(fā)階段、運行時修改
  • 錯誤的配置能夠在程序啟動時發(fā)現(xiàn),在運行時修改出錯則需要拋出運行時異常
  • 提供默認配置,幾乎所有的配置都是可選的,都有默認值

1.9 自動資源控制(Automatic Resource Control,ARC)

它是提供了一種智能途徑來控制緩存,調(diào)優(yōu)性能。特性包括:

  • 內(nèi)存內(nèi)緩存對象大小的控制,避免OOM出現(xiàn)
  • 池化(cache manager級別)的緩存大小獲取,避免單獨計算緩存大小的消耗
  • 靈活的獨立基于層的大小計算能力,不同層的大小都是可以單獨控制的
  • 可以統(tǒng)計字節(jié)大小、緩存條目數(shù)和百分比
  • 優(yōu)化高命中數(shù)據(jù)的獲取,以提升性能,參見下面對緩存數(shù)據(jù)在不同層之間的流轉(zhuǎn)的介紹

緩存數(shù)據(jù)的流轉(zhuǎn)包括了這樣幾種行為:

  • Flush:緩存條目向低層次移動。
  • Fault:從低層拷貝一個對象到高層。在獲取緩存的過程中,某一層發(fā)現(xiàn)自己的該緩存條目已經(jīng)失效,就觸發(fā)了Fault行為。
  • Eviction:把緩存條目除去。
  • Expiration:失效狀態(tài)。
  • Pinning:強制緩存條目保持在某一層。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容