spring ehcache 緩存框架

1、簡介算法

Ehcache是一個用Java實現的使用簡單,高速,實現線程安全的緩存管理類庫,ehcache提供了用內存,磁盤文件存儲,以及分佈式存儲方式等多種靈活的cache管理方案。同時ehcache做爲開放源代碼項目,採用限制比較寬鬆的Apache License V2.0做爲受權方式,被普遍地用於Hibernate, Spring,Cocoon等其餘開源系統。Ehcache 從 Hibernate 發展而來,逐漸涵蓋了 Cahce 界的所有功能,是目前發展勢頭最好的一個項目。具備快速,簡單,低消耗,依賴性小,擴展性強,支持對象或序列化緩存,支持緩存或元素的失效,提供 LRU、LFU 和 FIFO 緩存策略,支持內存緩存和磁盤緩存,分佈式緩存機制等等特色。spring

備註:爲了方便你們了最新版本的Ehcache,本文中1-6節採用的最新的Ehcache3.0的特性和使用介紹,從第7節開始採用的是Ehcache2.10.2版原本與Spring相結合來作案例介紹,包括後面的源碼分析也將採用這個版本數據庫

2、主要特性緩存

快速;
簡單;
多種緩存策略;
緩存數據有兩級:內存和磁盤,所以無需擔憂容量問題;
緩存數據會在虛擬機重啓的過程當中寫入磁盤;
能夠經過 RMI、可插入 API 等方式進行分佈式緩存;
具備緩存和緩存管理器的偵聽接口;
支持多緩存管理器實例,以及一個實例的多個緩存區域;
提供 Hibernate 的緩存實現;安全

3、Ehcache的架構設計圖服務器


說明
CacheManager:是緩存管理器,能夠經過單例或者多例的方式建立,也是Ehcache的入口類。
Cache:每一個CacheManager能夠管理多個Cache,每一個Cache能夠採用hash的方式管理多個Element。
Element:用於存放真正緩存內容的。網絡

結構圖以下所示:架構

4、Ehcache的緩存數據淘汰策略併發

FIFO:先進先出
LFU:最少被使用,緩存的元素有一個hit屬性,hit值最小的將會被清出緩存。
LRU:最近最少使用,緩存的元素有一個時間戳,當緩存容量滿了,而又須要騰出地方來緩存新的元素的時候,那麼現有緩存元素中時間戳離當前時間最遠的元素將被清出緩存。app

5、Ehcache的緩存數據過時策略

Ehcache採用的是懶淘汰機制,每次往緩存放入數據的時候,都會存一個時間,在讀取的時候要和設置的時間作TTL比較來判斷是否過時。

6、Ehcache緩存的使用介紹

6.一、目前最新的Ehcache是3.0版本,咱們也就使用3.0版原本介紹它的使用介紹,看以下代碼:


Paste_Image.png

注:這段代碼介紹了Ehcache3.0的緩存使用生命週期的一個過程。
一、靜態方法CacheManagerBuilder.newCacheManagerBuilder將返回一個新的org.ehcache.config.builders.CacheManagerBuilder的實例。
二、當咱們要構建一個緩存管理器的時候,使用CacheManagerBuilder來建立一個預配置(pre-configured)緩存。

  • 第一個參數是一個別名,用於Cache和Cachemanager進行配合。
  • 第二個參數是org.ehcache.config.CacheConfiguration主要用來配置Cache。咱們使用org.ehcache.config.builders.CacheConfigurationBuilder的靜態方法newCacheConfigurationBuilder來建立一個默認配置實例。

三、最後調用.build方法返回一個完整的實例,固然咱們也能使用CacheManager來初始化。
四、在你開始使用CacheManager的時候,須要使用init()方法進行初始化。
五、咱們能取回在第二步中設定的pre-configured別名,咱們對於key和要傳遞的值類型,要求是類型安全的,不然將拋出ClassCastException異常。
六、能夠根據需求,經過CacheManager建立出新的Cache。實例化和完整實例化的Cache將經過CacheManager.getCache API返回。
七、使用put方法存儲數據。
八、使用get方法獲取數據。
九、咱們能夠經過CacheManager.removeCache方法來獲取Cache,可是Cache取出來之後CacheManager將會刪除自身保存的Cache實例。
十、close方法將釋放CacheManager所管理的緩存資源。

6.二、關於磁盤持久化


Paste_Image.png

注:若是您想使用持久化機制,就須要提供一個磁盤存儲的位置給CacheManagerBuilder.persistence這個方法,另外在使用的過程當中,你還須要定義一個磁盤使用的資源池。

上面的例子實際上是分配了很是少的磁盤存儲量,不過咱們須要注意的是因爲存儲在磁盤上咱們須要作序列化和反序列化,以及讀和寫的操做。它的速度確定比內存要慢的多。

6.三、經過xml配置文件建立CacheManager


Paste_Image.png

注:
一、描述緩存的別名。
二、foo的key的類型指定爲String類型,而value並無指定類型,默認就是Object類型。
三、能夠在堆中爲foo建立2000個實體。
四、在開始淘汰過時緩存項以前,能夠分配多達500M的堆內存。
五、cache-template能夠實現一個配置抽象,以便在將來能夠進行擴展。
六、bar使用了cache-template模板myDefaults,而且覆蓋了key-type類型,myDefaults的key-type是Long類型,覆蓋後成了Number類型。

使用如下代碼建立CacheManager:


Paste_Image.png

7、UserManagerCache介紹

7.1 什麼是UserManagerCache,它能作什麼?
UserManagerCache這是在Ehcache3.0中引入的新的概念,它將直接建立緩存而不須要使用CacheManager來進行管理。因此這也就是UserManagerCache名稱的由來。
因爲沒有CacheManager的管理,用戶就必需要手動配置所須要的服務,固然若是你發現要使用大量的服務,那麼CacheManager則是更好的選擇。

7.2 使用示例
一、基本示例

1 UserManagedCache<Long, String> userManagedCache =
2     UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
3         .build(false); 
4 userManagedCache.init(); 
5 
6 userManagedCache.put(1L, "da one!"); 
7 
8 userManagedCache.close();

二、持久化示例

 1 LocalPersistenceService persistenceService = new DefaultLocalPersistenceService(new DefaultPersistenceConfiguration(new File(getStoragePath(), "myUserData"))); 
 2 
 3 PersistentUserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
 4     .with(new UserManagedPersistenceContext<Long, String>("cache-name", persistenceService)) 
 5     .withResourcePools(ResourcePoolsBuilder.newResourcePoolsBuilder()
 6         .heap(10L, EntryUnit.ENTRIES)
 7         .disk(10L, MemoryUnit.MB, true)) 
 8     .build(true);
 9 
10 // Work with the cache
11 cache.put(42L, "The Answer!");
12 assertThat(cache.get(42L), is("The Answer!"));
13 
14 cache.close(); 
15 cache.destroy();

三、讀寫緩存示例

1 UserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
2     .withLoaderWriter(new SampleLoaderWriter<Long, String>()) 
3     .build(true);
4 
5 // Work with the cache
6 cache.put(42L, "The Answer!");
7 assertThat(cache.get(42L), is("The Answer!"));
8 
9 cache.close();

注:
若是你但願頻繁的讀和寫緩存,則可使用CacheLoaderWriter。

四、緩存淘汰策略示例

 1 UserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
 2     .withEvictionAdvisor(new OddKeysEvictionAdvisor<Long, String>()) 
 3     .withResourcePools(ResourcePoolsBuilder.newResourcePoolsBuilder()
 4         .heap(2L, EntryUnit.ENTRIES)) 
 5     .build(true);
 6 
 7 // Work with the cache
 8 cache.put(42L, "The Answer!");
 9 cache.put(41L, "The wrong Answer!");
10 cache.put(39L, "The other wrong Answer!");
11 
12 cache.close();

注:
若是你想使用緩存淘汰算法來淘汰數據,則要使用EvictionAdvisor這個類。

五、按字節設定的緩存示例

 1 UserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
 2     .withSizeOfMaxObjectSize(500, MemoryUnit.B)
 3     .withSizeOfMaxObjectGraph(1000) 
 4     .withResourcePools(ResourcePoolsBuilder.newResourcePoolsBuilder()
 5         .heap(3, MemoryUnit.MB)) 
 6     .build(true);
 7 
 8 cache.put(1L, "Put");
 9 cache.put(1L, "Update");
10 
11 assertThat(cache.get(1L), is("Update"));
12 
13 cache.close();

注:
withSizeOfMaxObjectGraph這個主要是調整能夠設置多少字節對象。
.heap方法主要是設置每一個對象最大能夠設置多大。

8、緩存的使用模式

使用緩存時有幾種常見的訪問模式:
一、預留緩存(Cache-Aside)
應用程序在訪問數據庫以前必需要先訪問緩存,若是緩存中包含了該數據則直接返回,不用再通過數據庫,不然應用程序必需要從先數據庫中取回數據,存儲在緩存中而且將數據返回,當有數據要寫入的時候,緩存內容必需要和數據庫內容保持一致。

示例以下代碼分別對應讀和寫:

1 v = cache.get(k)
2 if(v == null) {
3     v = sor.get(k)
4     cache.put(k, v)
5 }
6 
7 v = newV
8 sor.put(k, v)
9 cache.put(k, v)

這種方式是將數據庫與緩存經過客戶端應用程序主動管理來進行同步,這不是很好的使用方式。

二、Read-Through模式
相比上面的由客戶端應用程序來管理數據庫和緩存同步的方式,這種模式緩存會配有一個緩存中間件,該中間件來負責數據庫數據和緩存之間的同步問題。當咱們應用要獲取緩存數據時,這個緩存中間件要確認緩存中是否有該數據,若是沒有,從數據庫加載,而後放入緩存,下次之後再訪問就能夠直接從緩存中得到。

三、Write-Through模式
這種模式就是緩存可以感知數據的變化。
也就是說在更新數據庫的同時也要更新緩存,該模式其實也是弱一致性,當數據庫更新數據失敗的時候,緩存不能繼續更新數據,要保持數據庫和緩存的最終一致性。

四、Write-behind模式
該模式是以緩存爲優先,將緩存更新的數據存放隊列中,而後數據庫定時批量從隊列中取出數據更新數據庫。

9、Spring3.2+Ehcache2.10.2的使用

爲了使例子更加簡單易懂,我沒有直接去鏈接數據庫而模擬了一些操做目的主要是演示Spring結合Ehcache的使用。
JavaBean代碼以下:

 1 public class User {  
 2     public Integer id;  
 3     public String name;  
 4     public String password;  
 5 
 6     // 這個須要,否則在實體綁定的時候出錯  
 7     public User(){}  
 8 
 9     public User(Integer id, String name, String password) {  
10         super();  
11         this.id = id;  
12         this.name = name;  
13         this.password = password;  
14     }  
15 
16     public Integer getId() {  
17         return id;  
18     }  
19     public void setId(Integer id) {  
20         this.id = id;  
21     }  
22     public String getName() {  
23         return name;  
24     }  
25     public void setName(String name) {  
26         this.name = name;  
27     }  
28     public String getPassword() {  
29         return password;  
30     }  
31     public void setPassword(String password) {  
32         this.password = password;  
33     }  
34 
35     @Override  
36     public String toString() {  
37         return "User [id=" + id + ", name=" + name + ", password=" + password  
38                 + "]";  
39     }  
40 }

UserDAO代碼以下:

 1 @Repository("userDao")  
 2 public class UserDao {  
 3     List<User> userList = initUsers();  
 4 
 5     public User findById(Integer id) { 
 6         for(User user : userList){  
 7             if(user.getId().equals(id)){  
 8                  return user;
 9             }  
10         }  
11         return null;  
12     }  
13 
14     public void removeById(Integer id) { 
15         User delUser = null;
16         for(User user : userList){  
17             if(user.getId().equals(id)){  
18                   delUser = user;
19             }  
20         }  
21         users.remove(delUser);  
22     }  
23 
24     public void addUser(User user){  
25         users.add(user);  
26     }  
27 
28     public void updateUser(User user){  
29         addUser(user);  
30     }  
31 
32     private List<User> initUsers(){  
33         List<User> users = new ArrayList<User>();  
34         User u1 = new User(1,"張三","123");  
35         User u2 = new User(2,"李四","124");  
36         User u3 = new User(3,"王五","125");  
37         users.add(u1);  
38         users.add(u2);  
39         users.add(u3);  
40         return users;  
41     }  
42 }

UserService代碼以下:

 1 @Service("userService")  
 2 public class UserService {  
 3 
 4     @Autowired  
 5     private UserDao userDao;  
 6 
 7     // 查詢全部數據,不須要key
 8     @Cacheable(value = "serviceCache")  
 9     public List<User> getAll(){  
10         printInfo("getAll");  
11         return userDao.users;  
12     }  
13     //根據ID來獲取數據,ID多是主鍵也多是惟一鍵
14     @Cacheable(value = "serviceCache", key="#id")  
15     public User findById(Integer id){  
16         printInfo(id);  
17         return userDao.findById(id);  
18     }  
19     //經過ID進行刪除 
20     @CacheEvict(value = "serviceCache", key="#id")  
21     public void removeById(Integer id){  
22         userDao.removeById(id);  
23     }  
24 
25    //添加數據
26     public void addUser(User user){  
27         if(user != null && user.getId() != null){  
28             userDao.addUser(user);  
29         }  
30     }  
31     // key 支持條件,包括 屬性condition ,能夠 id < 20 等等
32     @CacheEvict(value="serviceCache", key="#u.id")  
33     public void updateUser(User u){  
34         removeById(u.getId());  
35         userDao.updateUser(u);  
36     }  
37 
38    // allEntries 表示調用以後,清空緩存,默認false,  
39     // 還有個beforeInvocation 屬性,表示先清空緩存,再進行查詢  
40     @CacheEvict(value="serviceCache",allEntries=true)  
41     public void removeAll(){  
42         System.out.println("清除全部緩存");  
43     } 
44 
45     private void printInfo(Object str){  
46         System.out.println("非緩存查詢----------findById"+str);  
47     } 
48 }

ehcache配置文件內容以下:

1 <cache name="serviceCache"
2     eternal="false"  
3     maxElementsInMemory="100" 
4     overflowToDisk="false" 
5     diskPersistent="false"  
6     timeToIdleSeconds="0" 
7     timeToLiveSeconds="300"  
8     memoryStoreEvictionPolicy="LRU" /> 
9 </ehcache>

Spring配置文件內容以下:

 1 <bean id="cacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">  
 2         <property name="configLocation"  value="classpath:com/config/ehcache.xml"/> 
 3     </bean> 
 4 
 5     <!-- 支持緩存註解 -->
 6     <cache:annotation-driven cache-manager="cacheManager" />
 7 
 8     <!-- 默認是cacheManager -->
 9     <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">  
10         <property name="cacheManager"  ref="cacheManagerFactory"/>  
11     </bean>

10、Spring3.2+Ehcache2.10.2分佈式緩存的使用

10.1 Ehcache集羣簡介
從Ehcache1.2版本開始,Ehcache就可使用分佈式的緩存了,從 1.7版本開始,開始支持共五種集羣方案,分別是:

  • Terracotta
  • RMI
  • JMS
  • JGroups
  • EhCache Server

其中有三種上最爲經常使用集羣方式,分別是 RMI、JGroups 以及 EhCache Server 。
其實咱們在使用Ehcache分佈式緩存的過程當中,主要是以緩存插件的方式使用,若是咱們想根據本身的須要使用分佈式緩存那就須要本身開發來定製化,在後面咱們會發現其實Ehcache提供的分佈式緩存並非很是好用,有很多問題存在,因此對緩存數據一致性比較高的狀況下,使用集中式緩存更合適,好比Redis、Memcached等。

10.2 Ehcache集羣的基本概念
一、成員發現(Peer Discovery)
Ehcache集羣概念中有一個cache組,每一個cache都是另外一個cache的peer,並不像Redis或者其餘分佈式組件同樣有一個主的存在,Ehcache並無主Cache,但是那如何知道集羣中的其餘緩存都有誰呢?這個就是成員發現。

Ehcache提供了二種機制來實現成員發現功能,分別是手動發現和自動發現。

  • 手動發現

    在Ehcache的配置文件中指定cacheManagerPeerProviderFactory元素的class屬性爲
    net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory。這就須要本身去配置IP地址和端口號。
  • 自動發現

自動的發現方式用TCP廣播機制來肯定和維持一個廣播組。它只須要一個簡單的配置能夠自動的在組中添加和移除成員。在集羣中也不須要什麼優化服務器的知識,這是默認推薦的。

成員每秒向羣組發送一個「心跳」。若是一個成員 5秒種都沒有發出信號它將被羣組移除。若是一個新的成員發送了一個「心跳」它將被添加進羣組。

任何一個用這個配置安裝了複製功能的cache都將被其餘的成員發現並標識爲可用狀態。

要設置自動的成員發現,須要指定ehcache配置文件中:

1 cacheManagerPeerProviderFactory元素的properties屬性,就像下面這樣:
2 peerDiscovery=automatic
3 multicastGroupAddress=multicast address | multicast host name
4 multicastGroupPort=port
5 timeToLive=0-255 (timeToLive屬性詳見常見問題部分的描述)

10.3 結合Spring看示例
先看Spring配置文件:

 1 <!-- spring cache 配置 -->  
 2 <!-- 啓用緩存註解功能 -->  
 3 <cache:annotation-driven cache-manager="cacheManager"/>  
 4 
 5 <!-- cacheManager工廠類,指定ehcache.xml的位置 -->  
 6 <bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"  
 7       p:configLocation="classpath:ehcache/ehcache.xml"/>  
 8 
 9 <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"  
10       p:cacheManager-ref="ehcache"/>  
11 
12 <cache:annotation-driven />

Ehcache配置文件內容以下:

 1 <?xml version="1.0" encoding="UTF-8"?>  
 2 <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
 3          xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">  
 4 
 5     <!--EHCache分佈式緩存集羣環境配置-->  
 6     <!--rmi手動配置-->  
 7     <cacheManagerPeerProviderFactory class= "net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"  
 8               properties="peerDiscovery=manual,rmiUrls=//localhost:40000/user"/>  
 9 
10     <cacheManagerPeerListenerFactory  
11             class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"  
12             properties="hostName=localhost,port=40001, socketTimeoutMillis=120000"/>  
13     <defaultCache  
14             maxElementsInMemory="10000"  
15             eternal="false"  
16             timeToIdleSeconds="120"  
17             timeToLiveSeconds="120"  
18             overflowToDisk="true"  
19             diskSpoolBufferSizeMB="30"  
20             maxElementsOnDisk="10000000"  
21             diskPersistent="false"  
22             diskExpiryThreadIntervalSeconds="120"  
23             memoryStoreEvictionPolicy="LRU">  
24         <cacheEventListenerFactory  
25                 class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>  
26     </defaultCache>  
27     <cache name="user"  
28            maxElementsInMemory="1000"  
29            eternal="false"  
30            timeToIdleSeconds="100000"  
31            timeToLiveSeconds="100000"  
32            overflowToDisk="false">  
33         <cacheEventListenerFactory  
34                 class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>  
35     </cache>  
36 </ehcache>

以上配置其實就是使用RMI方式在集羣的環境進行緩存數據的複製。

11、Ehcache的使用場景

11.一、Ehcache使用的注意點

一、比較少的更新數據表的狀況
二、對併發要求不是很嚴格的狀況
多臺應用服務器中的緩存是不能進行實時同步的。
三、對一致性要求不高的狀況下
由於Ehcache本地緩存的特性,目前沒法很好的解決不一樣服務器間緩存同步的問題,因此咱們在一致性要求很是高的場合下,儘可能使用Redis、Memcached等集中式緩存。

11.二、Ehcache在集羣、分佈式的狀況下表現如何

在分佈式狀況下有二種同步方式:
一、RMI組播方式


Paste_Image.png


示例:

1 <cacheManagerPeerProviderFactory
2         class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
3         properties="peerDiscovery=automatic, multicastGroupAddress=localhost,
4         multicastGroupPort=4446,timeToLive=255"/>

原理:當緩存改變時,ehcache會向組播IP地址和端口號發送RMI UDP組播包。
缺陷:Ehcache的組播作得比較初級,功能只是基本實現(好比簡單的一個HUB,接兩臺單網卡的服務器,互相之間組播同步就沒問題),對一些複雜的環境(好比多臺服務器,每臺服務器上多地址,尤爲是集羣,存在一個集羣地址帶多個物理機,每臺物理機又帶多個虛擬站的子地址),就容易出現問題。

二、P2P方式
原理:P2P要求每一個節點的Ehcache都要指向其餘的N-1個節點。

三、JMS消息模式


Paste_Image.png


原理:這種模式的核心就是一個消息隊列,每一個應用節點都訂閱預先定義好的主題,同時,節點有元素更新時,也會發布更新元素到主題中去。各個應用服務器節點經過偵聽MQ獲取到最新的數據,而後分別更新本身的Ehcache緩存,Ehcache默認支持ActiveMQ,咱們也能夠經過自定義組件的方式實現相似Kafka,RabbitMQ。

四、Cache Server模式
原理:這種模式會存在主從節點。


Paste_Image.png

缺陷:緩存容易出現數據不一致的問題,

11.三、使用Ehcache的瓶頸是什麼

一、緩存漂移(Cache Drift):每一個應用節點只管理本身的緩存,在更新某個節點的時候,不會影響到其餘的節點,這樣數據之間可能就不一樣步了。

二、數據庫瓶頸(Database Bottlenecks ):對於單實例的應用來講,緩存能夠保護數據庫的讀風暴;可是,在集羣的環境下,每個應用節點都要按期保持數據最新,節點越多,要維持這樣的狀況對數據庫的開銷也越大。

11.四、實際工做中如何使用Ehcache

在實際工做中,我更可能是將Ehcache做爲與Redis配合的二級緩存。
第一種方式:


Paste_Image.png


注:
這種方式經過應用服務器的Ehcache定時輪詢Redis緩存服務器更同步更新本地緩存,缺點是由於每臺服務器定時Ehcache的時間不同,那麼不一樣服務器刷新最新緩存的時間也不同,會產生數據不一致問題,對一致性要求不高可使用。

第二種方式:


Paste_Image.png


注:
經過引入了MQ隊列,使每臺應用服務器的Ehcache同步偵聽MQ消息,這樣在必定程度上能夠達到準同步更新數據,經過MQ推送或者拉取的方式,可是由於不一樣服務器之間的網絡速度的緣由,因此也不能徹底達到強一致性。基於此原理使用Zookeeper等分佈式協調通知組件也是如此。

總結:
一、使用二級緩存的好處是減小緩存數據的網絡傳輸開銷,當集中式緩存出現故障的時候,Ehcache等本地緩存依然可以支撐應用程序正常使用,增長了程序的健壯性。另外使用二級緩存策略能夠在必定程度上阻止緩存穿透問題。

二、根據CAP原理咱們能夠知道,若是要使用強一致性緩存(根據自身業務決定),集中式緩存是最佳選擇,如(Redis,Memcached等)。

12、Ehcache2.10.2源碼分析

12.1 源碼淘汰策略解析
首先看一下類結構圖:


Paste_Image.png

從類結構圖上看一共有三種緩存淘汰策略分別是:LFU,LRU,FIFO。關於這三個概念在前面都已經有過解釋,咱們直接這三個的源碼:
一、LRUPolicy代碼以下:

 1 public class LruPolicy extends AbstractPolicy {
 2 
 3     /**
 4      * The name of this policy as a string literal
 5      */
 6      public static final String NAME = "LRU";
 7 
 8     /**
 9      * @return the name of the Policy. Inbuilt examples are LRU, LFU and FIFO.
10      */
11     public String getName() {
12         return NAME;
13     }
14 
15     /**
16      * Compares the desirableness for eviction of two elements
17      *
18      * Compares hit counts. If both zero,
19      *
20      * @param element1 the element to compare against
21      * @param element2 the element to compare
22      * @return true if the second element is preferable to the first element for ths policy
23      */
24     public boolean compare(Element element1, Element element2) {
25         return element2.getLastAccessTime() < element1.getLastAccessTime();
26 
27     }

注:
accessTime小的緩存淘汰。

二、LFUPolicy代碼以下:

 1 public class LfuPolicy extends AbstractPolicy {
 2 
 3     /**
 4      * The name of this policy as a string literal
 5      */
 6     public static final String NAME = "LFU";
 7 
 8     /**
 9      * @return the name of the Policy. Inbuilt examples are LRU, LFU and FIFO.
10      */
11     public String getName() {
12         return NAME;
13     }
14 
15     /**
16      * Compares the desirableness for eviction of two elements
17      *
18      * Compares hit counts. If both zero, 
19      *
20      * @param element1 the element to compare against
21      * @param element2 the element to compare
22      * @return true if the second element is preferable to the first element for ths policy
23      */
24     public boolean compare(Element element1, Element element2) {
25         return element2.getHitCount() < element1.getHitCount();
26 
27     }
28 }

注:
hit值小的緩存淘汰。

三、FIFOPolicy代碼以下:

 1 public class FifoPolicy extends AbstractPolicy {
 2 
 3     /**
 4      * The name of this policy as a string literal
 5      */
 6      public static final String NAME = "FIFO";
 7 
 8     /**
 9      * @return the name of the Policy. Inbuilt examples are LRU, LFU and FIFO.
10      */
11     public String getName() {
12         return NAME;
13     }
14 
15     /**
16      * Compares the desirableness for eviction of two elements
17      *
18      * Compares hit counts. If both zero,
19      *
20      * @param element1 the element to compare against
21      * @param element2 the element to compare
22      * @return true if the second element is preferable to the first element for ths policy
23      */
24     public boolean compare(Element element1, Element element2) {
25         return element2.getLatestOfCreationAndUpdateTime() < element1.getLatestOfCreationAndUpdateTime();
26 
27     }
28 }

注:
以creationAndUpdateTime最新或者最近的緩存淘汰。

四、這三個策略類統一繼承AbstractPolicy抽類
最關鍵的就是下面這個方法:

 1 public Element selectedBasedOnPolicy(Element[] sampledElements, Element justAdded) {
 2         //edge condition when Memory Store configured to size 0
 3         if (sampledElements.length == 1) {
 4             return sampledElements[0];
 5         }
 6         Element lowestElement = null;
 7         for (Element element : sampledElements) {
 8             if (element == null) {
 9                 continue;
10             }
11             if (lowestElement == null) {
12                 if (!element.equals(justAdded)) {
13                     lowestElement = element;
14                 }
15             } else if (compare(lowestElement, element) && !element.equals(justAdded)) {
16                 lowestElement = element;
17             }
18 
19         }
20         return lowestElement;
21     }

注:
一、這個方法主要是從取樣節點中查找須要淘汰的緩存。
二、最關鍵的就是調用compare這個方法其實就是調用的上面那三個策略實現的方法來找個能夠淘汰的緩存節點。

那麼接下來咱們看一下淘汰緩存的生命週期流程是怎麼樣的。


Paste_Image.png

12.2 EhcacheManager類解析
這個類是2.10.2版本的最核心類,初始化、建立緩存、獲取緩存都會用到這個類,這個類裏面有幾十個方法很是多,咱們會按照類別分別進行介紹,先看其構造方法吧。


Paste_Image.png

先看方法CacheManager()默認的狀況代碼以下:

1 public CacheManager() throws CacheException {
2         // default config will be done
3         status = Status.STATUS_UNINITIALISED;
4         init(null, null, null, null);
5 }

Status.STATUS_UNINITIALISED這句的意思是緩存未被初始化,在構造方法裏面要設定一個初始狀態。

咱們接着看init方法,這個方法是有別於其餘構造方法的,由於默認的狀況下這個方法的參數全傳null值,這就意味着使用ehcache本身默認的配置了。
代碼以下:

 1 protected synchronized void init(Configuration initialConfiguration, String configurationFileName, URL configurationURL,
 2             InputStream configurationInputStream) {
 3         Configuration configuration;
 4 
 5         if (initialConfiguration == null) {
 6             configuration = parseConfiguration(configurationFileName, configurationURL, configurationInputStream);
 7         } else {
 8             configuration = initialConfiguration;
 9         }
10 
11         assertManagementRESTServiceConfigurationIsCorrect(configuration);
12         assertNoCacheManagerExistsWithSameName(configuration);
13 
14         try {
15             doInit(configuration);
16         } catch (Throwable t) {
17             if (terracottaClient != null) {
18                 terracottaClient.shutdown();
19             }
20 
21             if (statisticsExecutor != null) {
22                 statisticsExecutor.shutdown();
23             }
24 
25             if (featuresManager != null) {
26                 featuresManager.dispose();
27             }
28 
29             if (diskStorePathManager != null) {
30                 diskStorePathManager.releaseLock();
31             }
32 
33             if (cacheManagerTimer != null) {
34                 cacheManagerTimer.cancel();
35                 cacheManagerTimer.purge();
36             }
37 
38             synchronized (CacheManager.class) {
39                 final String name = CACHE_MANAGERS_REVERSE_MAP.remove(this);
40                 CACHE_MANAGERS_MAP.remove(name);
41             }
42             ALL_CACHE_MANAGERS.remove(this);
43             if (t instanceof CacheException) {
44                 throw (CacheException) t;
45             } else {
46                 throw new CacheException(t);
47             }
48         }
49     }

說明
一、首先要判斷initialConfiguration這個參數是否是爲空,判斷的狀況下確定是爲就直接調用了parseConfiguration這個方法,這個方法調用classpath默認路徑來查找配置文件內容,初始化完configuration之後調用doInit方法。
二、doInit方法主要用來初始化最大的本地堆大小,初始化最大的本地持久化磁盤設置大小,集羣模式,事務設置等等。

12.3 Cache類解析

cache的類繼承結構以下所示:


Paste_Image.png


說明:
Ehcache接口是整個緩存的核心接口,該接口提供的方法能夠直接操做緩存,好比put,get等方法。因爲方法太多咱們只單拿出來put和get方法作一個深刻分析。

先看put方法源碼:

 1  private void putInternal(Element element, boolean doNotNotifyCacheReplicators, boolean useCacheWriter) {
 2         putObserver.begin();
 3         if (useCacheWriter) {
 4             initialiseCacheWriterManager(true);
 5         }
 6 
 7         checkStatus();
 8 
 9         if (disabled) {
10             putObserver.end(PutOutcome.IGNORED);
11             return;
12         }
13 
14         if (element == null) {
15             if (doNotNotifyCacheReplicators) {
16 
17                 LOG.debug("Element from replicated put is null. This happens because the element is a SoftReference" +
18                         " and it has been collected. Increase heap memory on the JVM or set -Xms to be the same as " +
19                         "-Xmx to avoid this problem.");
20 
21             }
22             putObserver.end(PutOutcome.IGNORED);
23             return;
24         }
25 
26 
27         if (element.getObjectKey() == null) {
28             putObserver.end(PutOutcome.IGNORED);
29             return;
30         }
31 
32         element.resetAccessStatistics();
33 
34         applyDefaultsToElementWithoutLifespanSet(element);
35 
36         backOffIfDiskSpoolFull();
37         element.updateUpdateStatistics();
38         boolean elementExists = false;
39         if (useCacheWriter) {
40             boolean notifyListeners = true;
41             try {
42                 elementExists = !compoundStore.putWithWriter(element, cacheWriterManager);
43             } catch (StoreUpdateException e) {
44                 elementExists = e.isUpdate();
45                 notifyListeners = configuration.getCacheWriterConfiguration().getNotifyListenersOnException();
46                 RuntimeException cause = e.getCause();
47                 if (cause instanceof CacheWriterManagerException) {
48                     throw ((CacheWriterManagerException)cause).getCause();
49                 }
50                 throw cause;
51             } finally {
52                 if (notifyListeners) {
53                     notifyPutInternalListeners(element, doNotNotifyCacheReplicators, elementExists);
54                 }
55             }
56         } else {
57             elementExists = !compoundStore.put(element);
58             notifyPutInternalListeners(element, doNotNotifyCacheReplicators, elementExists);
59         }
60         putObserver.end(elementExists ? PutOutcome.UPDATED : PutOutcome.ADDED);
61 
62     }

說明:
一、代碼的邏輯其實很簡單,咱們看一下compoundStore這個類,實際上咱們緩存的數據最終都是要到這個類裏面進行存儲的。
二、代碼裏面使用了putObserver觀察者對象主要是用來作計數統計任務用的。

看一下compoundStore類的繼承結構圖以下:


Paste_Image.png


經過圖中能夠看到全部的存儲類都實現Store接口類,大概有如下幾種存儲方式:
一、集羣方式:ClusteredStore
二、緩存方式:CacheStore
三、內存方式:MemoryStore
四、磁盤方式:DiskStore

咱們以DiskStore爲例深刻講解磁盤的部分源碼分析。

 1 writeLock().lock();
 2         try {
 3             // ensure capacity
 4             if (count + 1 > threshold) {
 5                 rehash();
 6             }
 7             HashEntry[] tab = table;
 8             int index = hash & (tab.length - 1);
 9             HashEntry first = tab[index];
10             HashEntry e = first;
11             while (e != null && (e.hash != hash || !key.equals(e.key))) {
12                 e = e.next;
13             }
14 
15             Element oldElement;
16             if (e != null) {
17                 DiskSubstitute onDiskSubstitute = e.element;
18                 if (!onlyIfAbsent) {
19                     e.element = encoded;
20                     installed = true;
21                     oldElement = decode(onDiskSubstitute);
22 
23                     free(onDiskSubstitute);
24                     final long existingHeapSize = onHeapPoolAccessor.delete(onDiskSubstitute.onHeapSize);
25                     LOG.debug("put updated, deleted {} on heap", existingHeapSize);
26 
27                     if (onDiskSubstitute instanceof DiskStorageFactory.DiskMarker) {
28                         final long existingDiskSize = onDiskPoolAccessor.delete(((DiskStorageFactory.DiskMarker) onDiskSubstitute).getSize());
29                         LOG.debug("put updated, deleted {} on disk", existingDiskSize);
30                     }
31                     e.faulted.set(faulted);
32                     cacheEventNotificationService.notifyElementUpdatedOrdered(oldElement, element);
33                 } else {
34                     oldElement = decode(onDiskSubstitute);
35 
36                     free(encoded);
37                     final long outgoingHeapSize = onHeapPoolAccessor.delete(encoded.onHeapSize);
38                     LOG.debug("put if absent failed, deleted {} on heap", outgoingHeapSize);
39                 }
40             } else {
41                 oldElement = null;
42                 ++modCount;
43                 tab[index] = new HashEntry(key, hash, first, encoded, new AtomicBoolean(faulted));
44                 installed = true;
45                 // write-volatile
46                 count = count + 1;
47                 cacheEventNotificationService.notifyElementPutOrdered(element);
48             }
49             return oldElement;
50 
51         } finally {
52             writeLock().unlock();
53 
54             if (installed) {
55                 encoded.installed();
56             }
57         }

說明:
一、流程採用寫鎖,先將這段代碼鎖定。
二、程序中有HashEntry[] tab這樣一個桶,每一個桶中存儲一個鏈表,首先經過hash & (tab -1) 也就是key的hash值與桶的長度減1取餘得出一個桶的index。而後取出鏈表實體,獲得當前鏈表實體的下一個元素,若是元素爲null則直接將元素賦值,不然取出舊的元素用新元素替換,釋放舊元素空間,返回舊元素。

十3、Guava Cache的使用與實現

Guava Cache與ConcurrentMap很類似,但也不徹底同樣。最基本的區別是ConcurrentMap會一直保存全部添加的元素,直到顯式地移除。相對地,Guava Cache爲了限制內存佔用,一般都設定爲自動回收元素。在某些場景下,儘管LoadingCache 不回收元素,它也是頗有用的,由於它會自動加載緩存。

一般來講,Guava Cache
適用於:
你願意消耗一些內存空間來提高速度。
你預料到某些鍵會被查詢一次以上。
緩存中存放的數據總量不會超出內存容量。(Guava Cache是單個應用運行時的本地緩存。它不把數據存放到文件或外部服務器。若是這不符合你的需求,請嘗試Memcached或者Redis等集中式緩存。

Guava Cache是一個全內存的本地緩存實現,它提供了線程安全的實現機制。

Guava Cache有兩種建立方式:

  1. CacheLoader  
  2. Callable callback

13.1 CacheLoader方式
先看一段示例代碼以下:

 1  public static void main(String[] args) throws ExecutionException, InterruptedException {
 2         //緩存接口這裏是LoadingCache,LoadingCache在緩存項不存在時能夠自動加載緩存
 3         LoadingCache<Integer, String> strCache
 4                 //CacheBuilder的構造函數是私有的,只能經過其靜態方法newBuilder()來得到CacheBuilder的實例
 5                 = CacheBuilder.newBuilder()
 6                 //設置併發級別爲8,併發級別是指能夠同時寫緩存的線程數
 7                 .concurrencyLevel(8)
 8                 //設置寫緩存後8秒鐘過時
 9                 .expireAfterWrite(8, TimeUnit.SECONDS)
10                 //設置緩存容器的初始容量爲10
11                 .initialCapacity(10)
12                 //設置緩存最大容量爲100,超過100以後就會按照LRU最近雖少使用算法來移除緩存項
13                 .maximumSize(100)
14                 //設置要統計緩存的命中率
15                 .recordStats()
16                 //設置緩存的移除通知
17                 .removalListener(new RemovalListener<Object, Object>() {
18                     public void onRemoval(RemovalNotification<Object, Object> notification) {
19                         System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
20                     }
21                 })
22                 //build方法中能夠指定CacheLoader,在緩存不存在時經過CacheLoader的實現自動加載緩存
23                 .build(
24                         new CacheLoader<Integer, String>() {
25                             @Override
26                             public String load(Integer key) throws Exception {
27                                 System.out.println("load data: " + key);
28                                 String str = key + ":cache-value";
29                                 return str;
30                             }
31                         }
32                 );
33 
34         for (int i = 0; i < 20; i++) {
35             //從緩存中獲得數據,因爲咱們沒有設置過緩存,因此須要經過CacheLoader加載緩存數據
36             String str = strCache.get(1);
37             System.out.println(str);
38             //休眠1秒
39             TimeUnit.SECONDS.sleep(1);
40         }
41 
42         System.out.println("cache stats:");
43         //最後打印緩存的命中率等 狀況
44         System.out.println(strCache.stats().toString());
45     }

運行結果以下:


Paste_Image.png


說明:
guava中使用緩存須要先聲明一個CacheBuilder對象,並設置緩存的相關參數,而後調用其build方法得到一個Cache接口的實例。

13.2 Callable方式
方法原型以下:get(K, Callable<V>)
這個方法返回緩存中相應的值,若是未獲取到緩存值則調用Callable方法。這個方法簡便地實現了模式"若是有緩存則返回;不然運算、緩存、而後返回"。
看示例代碼以下:

 1 Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(1000).build();  
 2         String resultVal = cache.get("test", new Callable<String>() {  
 3             public String call() {  
 4                 //未根據key查到對應緩存,設置緩存
 5                 String strProValue="test-value"             
 6                 return strProValue;
 7             }  
 8         });  
 9 
10       System.out.println("return value : " + resultVal);  
11     }

13.3 緩存過時刪除
guava的cache數據過時刪除的方式有二種,分別是主動刪除和被動刪除二種。

被動刪除三種方式

  • 基於條數限制的刪除
    使用CacheBuilder.maximumSize(long)方法進行設置。
    注意點:
    一、這個size不是容量大小,而是記錄條數。
    二、使用CacheLoader方式加載緩存的時候,在併發狀況下若是一個key過時刪除,正好同時有一個請求獲取緩存,有可能會報錯。

  • 基於過時時間刪除
    在Guava Cache中提供了二個方法能夠基於過時時間刪除
    一、expireAfterAccess(long, TimeUnit):某個key最後一次訪問後,再隔多長時間後刪除。
    二、expireAfterWrite(long, TimeUnit):某個key被建立後,再隔多長時間後刪除。

  • 基於引用的刪除
    經過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache能夠把緩存設置爲容許垃圾回收。

主動刪除三種方式

      • 個別清除:Cache.invalidate(key)
      • 批量清除:Cache.invalidateAll(keys)
      • 清除全部緩存項:Cache.invalidateAll()
相關文章
相關標籤/搜索