理論總結
它要解決什么樣的問題?
數據的訪問、存取、計算太慢、太不穩定、太消耗資源,同時,這樣的操作存在重復性。因此希望有這樣一種中間媒介,放置在其間,只保存自己關心的數據,而不關心具體數據邏輯內容,對于重復性的操作給出響應。對于數據和服務的使用者,它是透明的。
為哪些數據做緩存?
- 模型對象,這在業務邏輯層面最常見。
- 數據庫查詢結果集。
- 頁面緩存、頁面片段緩存。
- 運算結果集,尤其對于冪等性服務。
- 外部接口查詢結果。
緩存框架的核心
緩存生命周期管理,很多重要特性都是圍繞它來展開的。
重要特性,這些特性不一定全部要具備,但是多數都要包含
一致性選擇。緩存框架的設計必須首先考慮這一點。通常我們見到的緩存框架都是最終一致性的,允許獲取數據有一定的延遲窗口。一致性關系到緩存的生命周期,是緩存的核心理念之一。
分級存儲。也和緩存生命周期密切相關。至少應包括內存和磁盤兩級存儲,有些緩存框架包含組網內部節點的分級等等,允許用戶管理緩存數據在不同級別存儲中的躍遷。分級存儲還包括對存儲數據的管理,以提高數據獲取的效率;包括躍遷策略,超時策略的定制,比如在某一級滿足怎樣的超時策略可以發生向下躍遷。
規約配置,默認配置。可以支持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/Expired8 分布式緩存
從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(最少訪問次數)。