玩轉EhCache之最簡單的緩存框架

一、簡介

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

備注:為了方便大家了最新版本的Ehcache,本文中1-6節采用的最新的Ehcache3.0的特性和使用介紹,從第7節開始采用的是Ehcache2.10.2版本來與Spring相結合來做案例介紹,包括后面的源碼分析也將采用這個版本

二、主要特性

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

三、Ehcache的架構設計圖


說明
CacheManager:是緩存管理器,可以通過單例或者多例的方式創建,也是Ehcache的入口類。
Cache:每個CacheManager可以管理多個Cache,每個Cache可以采用hash的方式管理多個Element。
Element:用于存放真正緩存內容的。

結構圖如下所示:

四、Ehcache的緩存數據淘汰策略

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

五、Ehcache的緩存數據過期策略

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

六、Ehcache緩存的使用介紹

6.1、目前最新的Ehcache是3.0版本,我們也就使用3.0版本來介紹它的使用介紹,看如下代碼:

Paste_Image.png

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

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

3、最后調用.build方法返回一個完整的實例,當然我們也能使用CacheManager來初始化。
4、在你開始使用CacheManager的時候,需要使用init()方法進行初始化。
5、我們能取回在第二步中設定的pre-configured別名,我們對于key和要傳遞的值類型,要求是類型安全的,否則將拋出ClassCastException異常。
6、可以根據需求,通過CacheManager創建出新的Cache。實例化和完整實例化的Cache將通過CacheManager.getCache API返回。
7、使用put方法存儲數據。
8、使用get方法獲取數據。
9、我們可以通過CacheManager.removeCache方法來獲取Cache,但是Cache取出來以后CacheManager將會刪除自身保存的Cache實例。
10、close方法將釋放CacheManager所管理的緩存資源。

6.2、關于磁盤持久化

Paste_Image.png

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

上面的例子其實是分配了非常少的磁盤存儲量,不過我們需要注意的是由于存儲在磁盤上我們需要做序列化和反序列化,以及讀和寫的操作。它的速度肯定比內存要慢的多。

6.3、通過xml配置文件創建CacheManager

Paste_Image.png

注:
1、描述緩存的別名。
2、foo的key的類型指定為String類型,而value并沒有指定類型,默認就是Object類型。
3、可以在堆中為foo創建2000個實體。
4、在開始淘汰過期緩存項之前,可以分配多達500M的堆內存。
5、cache-template可以實現一個配置抽象,以便在未來可以進行擴展。
6、bar使用了cache-template模板myDefaults,并且覆蓋了key-type類型,myDefaults的key-type是Long類型,覆蓋后成了Number類型。

使用以下代碼創建CacheManager:


Paste_Image.png

七、UserManagerCache介紹

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

** 7.2 使用示例**
1、基本示例

UserManagedCache<Long, String> userManagedCache =
    UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
        .build(false); 
userManagedCache.init(); 

userManagedCache.put(1L, "da one!"); 

userManagedCache.close(); 

2、持久化示例

LocalPersistenceService persistenceService = new DefaultLocalPersistenceService(new DefaultPersistenceConfiguration(new File(getStoragePath(), "myUserData"))); 

PersistentUserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
    .with(new UserManagedPersistenceContext<Long, String>("cache-name", persistenceService)) 
    .withResourcePools(ResourcePoolsBuilder.newResourcePoolsBuilder()
        .heap(10L, EntryUnit.ENTRIES)
        .disk(10L, MemoryUnit.MB, true)) 
    .build(true);

// Work with the cache
cache.put(42L, "The Answer!");
assertThat(cache.get(42L), is("The Answer!"));

cache.close(); 
cache.destroy(); 

3、讀寫緩存示例

UserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
    .withLoaderWriter(new SampleLoaderWriter<Long, String>()) 
    .build(true);

// Work with the cache
cache.put(42L, "The Answer!");
assertThat(cache.get(42L), is("The Answer!"));

cache.close();

注:
如果你希望頻繁的讀和寫緩存,則可以使用CacheLoaderWriter。

4、緩存淘汰策略示例

UserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
    .withEvictionAdvisor(new OddKeysEvictionAdvisor<Long, String>()) 
    .withResourcePools(ResourcePoolsBuilder.newResourcePoolsBuilder()
        .heap(2L, EntryUnit.ENTRIES)) 
    .build(true);

// Work with the cache
cache.put(42L, "The Answer!");
cache.put(41L, "The wrong Answer!");
cache.put(39L, "The other wrong Answer!");

cache.close(); 

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

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

UserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
    .withSizeOfMaxObjectSize(500, MemoryUnit.B)
    .withSizeOfMaxObjectGraph(1000) 
    .withResourcePools(ResourcePoolsBuilder.newResourcePoolsBuilder()
        .heap(3, MemoryUnit.MB)) 
    .build(true);

cache.put(1L, "Put");
cache.put(1L, "Update");

assertThat(cache.get(1L), is("Update"));

cache.close();

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

八、緩存的使用模式

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

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

v = cache.get(k)
if(v == null) {
    v = sor.get(k)
    cache.put(k, v)
}

v = newV
sor.put(k, v)
cache.put(k, v)

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

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

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

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

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

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

public class User {  
    public Integer id;  
    public String name;  
    public String password;  
      
    // 這個需要,不然在實體綁定的時候出錯  
    public User(){}  
      
    public User(Integer id, String name, String password) {  
        super();  
        this.id = id;  
        this.name = name;  
        this.password = password;  
    }  
      
    public Integer getId() {  
        return id;  
    }  
    public void setId(Integer id) {  
        this.id = id;  
    }  
    public String getName() {  
        return name;  
    }  
    public void setName(String name) {  
        this.name = name;  
    }  
    public String getPassword() {  
        return password;  
    }  
    public void setPassword(String password) {  
        this.password = password;  
    }  
  
    @Override  
    public String toString() {  
        return "User [id=" + id + ", name=" + name + ", password=" + password  
                + "]";  
    }  
}  

UserDAO代碼如下:

@Repository("userDao")  
public class UserDao {  
    List<User> userList = initUsers();  
      
    public User findById(Integer id) { 
        for(User user : userList){  
            if(user.getId().equals(id)){  
                 return user;
            }  
        }  
        return null;  
    }  
      
    public void removeById(Integer id) { 
        User delUser = null;
        for(User user : userList){  
            if(user.getId().equals(id)){  
                  delUser = user;
            }  
        }  
        users.remove(delUser);  
    }  
      
    public void addUser(User user){  
        users.add(user);  
    }  
      
    public void updateUser(User user){  
        addUser(user);  
    }  
 
    private List<User> initUsers(){  
        List<User> users = new ArrayList<User>();  
        User u1 = new User(1,"張三","123");  
        User u2 = new User(2,"李四","124");  
        User u3 = new User(3,"王五","125");  
        users.add(u1);  
        users.add(u2);  
        users.add(u3);  
        return users;  
    }  
}  

UserService代碼如下:

@Service("userService")  
public class UserService {  
      
    @Autowired  
    private UserDao userDao;  
      
    // 查詢所有數據,不需要key
    @Cacheable(value = "serviceCache")  
    public List<User> getAll(){  
        printInfo("getAll");  
        return userDao.users;  
    }  
    //根據ID來獲取數據,ID可能是主鍵也可能是唯一鍵
    @Cacheable(value = "serviceCache", key="#id")  
    public User findById(Integer id){  
        printInfo(id);  
        return userDao.findById(id);  
    }  
    //通過ID進行刪除 
    @CacheEvict(value = "serviceCache", key="#id")  
    public void removeById(Integer id){  
        userDao.removeById(id);  
    }  
    
   //添加數據
    public void addUser(User user){  
        if(user != null && user.getId() != null){  
            userDao.addUser(user);  
        }  
    }  
    // key 支持條件,包括 屬性condition ,可以 id < 20 等等
    @CacheEvict(value="serviceCache", key="#u.id")  
    public void updateUser(User u){  
        removeById(u.getId());  
        userDao.updateUser(u);  
    }  

   // allEntries 表示調用之后,清空緩存,默認false,  
    // 還有個beforeInvocation 屬性,表示先清空緩存,再進行查詢  
    @CacheEvict(value="serviceCache",allEntries=true)  
    public void removeAll(){  
        System.out.println("清除所有緩存");  
    } 

    private void printInfo(Object str){  
        System.out.println("非緩存查詢----------findById"+str);  
    } 
}  

ehcache配置文件內容如下

<cache name="serviceCache"
    eternal="false"  
    maxElementsInMemory="100" 
    overflowToDisk="false" 
    diskPersistent="false"  
    timeToIdleSeconds="0" 
    timeToLiveSeconds="300"  
    memoryStoreEvictionPolicy="LRU" /> 
</ehcache> 

Spring配置文件內容如下:

    <bean id="cacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">  
        <property name="configLocation"  value="classpath:com/config/ehcache.xml"/> 
    </bean> 
    
    <!-- 支持緩存注解 -->
    <cache:annotation-driven cache-manager="cacheManager" />
    
    <!-- 默認是cacheManager -->
    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">  
        <property name="cacheManager"  ref="cacheManagerFactory"/>  
    </bean>  

十、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集群的基本概念
1、成員發現(Peer Discovery)
Ehcache集群概念中有一個cache組,每個cache都是另一個cache的peer,并不像Redis或者其他分布式組件一樣有一個主的存在,Ehcache并沒有主Cache,可是那如何知道集群中的其他緩存都有誰呢?這個就是成員發現。

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

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

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

成員每秒向群組發送一個“心跳”。如果一個成員 5秒種都沒有發出信號它將被群組移除。如果一個新的成員發送了一個“心跳”它將被添加進群組。

任何一個用這個配置安裝了復制功能的cache都將被其他的成員發現并標識為可用狀態。

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

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

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

<!-- spring cache 配置 -->  
<!-- 啟用緩存注解功能 -->  
<cache:annotation-driven cache-manager="cacheManager"/>  
  
<!-- cacheManager工廠類,指定ehcache.xml的位置 -->  
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"  
      p:configLocation="classpath:ehcache/ehcache.xml"/>  
  
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"  
      p:cacheManager-ref="ehcache"/>  
  
<cache:annotation-driven />  

Ehcache配置文件內容如下:

<?xml version="1.0" encoding="UTF-8"?>  
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">  
    
    <!--EHCache分布式緩存集群環境配置-->  
    <!--rmi手動配置-->  
    <cacheManagerPeerProviderFactory class= "net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"  
              properties="peerDiscovery=manual,rmiUrls=//localhost:40000/user"/>  
  
    <cacheManagerPeerListenerFactory  
            class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"  
            properties="hostName=localhost,port=40001, socketTimeoutMillis=120000"/>  
    <defaultCache  
            maxElementsInMemory="10000"  
            eternal="false"  
            timeToIdleSeconds="120"  
            timeToLiveSeconds="120"  
            overflowToDisk="true"  
            diskSpoolBufferSizeMB="30"  
            maxElementsOnDisk="10000000"  
            diskPersistent="false"  
            diskExpiryThreadIntervalSeconds="120"  
            memoryStoreEvictionPolicy="LRU">  
        <cacheEventListenerFactory  
                class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>  
    </defaultCache>  
    <cache name="user"  
           maxElementsInMemory="1000"  
           eternal="false"  
           timeToIdleSeconds="100000"  
           timeToLiveSeconds="100000"  
           overflowToDisk="false">  
        <cacheEventListenerFactory  
                class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>  
    </cache>  
</ehcache>  

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

十一、Ehcache的使用場景

11.1、Ehcache使用的注意點

1、比較少的更新數據表的情況
2、對并發要求不是很嚴格的情況
多臺應用服務器中的緩存是不能進行實時同步的。
3、對一致性要求不高的情況下
因為Ehcache本地緩存的特性,目前無法很好的解決不同服務器間緩存同步的問題,所以我們在一致性要求非常高的場合下,盡量使用Redis、Memcached等集中式緩存。

11.2、Ehcache在集群、分布式的情況下表現如何

在分布式情況下有二種同步方式:
1、RMI組播方式

Paste_Image.png

示例:

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

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

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

3、JMS消息模式

Paste_Image.png

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

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

Paste_Image.png

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

11.3、使用Ehcache的瓶頸是什么

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

2、數據庫瓶頸(Database Bottlenecks ):對于單實例的應用來說,緩存可以保護數據庫的讀風暴;但是,在集群的環境下,每一個應用節點都要定期保持數據最新,節點越多,要維持這樣的情況對數據庫的開銷也越大。

11.4、實際工作中如何使用Ehcache

在實際工作中,我更多是將Ehcache作為與Redis配合的二級緩存。
第一種方式:

Paste_Image.png

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

第二種方式:

Paste_Image.png

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

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

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

**十二、Ehcache2.10.2源碼分析 **

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

Paste_Image.png

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

public class LruPolicy extends AbstractPolicy {

    /**
     * The name of this policy as a string literal
     */
     public static final String NAME = "LRU";

    /**
     * @return the name of the Policy. Inbuilt examples are LRU, LFU and FIFO.
     */
    public String getName() {
        return NAME;
    }

    /**
     * Compares the desirableness for eviction of two elements
     *
     * Compares hit counts. If both zero,
     *
     * @param element1 the element to compare against
     * @param element2 the element to compare
     * @return true if the second element is preferable to the first element for ths policy
     */
    public boolean compare(Element element1, Element element2) {
        return element2.getLastAccessTime() < element1.getLastAccessTime();

    }

注:
accessTime小的緩存淘汰。

2、LFUPolicy代碼如下:

public class LfuPolicy extends AbstractPolicy {

    /**
     * The name of this policy as a string literal
     */
    public static final String NAME = "LFU";

    /**
     * @return the name of the Policy. Inbuilt examples are LRU, LFU and FIFO.
     */
    public String getName() {
        return NAME;
    }

    /**
     * Compares the desirableness for eviction of two elements
     *
     * Compares hit counts. If both zero, 
     *
     * @param element1 the element to compare against
     * @param element2 the element to compare
     * @return true if the second element is preferable to the first element for ths policy
     */
    public boolean compare(Element element1, Element element2) {
        return element2.getHitCount() < element1.getHitCount();
        
    }
}

注:
hit值小的緩存淘汰。

3、FIFOPolicy代碼如下:

public class FifoPolicy extends AbstractPolicy {

    /**
     * The name of this policy as a string literal
     */
     public static final String NAME = "FIFO";

    /**
     * @return the name of the Policy. Inbuilt examples are LRU, LFU and FIFO.
     */
    public String getName() {
        return NAME;
    }

    /**
     * Compares the desirableness for eviction of two elements
     *
     * Compares hit counts. If both zero,
     *
     * @param element1 the element to compare against
     * @param element2 the element to compare
     * @return true if the second element is preferable to the first element for ths policy
     */
    public boolean compare(Element element1, Element element2) {
        return element2.getLatestOfCreationAndUpdateTime() < element1.getLatestOfCreationAndUpdateTime();

    }
}

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

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

public Element selectedBasedOnPolicy(Element[] sampledElements, Element justAdded) {
        //edge condition when Memory Store configured to size 0
        if (sampledElements.length == 1) {
            return sampledElements[0];
        }
        Element lowestElement = null;
        for (Element element : sampledElements) {
            if (element == null) {
                continue;
            }
            if (lowestElement == null) {
                if (!element.equals(justAdded)) {
                    lowestElement = element;
                }
            } else if (compare(lowestElement, element) && !element.equals(justAdded)) {
                lowestElement = element;
            }

        }
        return lowestElement;
    }

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

那么接下來我們看一下淘汰緩存的生命周期流程是怎么樣的。

Paste_Image.png

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

Paste_Image.png

先看方法CacheManager()默認的情況代碼如下:

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

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

我們接著看init方法,這個方法是有別于其他構造方法的,因為默認的情況下這個方法的參數全傳null值,這就意味著使用ehcache自己默認的配置了。
代碼如下:

protected synchronized void init(Configuration initialConfiguration, String configurationFileName, URL configurationURL,
            InputStream configurationInputStream) {
        Configuration configuration;

        if (initialConfiguration == null) {
            configuration = parseConfiguration(configurationFileName, configurationURL, configurationInputStream);
        } else {
            configuration = initialConfiguration;
        }

        assertManagementRESTServiceConfigurationIsCorrect(configuration);
        assertNoCacheManagerExistsWithSameName(configuration);

        try {
            doInit(configuration);
        } catch (Throwable t) {
            if (terracottaClient != null) {
                terracottaClient.shutdown();
            }

            if (statisticsExecutor != null) {
                statisticsExecutor.shutdown();
            }

            if (featuresManager != null) {
                featuresManager.dispose();
            }

            if (diskStorePathManager != null) {
                diskStorePathManager.releaseLock();
            }

            if (cacheManagerTimer != null) {
                cacheManagerTimer.cancel();
                cacheManagerTimer.purge();
            }

            synchronized (CacheManager.class) {
                final String name = CACHE_MANAGERS_REVERSE_MAP.remove(this);
                CACHE_MANAGERS_MAP.remove(name);
            }
            ALL_CACHE_MANAGERS.remove(this);
            if (t instanceof CacheException) {
                throw (CacheException) t;
            } else {
                throw new CacheException(t);
            }
        }
    }

說明
1、首先要判斷initialConfiguration這個參數是不是為空,判斷的情況下肯定是為就直接調用了parseConfiguration這個方法,這個方法調用classpath默認路徑來查找配置文件內容,初始化完configuration以后調用doInit方法。
2、doInit方法主要用來初始化最大的本地堆大小,初始化最大的本地持久化磁盤設置大小,集群模式,事務設置等等。

12.3 Cache類解析

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


Paste_Image.png

說明:
Ehcache接口是整個緩存的核心接口,該接口提供的方法可以直接操作緩存,比如put,get等方法。由于方法太多我們只單拿出來put和get方法做一個深入分析。

先看put方法源碼:

 private void putInternal(Element element, boolean doNotNotifyCacheReplicators, boolean useCacheWriter) {
        putObserver.begin();
        if (useCacheWriter) {
            initialiseCacheWriterManager(true);
        }

        checkStatus();

        if (disabled) {
            putObserver.end(PutOutcome.IGNORED);
            return;
        }

        if (element == null) {
            if (doNotNotifyCacheReplicators) {

                LOG.debug("Element from replicated put is null. This happens because the element is a SoftReference" +
                        " and it has been collected. Increase heap memory on the JVM or set -Xms to be the same as " +
                        "-Xmx to avoid this problem.");

            }
            putObserver.end(PutOutcome.IGNORED);
            return;
        }


        if (element.getObjectKey() == null) {
            putObserver.end(PutOutcome.IGNORED);
            return;
        }

        element.resetAccessStatistics();

        applyDefaultsToElementWithoutLifespanSet(element);

        backOffIfDiskSpoolFull();
        element.updateUpdateStatistics();
        boolean elementExists = false;
        if (useCacheWriter) {
            boolean notifyListeners = true;
            try {
                elementExists = !compoundStore.putWithWriter(element, cacheWriterManager);
            } catch (StoreUpdateException e) {
                elementExists = e.isUpdate();
                notifyListeners = configuration.getCacheWriterConfiguration().getNotifyListenersOnException();
                RuntimeException cause = e.getCause();
                if (cause instanceof CacheWriterManagerException) {
                    throw ((CacheWriterManagerException)cause).getCause();
                }
                throw cause;
            } finally {
                if (notifyListeners) {
                    notifyPutInternalListeners(element, doNotNotifyCacheReplicators, elementExists);
                }
            }
        } else {
            elementExists = !compoundStore.put(element);
            notifyPutInternalListeners(element, doNotNotifyCacheReplicators, elementExists);
        }
        putObserver.end(elementExists ? PutOutcome.UPDATED : PutOutcome.ADDED);

    }

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

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

Paste_Image.png

通過圖中可以看到所有的存儲類都實現Store接口類,大概有以下幾種存儲方式:
1、集群方式:ClusteredStore
2、緩存方式:CacheStore
3、內存方式:MemoryStore
4、磁盤方式:DiskStore

我們以DiskStore為例深入講解磁盤的部分源碼分析。

writeLock().lock();
        try {
            // ensure capacity
            if (count + 1 > threshold) {
                rehash();
            }
            HashEntry[] tab = table;
            int index = hash & (tab.length - 1);
            HashEntry first = tab[index];
            HashEntry e = first;
            while (e != null && (e.hash != hash || !key.equals(e.key))) {
                e = e.next;
            }

            Element oldElement;
            if (e != null) {
                DiskSubstitute onDiskSubstitute = e.element;
                if (!onlyIfAbsent) {
                    e.element = encoded;
                    installed = true;
                    oldElement = decode(onDiskSubstitute);

                    free(onDiskSubstitute);
                    final long existingHeapSize = onHeapPoolAccessor.delete(onDiskSubstitute.onHeapSize);
                    LOG.debug("put updated, deleted {} on heap", existingHeapSize);

                    if (onDiskSubstitute instanceof DiskStorageFactory.DiskMarker) {
                        final long existingDiskSize = onDiskPoolAccessor.delete(((DiskStorageFactory.DiskMarker) onDiskSubstitute).getSize());
                        LOG.debug("put updated, deleted {} on disk", existingDiskSize);
                    }
                    e.faulted.set(faulted);
                    cacheEventNotificationService.notifyElementUpdatedOrdered(oldElement, element);
                } else {
                    oldElement = decode(onDiskSubstitute);

                    free(encoded);
                    final long outgoingHeapSize = onHeapPoolAccessor.delete(encoded.onHeapSize);
                    LOG.debug("put if absent failed, deleted {} on heap", outgoingHeapSize);
                }
            } else {
                oldElement = null;
                ++modCount;
                tab[index] = new HashEntry(key, hash, first, encoded, new AtomicBoolean(faulted));
                installed = true;
                // write-volatile
                count = count + 1;
                cacheEventNotificationService.notifyElementPutOrdered(element);
            }
            return oldElement;

        } finally {
            writeLock().unlock();

            if (installed) {
                encoded.installed();
            }
        }

說明:
1、流程采用寫鎖,先將這段代碼鎖定。
2、程序中有HashEntry[] tab這樣一個桶,每個桶中存儲一個鏈表,首先通過hash & (tab -1) 也就是key的hash值與桶的長度減1取余得出一個桶的index。然后取出鏈表實體,得到當前鏈表實體的下一個元素,如果元素為null則直接將元素賦值,否則取出舊的元素用新元素替換,釋放舊元素空間,返回舊元素。

十三、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方式
先看一段示例代碼如下:

 public static void main(String[] args) throws ExecutionException, InterruptedException {
        //緩存接口這里是LoadingCache,LoadingCache在緩存項不存在時可以自動加載緩存
        LoadingCache<Integer, String> strCache
                //CacheBuilder的構造函數是私有的,只能通過其靜態方法newBuilder()來獲得CacheBuilder的實例
                = CacheBuilder.newBuilder()
                //設置并發級別為8,并發級別是指可以同時寫緩存的線程數
                .concurrencyLevel(8)
                //設置寫緩存后8秒鐘過期
                .expireAfterWrite(8, TimeUnit.SECONDS)
                //設置緩存容器的初始容量為10
                .initialCapacity(10)
                //設置緩存最大容量為100,超過100之后就會按照LRU最近雖少使用算法來移除緩存項
                .maximumSize(100)
                //設置要統計緩存的命中率
                .recordStats()
                //設置緩存的移除通知
                .removalListener(new RemovalListener<Object, Object>() {
                    public void onRemoval(RemovalNotification<Object, Object> notification) {
                        System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
                    }
                })
                //build方法中可以指定CacheLoader,在緩存不存在時通過CacheLoader的實現自動加載緩存
                .build(
                        new CacheLoader<Integer, String>() {
                            @Override
                            public String load(Integer key) throws Exception {
                                System.out.println("load data: " + key);
                                String str = key + ":cache-value";
                                return str;
                            }
                        }
                );

        for (int i = 0; i < 20; i++) {
            //從緩存中得到數據,由于我們沒有設置過緩存,所以需要通過CacheLoader加載緩存數據
            String str = strCache.get(1);
            System.out.println(str);
            //休眠1秒
            TimeUnit.SECONDS.sleep(1);
        }

        System.out.println("cache stats:");
        //最后打印緩存的命中率等 情況
        System.out.println(strCache.stats().toString());
    }

運行結果如下:

Paste_Image.png

說明:
guava中使用緩存需要先聲明一個CacheBuilder對象,并設置緩存的相關參數,然后調用其build方法獲得一個Cache接口的實例。

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

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

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

被動刪除三種方式

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

  • 基于過期時間刪除
    在Guava Cache中提供了二個方法可以基于過期時間刪除
    1、expireAfterAccess(long, TimeUnit):某個key最后一次訪問后,再隔多長時間后刪除。
    2、expireAfterWrite(long, TimeUnit):某個key被創建后,再隔多長時間后刪除。

  • 基于引用的刪除
    通過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache可以把緩存設置為允許垃圾回收。

主動刪除三種方式

  • 個別清除:Cache.invalidate(key)
  • 批量清除:Cache.invalidateAll(keys)
  • 清除所有緩存項:Cache.invalidateAll()
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 理論總結 它要解決什么樣的問題? 數據的訪問、存取、計算太慢、太不穩定、太消耗資源,同時,這樣的操作存在重復性。因...
    jiangmo閱讀 2,924評論 0 11
  • Ehcache是現在最流行的純Java開源緩存框架,配置簡單、結構清晰、功能強大,最初知道它,是從Hibernat...
    安易學車閱讀 2,044評論 0 11
  • 原文連接:https://my.oschina.net/coolfire368/blog/123377 ehcac...
    晴天哥_王志閱讀 1,369評論 0 1
  • 第一章(接) 看到王源盯著小土豆一眨不眨的眼神,龔阿姨立刻笑瞇瞇地把小土豆拉到身前,介紹道:“源源,這是王俊凱...
    王先生的oo兔閱讀 293評論 0 0
  • 1,黑屏是啥?先看看黑屏的樣子吧。 2,為啥黑屏?想想黑屏常常發生在啥時候?場景1:開機360提醒你軟件更新,漏洞...
    龍少俠linux閱讀 774評論 0 3