天天看點

玩轉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、關于磁盤持久化

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

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

6.3、通過xml配置檔案建立CacheManager

注:

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:

七、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多點傳播方式

示例:

<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消息模式

原理:這種模式的核心就是一個消息隊列,每個應用節點都訂閱預先定義好的主題,同時,節點有元素更新時,也會釋出更新元素到主題中去。各個應用伺服器節點通過偵聽MQ擷取到最新的資料,然後分别更新自己的Ehcache緩存,Ehcache預設支援ActiveMQ,我們也可以通過自定義元件的方式實作類似Kafka,RabbitMQ。

4、Cache Server模式

原理:這種模式會存在主從節點。

缺陷:緩存容易出現資料不一緻的問題,

11.3、使用Ehcache的瓶頸是什麼

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

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

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

在實際工作中,我更多是将Ehcache作為與Redis配合的二級緩存。

第一種方式:

這種方式通過應用伺服器的Ehcache定時輪詢Redis緩存伺服器更同步更新本地緩存,缺點是因為每台伺服器定時Ehcache的時間不一樣,那麼不同伺服器重新整理最新緩存的時間也不一樣,會産生資料不一緻問題,對一緻性要求不高可以使用。

第二種方式:

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

總結:

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

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

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

12.1 源碼淘汰政策解析

首先看一下類結構圖:

從類結構圖上看一共有三種緩存淘汰政策分别是: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這個方法其實就是調用的上面那三個政策實作的方法來找個可以淘汰的緩存節點。

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

12.2 EhcacheManager類解析

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

先看方法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的類繼承結構如下所示:

說明:

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類的繼承結構圖如下:

通過圖中可以看到所有的存儲類都實作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());
    }
           

運作結果如下:

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()