一 介紹
EhCache 是一個純Java的進程內緩存框架,具有快速、精干等特點,是Hibernate中默認CacheProvider。Ehcache是一種廣泛使用的開源Java分布式緩存。主要面向通用緩存,Java EE和輕量級容器。它具有內存和磁盤存儲,緩存加載器,緩存擴展,緩存異常處理程序,一個gzip緩存servlet過濾器,支持REST和SOAP api等特點。
特性
- 快速、簡單
- 多種
緩存策略 - 緩存數據有兩級:
內存和磁盤,因此無需擔心容量問題 - 緩存數據會在虛擬機
重啟的過程中寫入磁盤 - 可以通過
RMI、可插入API等方式進行分布式緩存 - 具有緩存和緩存管理器的偵聽接口
- 支持
多緩存管理器實例,以及一個實例的多個緩存區域 - 提供
Hibernate的緩存實現
集成
可以單獨使用,一般在第三方庫中被用到的比較多(如mybatis、shiro等)ehcache 對分布式支持不夠好,多個節點不能同步,通常和redis一塊使用
靈活性
ehcache具備對象api接口和可序列化api接口
不能序列化的對象可以使用出磁盤存儲外ehcache的所有功能
支持基於Cache和基於Element的過期策略,每個Cache的存活時間都是可以設置和控制的。
提供了LRU、LFU和FIFO緩存淘汰算法,Ehcache 1.2引入了最少使用和先進先出緩存淘汰算法,構成了完整的緩存淘汰算法。
提供內存和磁盤存儲,Ehcache和大多數緩存解決方案一樣,提供高性能的內存和磁盤存儲。
動態、運行時緩存配置,存活時間、空閑時間、內存和磁盤存放緩存的最大數目都是可以在運行時修改的。
應用持久化
在vm重啟后,持久化到磁盤的存儲可以復原數據
Ehache是第一個引入緩存數據持久化存儲的開源java緩存框架,緩存的數據可以在機器重啟后從磁盤上重新獲得
根據需要將緩存刷到磁盤。將緩存條目刷到磁盤的操作可以通過cache.fiush方法執行,這大大方便了ehcache的使用
ehcache 和 redis 比較
- ehcache直接在jvm虛擬機中緩存,
速度快,效率高;但是緩存共享麻煩,集群分布式應用不方便。 - redis是通過socket訪問到緩存服務,效率比ecache低,比數據庫要快很多,
處理集群和分布式緩存方便,有成熟的方案。如果是單個應用或者對緩存訪問要求很高的應用,用ehcache。如果是大型系統,存在緩存共享、分布式部署、緩存內容很大的,建議用redis。
二 Hello World
依賴
<dependencies> <dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache</artifactId> <version>2.10.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies>
配置文件
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"> <!-- 磁盤緩存位置 --> <diskStore path="java.io.tmpdir/ehcache"/> <!-- 默認緩存 --> <defaultCache maxEntriesLocalHeap="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" maxEntriesLocalDisk="10000000" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> <persistence strategy="localTempSwap"/> </defaultCache> <!-- helloworld緩存 --> <cache name="HelloWorldCache" maxElementsInMemory="1000" eternal="false" timeToIdleSeconds="5" timeToLiveSeconds="5" overflowToDisk="false" memoryStoreEvictionPolicy="LRU"/> </ehcache>
測試類
package com.zyc; import org.junit.Test; import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Element; public class Test1 {
三 配置文件說明
diskStore
- path :指定磁盤存儲的位置
defaultCache
默認的緩存
cache
自定的緩存,當自定的配置不滿足實際情況時可以通過自定義(可以包含多個cache節點)
name: 緩存的名稱,可以通過指定名稱獲取指定的某個Cache對象maxElementsInMemory:內存中允許存儲的最大的元素個數,0代表無限個clearOnFlush:內存數量最大時是否清除。eternal:設置緩存中對象是否為永久的,如果是,超時設置將被忽略,對象從不過期。根據存儲數據的不同,例如一些靜態不變的數據如省市區等可以設置為永不過時timeToIdleSeconds: 設置對象在失效前的允許閑置時間(單位:秒)。僅當eternal=false對象不是永久有效時使用,可選屬性,默認值是0,也就是可閑置時間無窮大。timeToLiveSeconds:緩存數據的生存時間(TTL),也就是一個元素從構建到消亡的最大時間間隔值,這只能在元素不是永久駐留時有效,如果該值是0就意味着元素可以停頓無窮長的時間。(和上面的兩者取最小值)overflowToDisk:內存不足時,是否啟用磁盤緩存。maxEntriesLocalDisk:當內存中對象數量達到maxElementsInMemory時,Ehcache將會對象寫到磁盤中。maxElementsOnDisk:硬盤最大緩存個數。diskSpoolBufferSizeMB:這個參數設置DiskStore(磁盤緩存)的緩存區大小。默認是30MB。每個Cache都應該有自己的一個緩沖區。diskPersistent:是否在VM重啟時存儲硬盤的緩存數據。默認值是false。diskExpiryThreadIntervalSeconds:磁盤失效線程運行時間間隔,默認是120秒。memoryStoreEvictionPolicy:當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理內存。默認策略是LRU(最近最少使用)。你可以設置為FIFO(先進先出)或是LFU(較少使用)。這里比較遺憾,Ehcache並沒有提供一個用戶定制策略的接口,僅僅支持三種指定策略,感覺做的不夠理想。
編程方式配置
Cache cache = manager.getCache("mycache");
CacheConfiguration config = cache.getCacheConfiguration();
config.setTimeToIdleSeconds(60);
config.setTimeToLiveSeconds(120);
config.setmaxEntriesLocalHeap(10000);
config.setmaxEntriesLocalDisk(1000000);
持久化配置
類必須實現序列化接口,不需要的屬性用transientx修飾
這種是所有數據都放到磁盤里去了
<!-- helloworld緩存 --> <cache name="HelloWorldCache" maxElementsInMemory="1" //設置成1,overflowToDisk設置成true,只要有一個緩存元素,就直接存到硬盤上去 eternal="false" timeToIdleSeconds="50000" timeToLiveSeconds="50000" overflowToDisk="true" diskPersistent="true" //設置成true表示緩存虛擬機重啟期數據 memoryStoreEvictionPolicy="LRU"/>
自己決定說明時候持久化
測試得出以下兩個方法在配置持久化環境的情況下都會將內存中的數據放到磁盤上
cache.flush(); // 8. 關閉緩存管理器 cacheManager.shutdown()
自動持久化
想利用spring 的注解,不想手動shutdown ,因此web.xml 配置listener 監聽,在銷毀的時候進行shutdown,這里利用ehcache 的監聽.
<!-- ehcache 磁盤緩存 監控,持久化恢復 --> <listener> <listener-class>net.sf.ehcache.constructs.web.ShutdownListener</listener-class> </listener>
直接殺死線程,這肯定監聽不到。
更多資料(未測試過):記一次,ehcache緩存到磁盤,再恢復的過程
spring持久化測試情況
通過注解調用發現,每次test結束后,都會自動持久化
@Test public void testPersist(){ System.out.println(ehcacheService.getDataFromDB("tt1")); System.out.println(ehcacheService.getDataFromDB("tt1")); } @Cacheable(value="HelloWorldCache", key="#key") @Override public String getDataFromDB(String key) { System.out.println("從數據庫中獲取數據..."); return key + ":" + String.valueOf(Math.round(Math.random()*1000000)); }
四 一致性模型
說到一致性,數據庫的一致性是怎樣的?不妨先來回顧一下數據庫的幾個隔離級別:
未提交讀(Read Uncommitted):在讀數據時不會檢查或使用任何鎖。因此,在這種隔離級別中可能讀取到沒有提交的數據。會出現臟讀、不可重復讀、幻象讀。
已提交讀(Read Committed):只讀取提交的數據並等待其他事務釋放排他鎖。讀數據的共享鎖在讀操作完成后立即釋放。已提交讀是數據庫的默認隔離級別。會出現不可重復讀、幻象讀。
可重復讀(Repeatable Read):像已提交讀級別那樣讀數據,但會保持共享鎖直到事務結束。會出現幻象讀。
可序列化(Serializable):工作方式類似於可重復讀。但它不僅會鎖定受影響的數據,還會鎖定這個范圍,這就阻止了新數據插入查詢所涉及的范圍。
模型分類
-
強一致性模型:系統中的某個數據被成功更新(事務成功返回)后,后續任何對該數據的讀取操作都得到到更新后的值。這是傳統關系數據庫提供的一致性模型,也是關系數據庫深受人們喜愛的原因之一。強一致性模型下的性能消耗通常是最大的 -
弱一致性模型:系統中的某個數據被更新后,后續對該數據的讀取操作得到的不一定是更新后的值,這種情況下通常有個“不一致性時間窗口”存在:即數據更新完成后在經過這個時間窗口,后續讀取操作就能夠得到更新后的值。 -
最終一致性模型:屬於弱一致性的一種,即某個數據被更新后,如果該數據后續沒有被再次更新,那么最終(沒有時間窗口)所有的讀取操作都會返回更新后的值 -
Bulk Load:這種模型是基於批量加載數據到緩存里面的場景而優化的,沒有引入鎖和常規的淘汰算法這些降低性能的東西,它和最終一致性模型很像,但是有批量、高速寫和弱一致性保證的機制。
最終一致性模型包含如下幾個必要屬性
-
讀寫一致:某線程A,更新某條數據以后,后續的訪問全部都能取得更新后的數據。 -
會話內一致:它本質上和上面那一條是一致的,某用戶更改了數據,只要會話還存在,后續他取得的所有數據都必須是更改后的數據。 -
單調讀一致:如果一個進程可以看到當前的值,那么后續的訪問不能返回之前的值。 -
單調寫一致:對同一進程內的寫行為必須是保序的,否則,寫完畢的結果就是不可預期的了。·
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"); //代碼上設置
3、原子方法(Atomic methods):方法執行是原子化的,即CAS操作(Compare and Swap)。CAS最終也實現了強一致性的效果,但不同的是,它是采用樂觀鎖而不是悲觀鎖來實現的。在樂觀鎖機制下,更新的操作可能不成功,因為在這過程中可能會有其他線程對同一條數據進行變更,那么在失敗后需要重新執行更新操作。現代的CPU都支持CAS原語了。
cache.putIfAbsent(Element element); cache.replace(Element oldOne, Element newOne); cache.remove(Element);
五 Spring整合
spring注解
Spring對緩存的支持類似於對事務的支持。
首先使用注解標記方法,相當於定義了切點,然后使用Aop技術在這個方法的調用前、調用后獲取方法的入參和返回值,進而實現了緩存的邏輯。
@Cacheable
表明所修飾的方法是可以緩存的:當第一次調用這個方法時,它的結果會被緩存下來,在緩存的有效時間內,以后訪問這個方法都直接返回緩存結果,不再執行方法中的代碼段。
- 這個注解可以用
condition屬性來設置條件,如果不滿足條件,就不使用緩存能力,直接執行方法。 - 可以使用
key屬性來指定key的生成規則。
參數
-
value:緩存位置名稱,不能為空,如果使用EHCache,就是ehcache.xml中聲明的cache的name, 指明將值緩存到哪個Cache中 -
key:緩存的key,默認為空,既表示使用方法的參數類型及參數值作為key,支持SpEL,如果要引用參數值使用井號加參數名,如:#userId,
一般來說,我們的更新操作只需要刷新緩存中某一個值,所以定義緩存的key值的方式就很重要,最好是能夠唯一,因為這樣可以准確的清除掉特定的緩存,而不會影響到其它緩存值 ,
本例子中使用實體加冒號再加ID組合成鍵的名稱,如"user:1"、"order:223123"等 -
condition:觸發條件,只有滿足條件的情況才會加入緩存,默認為空,既表示全部都加入緩存,支持SpEL
// 將緩存保存到名稱為UserCache中,鍵為"user:"字符串加上userId值,如 'user:1' @Cacheable(value="UserCache", key="'user:' + #userId") public User findById(String userId) { return (User) new User("1", "mengdee"); } // 將緩存保存進UserCache中,並當參數userId的長度小於12時才保存進緩存,默認使用參數值及類型作為緩存的key // 保存緩存需要指定key,value, value的數據類型,不指定key默認和參數名一樣如:"1" @Cacheable(value="UserCache", condition="#userId.length() < 12") public boolean isReserved(String userId) { System.out.println("UserCache:"+userId); return false; }
@CachePut
與@Cacheable不同,@CachePut不僅會緩存方法的結果,還會執行方法的代碼段。它支持的屬性和用法都與@Cacheable一致。一個緩存后就不執行代碼了,一個還要執行)
@CacheEvict
與@Cacheable功能相反,@CacheEvict表明所修飾的方法是用來刪除失效或無用的緩存數據。
參數
- value:緩存位置名稱,不能為空,同上
- key:緩存的key,默認為空,同上
- condition:觸發條件,只有滿足條件的情況才會清除緩存,默認為空,支持SpEL
allEntries:true表示清除value中的全部緩存,默認為false
//清除掉UserCache中某個指定key的緩存 @CacheEvict(value="UserCache",key="'user:' + #userId") public void removeUser(User user) { System.out.println("UserCache"+user.getUserId()); } //清除掉UserCache中全部的緩存 @CacheEvict(value="UserCache", allEntries=true) public final void setReservedUsers(String[] reservedUsers) { System.out.println("UserCache deleteall"); }
代碼測試
目錄結構
pom
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.zyc</groupId> <artifactId>ehcache1</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <junit.version>4.10</junit.version> <spring.version>4.2.3.RELEASE</spring.version> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope> </dependency> <!-- springframework --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework