ehcache是現在最流行的純java開源緩存框架,配置簡單、結構清晰、功能強大,最初知道它,是從hibernate的緩存開始的。網上中文的ehcache材料以簡單介紹和配置方法居多,如果你有這方面的問題,請自行google;對于api,官網上介紹已經非常清楚,請參見官網;但是很少見到特性說明和對實現原理的分析,因此在這篇文章里面,我會詳細介紹和分析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、開啟jmx
ehcache的jmx功能是默認開啟的,你可以監控和管理如下的mbean:
cachemanager、cache、cacheconfiguration、cachestatistics
9、分布式緩存
從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容器里面配置。
標準服務端包含了內嵌的glassfish web容器。它被打成了war包,可以任意部署到支持servlet 2.5的web容器內。glassfish v2/3、tomcat 6和jetty 6都已經經過了測試。
10、搜索
標準分布式搜索使用了流式查詢接口的方式,請參閱文檔。
11、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的事務支持,支持事務資源管理,二階段提交和回滾,以及本地事務。
12、開源協議
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支持
ehcache的結構設計概覽:
三、核心定義:
cache manager:緩存管理器,以前是只允許單例的,不過現在也可以多實例了
cache:緩存管理器內可以放置若干cache,存放數據的實質,所有cache都實現了ehcache接口
element:單條緩存數據的組成單位
system of record(sor):可以取到真實數據的組件,可以是真正的業務邏輯、外部接口調用、存放真實數據的數據庫等等,緩存就是從sor中讀取或者寫入到sor中去的。
代碼示例:
1
2
3
4
5
|
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的配置方式,配置都是可以在運行時動態修改的:
1
2
3
4
5
6
7
8
9
|
cache testcache = new cache( new cacheconfiguration( "testcache" , maxelements) .memorystoreevictionpolicy(memorystoreevictionpolicy.lfu) .overflowtodisk( true ) .eternal( false ) .timetoliveseconds( 60 ) .timetoidleseconds( 30 ) .diskpersistent( false ) .diskexpirythreadintervalseconds( 0 )); |
事務的例子:
1
2
3
4
5
6
7
8
9
10
11
|
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,更新某條數據以后,后續的訪問全部都能取得更新后的數據。
會話內一致:它本質上和上面那一條是一致的,某用戶更改了數據,只要會話還存在,后續他取得的所有數據都必須是更改后的數據。
單調讀一致:如果一個進程可以看到當前的值,那么后續的訪問不能返回之前的值。
單調寫一致:對同一進程內的寫行為必須是保序的,否則,寫完畢的結果就是不可預期的了。
4、bulk load:這種模型是基于批量加載數據到緩存里面的場景而優化的,沒有引入鎖和常規的淘汰算法這些降低性能的東西,它和最終一致性模型很像,但是有批量、高速寫和弱一致性保證的機制。
這樣幾個api也會影響到一致性的結果:
1、顯式鎖(explicit locking):如果我們本身就配置為強一致性,那么自然所有的緩存操作都具備事務性質。而如果我們配置成最終一致性時,再在外部使用顯式鎖api,也可以達到事務的效果。當然這樣的鎖可以控制得更細粒度,但是依然可能存在競爭和線程阻塞。
2、無鎖可讀取視圖(unlockedreadsview):一個允許臟讀的decorator,它只能用在強一致性的配置下,它通過申請一個特殊的寫鎖來比完全的強一致性配置提升性能。
舉例如下,xml配置為強一致性模型:
1
2
3
4
5
6
|
<cache name= "mycache" maxelementsinmemory= "500" eternal= "false" overflowtodisk= "false" <terracotta clustered= "true" consistency= "strong" /> </cache> |
但是使用unlockedreadsview:
1
2
|
cache cache = cachemanager.getehcache( "mycache" ); unlockedreadsview unlockedreadsview = new unlockedreadsview(cache, "myunlockedcache" ); |
3、原子方法(atomic methods):方法執行是原子化的,即cas操作(compare and swap)。cas最終也實現了強一致性的效果,但不同的是,它是采用樂觀鎖而不是悲觀鎖來實現的。在樂觀鎖機制下,更新的操作可能不成功,因為在這過程中可能會有其他線程對同一條數據進行變更,那么在失敗后需要重新執行更新操作。現代的cpu都支持cas原語了。
1
2
3
|
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端遠大于應用節點。
3、復制式緩存(replicated ehcache):緩存數據時同時存放在多個應用節點的,數據復制和失效的事件以同步或者異步的形式在各個集群節點間傳播。上述事件到來時,會阻塞寫線程的操作。在這種模式下,只有弱一致性模型。
它有如下幾種事件傳播機制:rmi、jgroups、jms和cache server。
rmi模式下,所有節點全部對等:
jgroup模式:可以配置單播或者多播,協議棧和配置都非常靈活。
1
2
3
4
5
6
|
<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 ):對于單實例的應用來說,緩存可以保護數據庫的讀風暴;但是,在集群的環境下,每一個應用節點都要定期保持數據最新,節點越多,要維持這樣的情況對數據庫的開銷也越大。
六、存儲方式:
1、堆內存儲:速度快,但是容量有限。
2、堆外(offheapstore)存儲:被稱為bigmemory,只在企業版本的ehcache中提供,原理是利用nio的directbytebuffers實現,比存儲到磁盤上快,而且完全不受gc的影響,可以保證響應時間的穩定性;但是direct buffer的在分配上的開銷要比heap buffer大,而且要求必須以字節數組方式存儲,因此對象必須在存儲過程中進行序列化,讀取則進行反序列化操作,它的速度大約比堆內存儲慢一個數量級。
(注:direct buffer不受gc影響,但是direct buffer歸屬的的java對象是在堆上且能夠被gc回收的,一旦它被回收,jvm將釋放direct buffer的堆外空間。)
3、磁盤存儲。
七、緩存使用模式:
cache-aside:直接操作。先詢問cache某條緩存數據是否存在,存在的話直接從cache中返回數據,繞過sor;如果不存在,從sor中取得數據,然后再放入cache中。
1
2
3
4
5
6
7
8
9
10
11
|
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的時候使用,后者則允許你自由控制緩存更新通知的時機。
更多push和pull的變化和不同,也可參見這里。
八、多種配置方式:
包括配置文件、聲明式配置、編程式配置,甚至通過指定構造器的參數來完成配置,配置設計的原則包括:
所有配置要放到一起
緩存的配置可以很容易在開發階段、運行時修改
錯誤的配置能夠在程序啟動時發現,在運行時修改出錯則需要拋出運行時異常
提供默認配置,幾乎所有的配置都是可選的,都有默認值
九、自動資源控制(automatic resource control,arc):
它是提供了一種智能途徑來控制緩存,調優性能。特性包括:
內存內緩存對象大小的控制,避免oom出現
池化(cache manager級別)的緩存大小獲取,避免單獨計算緩存大小的消耗
靈活的獨立基于層的大小計算能力,下圖中可以看到,不同層的大小都是可以單獨控制的
可以統計字節大小、緩存條目數和百分比
優化高命中數據的獲取,以提升性能,參見下面對緩存數據在不同層之間的流轉的介紹
緩存數據的流轉包括了這樣幾種行為:
flush:緩存條目向低層次移動。
fault:從低層拷貝一個對象到高層。在獲取緩存的過程中,某一層發現自己的該緩存條目已經失效,就觸發了fault行為。
eviction:把緩存條目除去。
expiration:失效狀態。
pinning:強制緩存條目保持在某一層。
下面的圖反映了數據在各個層之間的流轉,也反映了數據的生命周期:
十、監控功能:
監控的拓撲:
每個應用節點部署一個監控探針,通過tcp協議與監控服務器聯系,最終將數據提供給富文本客戶端或者監控操作服務器。
十一、廣域網復制:
緩存數據復制方面,ehcache允許兩個地理位置各異的節點在廣域網下維持數據一致性,同時它提供了這樣幾種方案(注:下面的示例都只繪制了兩個節點的情形,實際可以推廣到n個節點):
第一種方案: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特性應用較少或者比較邊緣化,沒有提到,例如對于jmx的支持;還有一些則是有類似的特性和介紹了,例如對于web的支持,請參見我這篇關于oscache的解讀,其中的“web支持”一節有詳細的原理分析。
最后,關于ehcache的性能比對,下面這張圖來自ehcache的創始人greg luck的blog:
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。