一 介紹
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);
持久化配置
類必須實現序列化接口,不需要的屬性用transient
x修飾
這種是所有數據都放到磁盤里去了
<!-- 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