一谷誓、簡(jiǎn)介
Ehcache是一個(gè)用Java實(shí)現(xiàn)的使用簡(jiǎn)單谓谦,高速,實(shí)現(xiàn)線程安全的緩存管理類庫(kù),ehcache提供了用內(nèi)存畅姊,磁盤文件存儲(chǔ)咒钟,以及分布式存儲(chǔ)方式等多種靈活的cache管理方案。同時(shí)ehcache作為開放源代碼項(xiàng)目若未,采用限制比較寬松的Apache License V2.0作為授權(quán)方式朱嘴,被廣泛地用于Hibernate, Spring,Cocoon等其他開源系統(tǒng)粗合。Ehcache 從 Hibernate 發(fā)展而來(lái)萍嬉,逐漸涵蓋了 Cahce 界的全部功能,是目前發(fā)展勢(shì)頭最好的一個(gè)項(xiàng)目。具有快速,簡(jiǎn)單,低消耗隙疚,依賴性小帚湘,擴(kuò)展性強(qiáng),支持對(duì)象或序列化緩存,支持緩存或元素的失效甚淡,提供 LRU大诸、LFU 和 FIFO 緩存策略,支持內(nèi)存緩存和磁盤緩存贯卦,分布式緩存機(jī)制等等特點(diǎn)资柔。
備注:為了方便大家了最新版本的Ehcache,本文中1-6節(jié)采用的最新的Ehcache3.0的特性和使用介紹撵割,從第7節(jié)開始采用的是Ehcache2.10.2版本來(lái)與Spring相結(jié)合來(lái)做案例介紹贿堰,包括后面的源碼分析也將采用這個(gè)版本
二、主要特性
快速啡彬;
簡(jiǎn)單羹与;
多種緩存策略;
緩存數(shù)據(jù)有兩級(jí):內(nèi)存和磁盤庶灿,因此無(wú)需擔(dān)心容量問題纵搁;
緩存數(shù)據(jù)會(huì)在虛擬機(jī)重啟的過程中寫入磁盤;
可以通過 RMI往踢、可插入 API 等方式進(jìn)行分布式緩存腾誉;
具有緩存和緩存管理器的偵聽接口;
支持多緩存管理器實(shí)例峻呕,以及一個(gè)實(shí)例的多個(gè)緩存區(qū)域利职;
提供 Hibernate 的緩存實(shí)現(xiàn);
三瘦癌、Ehcache的架構(gòu)設(shè)計(jì)圖
說(shuō)明
CacheManager:是緩存管理器猪贪,可以通過單例或者多例的方式創(chuàng)建,也是Ehcache的入口類讯私。
Cache:每個(gè)CacheManager可以管理多個(gè)Cache热押,每個(gè)Cache可以采用hash的方式管理多個(gè)Element西傀。
Element:用于存放真正緩存內(nèi)容的。
結(jié)構(gòu)圖如下所示:
四楞黄、Ehcache的緩存數(shù)據(jù)淘汰策略
FIFO:先進(jìn)先出
LFU:最少被使用,緩存的元素有一個(gè)hit屬性抡驼,hit值最小的將會(huì)被清出緩存鬼廓。
LRU:最近最少使用,緩存的元素有一個(gè)時(shí)間戳致盟,當(dāng)緩存容量滿了碎税,而又需要騰出地方來(lái)緩存新的元素的時(shí)候,那么現(xiàn)有緩存元素中時(shí)間戳離當(dāng)前時(shí)間最遠(yuǎn)的元素將被清出緩存馏锡。
五雷蹂、Ehcache的緩存數(shù)據(jù)過期策略
Ehcache采用的是懶淘汰機(jī)制,每次往緩存放入數(shù)據(jù)的時(shí)候杯道,都會(huì)存一個(gè)時(shí)間匪煌,在讀取的時(shí)候要和設(shè)置的時(shí)間做TTL比較來(lái)判斷是否過期。
六党巾、Ehcache緩存的使用介紹
6.1萎庭、目前最新的Ehcache是3.0版本,我們也就使用3.0版本來(lái)介紹它的使用介紹齿拂,看如下代碼:
注:這段代碼介紹了Ehcache3.0的緩存使用生命周期的一個(gè)過程驳规。
1、靜態(tài)方法CacheManagerBuilder.newCacheManagerBuilder將返回一個(gè)新的org.ehcache.config.builders.CacheManagerBuilder的實(shí)例署海。
2吗购、當(dāng)我們要構(gòu)建一個(gè)緩存管理器的時(shí)候,使用CacheManagerBuilder來(lái)創(chuàng)建一個(gè)預(yù)配置(pre-configured)緩存砸狞。
- 第一個(gè)參數(shù)是一個(gè)別名捻勉,用于Cache和Cachemanager進(jìn)行配合。
- 第二個(gè)參數(shù)是org.ehcache.config.CacheConfiguration主要用來(lái)配置Cache刀森。我們使用org.ehcache.config.builders.CacheConfigurationBuilder的靜態(tài)方法newCacheConfigurationBuilder來(lái)創(chuàng)建一個(gè)默認(rèn)配置實(shí)例贯底。
3、最后調(diào)用.build方法返回一個(gè)完整的實(shí)例撒强,當(dāng)然我們也能使用CacheManager來(lái)初始化禽捆。
4、在你開始使用CacheManager的時(shí)候飘哨,需要使用init()方法進(jìn)行初始化胚想。
5、我們能取回在第二步中設(shè)定的pre-configured別名芽隆,我們對(duì)于key和要傳遞的值類型浊服,要求是類型安全的统屈,否則將拋出ClassCastException異常。
6牙躺、可以根據(jù)需求愁憔,通過CacheManager創(chuàng)建出新的Cache。實(shí)例化和完整實(shí)例化的Cache將通過CacheManager.getCache API返回孽拷。
7吨掌、使用put方法存儲(chǔ)數(shù)據(jù)。
8脓恕、使用get方法獲取數(shù)據(jù)膜宋。
9、我們可以通過CacheManager.removeCache方法來(lái)獲取Cache炼幔,但是Cache取出來(lái)以后CacheManager將會(huì)刪除自身保存的Cache實(shí)例秋茫。
10、close方法將釋放CacheManager所管理的緩存資源乃秀。
6.2肛著、關(guān)于磁盤持久化
注:如果您想使用持久化機(jī)制,就需要提供一個(gè)磁盤存儲(chǔ)的位置給CacheManagerBuilder.persistence這個(gè)方法跺讯,另外在使用的過程中策泣,你還需要定義一個(gè)磁盤使用的資源池。
上面的例子其實(shí)是分配了非常少的磁盤存儲(chǔ)量抬吟,不過我們需要注意的是由于存儲(chǔ)在磁盤上我們需要做序列化和反序列化萨咕,以及讀和寫的操作。它的速度肯定比內(nèi)存要慢的多火本。
6.3危队、通過xml配置文件創(chuàng)建CacheManager
注:
1、描述緩存的別名钙畔。
2茫陆、foo的key的類型指定為String類型,而value并沒有指定類型擎析,默認(rèn)就是Object類型簿盅。
3、可以在堆中為foo創(chuàng)建2000個(gè)實(shí)體揍魂。
4桨醋、在開始淘汰過期緩存項(xiàng)之前,可以分配多達(dá)500M的堆內(nèi)存现斋。
5喜最、cache-template可以實(shí)現(xiàn)一個(gè)配置抽象,以便在未來(lái)可以進(jìn)行擴(kuò)展庄蹋。
6瞬内、bar使用了cache-template模板myDefaults迷雪,并且覆蓋了key-type類型,myDefaults的key-type是Long類型虫蝶,覆蓋后成了Number類型章咧。
使用以下代碼創(chuàng)建CacheManager:
七、UserManagerCache介紹
** 7.1 什么是UserManagerCache能真,它能做什么赁严?**
UserManagerCache這是在Ehcache3.0中引入的新的概念,它將直接創(chuàng)建緩存而不需要使用CacheManager來(lái)進(jìn)行管理舟陆。所以這也就是UserManagerCache名稱的由來(lái)误澳。
由于沒有CacheManager的管理耻矮,用戶就必須要手動(dòng)配置所需要的服務(wù)秦躯,當(dāng)然如果你發(fā)現(xiàn)要使用大量的服務(wù),那么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();
注:
如果你想使用緩存淘汰算法來(lái)淘汰數(shù)據(jù)载荔,則要使用EvictionAdvisor這個(gè)類。
5采桃、按字節(jié)設(shè)定的緩存示例
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這個(gè)主要是調(diào)整可以設(shè)置多少字節(jié)對(duì)象懒熙。
.heap方法主要是設(shè)置每個(gè)對(duì)象最大可以設(shè)置多大。
八普办、緩存的使用模式
使用緩存時(shí)有幾種常見的訪問模式:
1工扎、預(yù)留緩存(Cache-Aside)
應(yīng)用程序在訪問數(shù)據(jù)庫(kù)之前必須要先訪問緩存,如果緩存中包含了該數(shù)據(jù)則直接返回衔蹲,不用再經(jīng)過數(shù)據(jù)庫(kù)肢娘,否則應(yīng)用程序必須要從先數(shù)據(jù)庫(kù)中取回?cái)?shù)據(jù),存儲(chǔ)在緩存中并且將數(shù)據(jù)返回舆驶,當(dāng)有數(shù)據(jù)要寫入的時(shí)候橱健,緩存內(nèi)容必須要和數(shù)據(jù)庫(kù)內(nèi)容保持一致。
示例如下代碼分別對(duì)應(yīng)讀和寫:
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)
這種方式是將數(shù)據(jù)庫(kù)與緩存通過客戶端應(yīng)用程序主動(dòng)管理來(lái)進(jìn)行同步沙廉,這不是很好的使用方式畴博。
2、Read-Through模式
相比上面的由客戶端應(yīng)用程序來(lái)管理數(shù)據(jù)庫(kù)和緩存同步的方式蓝仲,這種模式緩存會(huì)配有一個(gè)緩存中間件俱病,該中間件來(lái)負(fù)責(zé)數(shù)據(jù)庫(kù)數(shù)據(jù)和緩存之間的同步問題官疲。當(dāng)我們應(yīng)用要獲取緩存數(shù)據(jù)時(shí),這個(gè)緩存中間件要確認(rèn)緩存中是否有該數(shù)據(jù)亮隙,如果沒有途凫,從數(shù)據(jù)庫(kù)加載,然后放入緩存溢吻,下次以后再訪問就可以直接從緩存中獲得维费。
3、Write-Through模式
這種模式就是緩存能夠感知數(shù)據(jù)的變化促王。
也就是說(shuō)在更新數(shù)據(jù)庫(kù)的同時(shí)也要更新緩存犀盟,該模式其實(shí)也是弱一致性,當(dāng)數(shù)據(jù)庫(kù)更新數(shù)據(jù)失敗的時(shí)候蝇狼,緩存不能繼續(xù)更新數(shù)據(jù)阅畴,要保持?jǐn)?shù)據(jù)庫(kù)和緩存的最終一致性。
4迅耘、Write-behind模式
該模式是以緩存為優(yōu)先贱枣,將緩存更新的數(shù)據(jù)存放隊(duì)列中,然后數(shù)據(jù)庫(kù)定時(shí)批量從隊(duì)列中取出數(shù)據(jù)更新數(shù)據(jù)庫(kù)颤专。
九纽哥、Spring3.2+Ehcache2.10.2的使用
為了使例子更加簡(jiǎn)單易懂,我沒有直接去連接數(shù)據(jù)庫(kù)而模擬了一些操作目的主要是演示Spring結(jié)合Ehcache的使用栖秕。
JavaBean代碼如下:
public class User {
public Integer id;
public String name;
public String password;
// 這個(gè)需要春塌,不然在實(shí)體綁定的時(shí)候出錯(cuò)
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;
// 查詢所有數(shù)據(jù),不需要key
@Cacheable(value = "serviceCache")
public List<User> getAll(){
printInfo("getAll");
return userDao.users;
}
//根據(jù)ID來(lái)獲取數(shù)據(jù)簇捍,ID可能是主鍵也可能是唯一鍵
@Cacheable(value = "serviceCache", key="#id")
public User findById(Integer id){
printInfo(id);
return userDao.findById(id);
}
//通過ID進(jìn)行刪除
@CacheEvict(value = "serviceCache", key="#id")
public void removeById(Integer id){
userDao.removeById(id);
}
//添加數(shù)據(jù)
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 表示調(diào)用之后垦写,清空緩存吕世,默認(rèn)false,
// 還有個(gè)beforeInvocation 屬性,表示先清空緩存梯投,再進(jìn)行查詢
@CacheEvict(value="serviceCache",allEntries=true)
public void removeAll(){
System.out.println("清除所有緩存");
}
private void printInfo(Object str){
System.out.println("非緩存查詢----------findById"+str);
}
}
ehcache配置文件內(nèi)容如下
<cache name="serviceCache"
eternal="false"
maxElementsInMemory="100"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="300"
memoryStoreEvictionPolicy="LRU" />
</ehcache>
Spring配置文件內(nèi)容如下:
<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" />
<!-- 默認(rèn)是cacheManager -->
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="cacheManagerFactory"/>
</bean>
十命辖、Spring3.2+Ehcache2.10.2分布式緩存的使用
10.1 Ehcache集群簡(jiǎn)介
從Ehcache1.2版本開始,Ehcache就可以使用分布式的緩存了分蓖,從 1.7版本開始尔艇,開始支持共五種集群方案,分別是:
- Terracotta
- RMI
- JMS
- JGroups
- EhCache Server
其中有三種上最為常用集群方式么鹤,分別是 RMI终娃、JGroups 以及 EhCache Server 。
其實(shí)我們?cè)谑褂肊hcache分布式緩存的過程中蒸甜,主要是以緩存插件的方式使用棠耕,如果我們想根據(jù)自己的需要使用分布式緩存那就需要自己開發(fā)來(lái)定制化余佛,在后面我們會(huì)發(fā)現(xiàn)其實(shí)Ehcache提供的分布式緩存并不是非常好用,有不少問題存在窍荧,所以對(duì)緩存數(shù)據(jù)一致性比較高的情況下辉巡,使用集中式緩存更合適,比如Redis蕊退、Memcached等郊楣。
10.2 Ehcache集群的基本概念
1、成員發(fā)現(xiàn)(Peer Discovery)
Ehcache集群概念中有一個(gè)cache組瓤荔,每個(gè)cache都是另一個(gè)cache的peer净蚤,并不像Redis或者其他分布式組件一樣有一個(gè)主的存在,Ehcache并沒有主Cache输硝,可是那如何知道集群中的其他緩存都有誰(shuí)呢今瀑?這個(gè)就是成員發(fā)現(xiàn)。
Ehcache提供了二種機(jī)制來(lái)實(shí)現(xiàn)成員發(fā)現(xiàn)功能腔丧,分別是手動(dòng)發(fā)現(xiàn)和自動(dòng)發(fā)現(xiàn)放椰。
- 手動(dòng)發(fā)現(xiàn)
在Ehcache的配置文件中指定cacheManagerPeerProviderFactory元素的class屬性為
net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory作烟。這就需要自己去配置IP地址和端口號(hào)愉粤。
- 自動(dòng)發(fā)現(xiàn)
自動(dòng)的發(fā)現(xiàn)方式用TCP廣播機(jī)制來(lái)確定和維持一個(gè)廣播組。它只需要一個(gè)簡(jiǎn)單的配置可以自動(dòng)的在組中添加和移除成員拿撩。在集群中也不需要什么優(yōu)化服務(wù)器的知識(shí)衣厘,這是默認(rèn)推薦的。
成員每秒向群組發(fā)送一個(gè)“心跳”压恒。如果一個(gè)成員 5秒種都沒有發(fā)出信號(hào)它將被群組移除影暴。如果一個(gè)新的成員發(fā)送了一個(gè)“心跳”它將被添加進(jìn)群組。
任何一個(gè)用這個(gè)配置安裝了復(fù)制功能的cache都將被其他的成員發(fā)現(xiàn)并標(biāo)識(shí)為可用狀態(tài)探赫。
要設(shè)置自動(dòng)的成員發(fā)現(xiàn)型宙,需要指定ehcache配置文件中
cacheManagerPeerProviderFactory元素的properties屬性,就像下面這樣:
peerDiscovery=automatic
multicastGroupAddress=multicast address | multicast host name
multicastGroupPort=port
timeToLive=0-255 (timeToLive屬性詳見常見問題部分的描述)
10.3 結(jié)合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配置文件內(nèi)容如下:
<?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分布式緩存集群環(huán)境配置-->
<!--rmi手動(dòng)配置-->
<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>
以上配置其實(shí)就是使用RMI方式在集群的環(huán)境進(jìn)行緩存數(shù)據(jù)的復(fù)制妆兑。
十一、Ehcache的使用場(chǎng)景
11.1毛仪、Ehcache使用的注意點(diǎn)
1搁嗓、比較少的更新數(shù)據(jù)表的情況
2、對(duì)并發(fā)要求不是很嚴(yán)格的情況
多臺(tái)應(yīng)用服務(wù)器中的緩存是不能進(jìn)行實(shí)時(shí)同步的箱靴。
3腺逛、對(duì)一致性要求不高的情況下
因?yàn)镋hcache本地緩存的特性,目前無(wú)法很好的解決不同服務(wù)器間緩存同步的問題衡怀,所以我們?cè)谝恢滦砸蠓浅8叩膱?chǎng)合下棍矛,盡量使用Redis安疗、Memcached等集中式緩存。
11.2够委、Ehcache在集群茂契、分布式的情況下表現(xiàn)如何
在分布式情況下有二種同步方式:
1、RMI組播方式
示例:
<cacheManagerPeerProviderFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
properties="peerDiscovery=automatic, multicastGroupAddress=localhost,
multicastGroupPort=4446,timeToLive=255"/>
原理:當(dāng)緩存改變時(shí),ehcache會(huì)向組播IP地址和端口號(hào)發(fā)送RMI UDP組播包慨绳。
缺陷:Ehcache的組播做得比較初級(jí),功能只是基本實(shí)現(xiàn)(比如簡(jiǎn)單的一個(gè)HUB,接兩臺(tái)單網(wǎng)卡的服務(wù)器,互相之間組播同步就沒問題)掉冶,對(duì)一些復(fù)雜的環(huán)境(比如多臺(tái)服務(wù)器,每臺(tái)服務(wù)器上多地址,尤其是集群脐雪,存在一個(gè)集群地址帶多個(gè)物理機(jī)厌小,每臺(tái)物理機(jī)又帶多個(gè)虛擬站的子地址),就容易出現(xiàn)問題战秋。
2璧亚、P2P方式
原理:P2P要求每個(gè)節(jié)點(diǎn)的Ehcache都要指向其他的N-1個(gè)節(jié)點(diǎn)。
3脂信、JMS消息模式
原理:這種模式的核心就是一個(gè)消息隊(duì)列癣蟋,每個(gè)應(yīng)用節(jié)點(diǎn)都訂閱預(yù)先定義好的主題,同時(shí)狰闪,節(jié)點(diǎn)有元素更新時(shí)疯搅,也會(huì)發(fā)布更新元素到主題中去。各個(gè)應(yīng)用服務(wù)器節(jié)點(diǎn)通過偵聽MQ獲取到最新的數(shù)據(jù)埋泵,然后分別更新自己的Ehcache緩存幔欧,Ehcache默認(rèn)支持ActiveMQ,我們也可以通過自定義組件的方式實(shí)現(xiàn)類似Kafka丽声,RabbitMQ礁蔗。
4、Cache Server模式
原理:這種模式會(huì)存在主從節(jié)點(diǎn)雁社。
缺陷:緩存容易出現(xiàn)數(shù)據(jù)不一致的問題浴井,
11.3、使用Ehcache的瓶頸是什么
1霉撵、緩存漂移(Cache Drift):每個(gè)應(yīng)用節(jié)點(diǎn)只管理自己的緩存磺浙,在更新某個(gè)節(jié)點(diǎn)的時(shí)候,不會(huì)影響到其他的節(jié)點(diǎn)喊巍,這樣數(shù)據(jù)之間可能就不同步了屠缭。
2、數(shù)據(jù)庫(kù)瓶頸(Database Bottlenecks ):對(duì)于單實(shí)例的應(yīng)用來(lái)說(shuō)崭参,緩存可以保護(hù)數(shù)據(jù)庫(kù)的讀風(fēng)暴呵曹;但是,在集群的環(huán)境下,每一個(gè)應(yīng)用節(jié)點(diǎn)都要定期保持?jǐn)?shù)據(jù)最新奄喂,節(jié)點(diǎn)越多铐殃,要維持這樣的情況對(duì)數(shù)據(jù)庫(kù)的開銷也越大。
11.4跨新、實(shí)際工作中如何使用Ehcache
在實(shí)際工作中富腊,我更多是將Ehcache作為與Redis配合的二級(jí)緩存。
第一種方式:
注:
這種方式通過應(yīng)用服務(wù)器的Ehcache定時(shí)輪詢Redis緩存服務(wù)器更同步更新本地緩存域帐,缺點(diǎn)是因?yàn)槊颗_(tái)服務(wù)器定時(shí)Ehcache的時(shí)間不一樣赘被,那么不同服務(wù)器刷新最新緩存的時(shí)間也不一樣,會(huì)產(chǎn)生數(shù)據(jù)不一致問題肖揣,對(duì)一致性要求不高可以使用民假。
第二種方式:
注:
通過引入了MQ隊(duì)列,使每臺(tái)應(yīng)用服務(wù)器的Ehcache同步偵聽MQ消息龙优,這樣在一定程度上可以達(dá)到準(zhǔn)同步更新數(shù)據(jù)羊异,通過MQ推送或者拉取的方式,但是因?yàn)椴煌?wù)器之間的網(wǎng)絡(luò)速度的原因彤断,所以也不能完全達(dá)到強(qiáng)一致性野舶。基于此原理使用Zookeeper等分布式協(xié)調(diào)通知組件也是如此宰衙。
總結(jié):
1平道、使用二級(jí)緩存的好處是減少緩存數(shù)據(jù)的網(wǎng)絡(luò)傳輸開銷,當(dāng)集中式緩存出現(xiàn)故障的時(shí)候菩浙,Ehcache等本地緩存依然能夠支撐應(yīng)用程序正常使用巢掺,增加了程序的健壯性句伶。另外使用二級(jí)緩存策略可以在一定程度上阻止緩存穿透問題劲蜻。
2、根據(jù)CAP原理我們可以知道考余,如果要使用強(qiáng)一致性緩存(根據(jù)自身業(yè)務(wù)決定)先嬉,集中式緩存是最佳選擇,如(Redis楚堤,Memcached等)疫蔓。
**十二、Ehcache2.10.2源碼分析 **
12.1 源碼淘汰策略解析
首先看一下類結(jié)構(gòu)圖:
從類結(jié)構(gòu)圖上看一共有三種緩存淘汰策略分別是:LFU身冬,LRU衅胀,F(xiàn)IFO。關(guān)于這三個(gè)概念在前面都已經(jīng)有過解釋酥筝,我們直接這三個(gè)的源碼:
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丧凤、這三個(gè)策略類統(tǒng)一繼承AbstractPolicy抽類
最關(guān)鍵的就是下面這個(gè)方法:
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募闲、這個(gè)方法主要是從取樣節(jié)點(diǎn)中查找需要淘汰的緩存。
2愿待、最關(guān)鍵的就是調(diào)用compare這個(gè)方法其實(shí)就是調(diào)用的上面那三個(gè)策略實(shí)現(xiàn)的方法來(lái)找個(gè)可以淘汰的緩存節(jié)點(diǎn)浩螺。
那么接下來(lái)我們看一下淘汰緩存的生命周期流程是怎么樣的。
12.2 EhcacheManager類解析
這個(gè)類是2.10.2版本的最核心類仍侥,初始化年扩、創(chuàng)建緩存、獲取緩存都會(huì)用到這個(gè)類访圃,這個(gè)類里面有幾十個(gè)方法非常多厨幻,我們會(huì)按照類別分別進(jìn)行介紹,先看其構(gòu)造方法吧腿时。
先看方法CacheManager()默認(rèn)的情況代碼如下:
public CacheManager() throws CacheException {
// default config will be done
status = Status.STATUS_UNINITIALISED;
init(null, null, null, null);
}
Status.STATUS_UNINITIALISED這句的意思是緩存未被初始化况脆,在構(gòu)造方法里面要設(shè)定一個(gè)初始狀態(tài)。
我們接著看init方法批糟,這個(gè)方法是有別于其他構(gòu)造方法的格了,因?yàn)槟J(rèn)的情況下這個(gè)方法的參數(shù)全傳null值,這就意味著使用ehcache自己默認(rèn)的配置了徽鼎。
代碼如下:
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);
}
}
}
說(shuō)明
1盛末、首先要判斷initialConfiguration這個(gè)參數(shù)是不是為空,判斷的情況下肯定是為就直接調(diào)用了parseConfiguration這個(gè)方法否淤,這個(gè)方法調(diào)用classpath默認(rèn)路徑來(lái)查找配置文件內(nèi)容悄但,初始化完configuration以后調(diào)用doInit方法。
2石抡、doInit方法主要用來(lái)初始化最大的本地堆大小檐嚣,初始化最大的本地持久化磁盤設(shè)置大小,集群模式啰扛,事務(wù)設(shè)置等等嚎京。
12.3 Cache類解析
cache的類繼承結(jié)構(gòu)如下所示:
說(shuō)明:
Ehcache接口是整個(gè)緩存的核心接口,該接口提供的方法可以直接操作緩存隐解,比如put,get等方法鞍帝。由于方法太多我們只單拿出來(lái)put和get方法做一個(gè)深入分析。
先看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);
}
說(shuō)明:
1煞茫、代碼的邏輯其實(shí)很簡(jiǎn)單帕涌,我們看一下compoundStore這個(gè)類岩臣,實(shí)際上我們緩存的數(shù)據(jù)最終都是要到這個(gè)類里面進(jìn)行存儲(chǔ)的。
2宵膨、代碼里面使用了putObserver觀察者對(duì)象主要是用來(lái)做計(jì)數(shù)統(tǒng)計(jì)任務(wù)用的架谎。
看一下compoundStore類的繼承結(jié)構(gòu)圖如下:
通過圖中可以看到所有的存儲(chǔ)類都實(shí)現(xiàn)Store接口類,大概有以下幾種存儲(chǔ)方式:
1辟躏、集群方式:ClusteredStore
2谷扣、緩存方式:CacheStore
3、內(nèi)存方式: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();
}
}
說(shuō)明:
1、流程采用寫鎖瑞凑,先將這段代碼鎖定末秃。
2、程序中有HashEntry[] tab這樣一個(gè)桶籽御,每個(gè)桶中存儲(chǔ)一個(gè)鏈表练慕,首先通過hash & (tab -1) 也就是key的hash值與桶的長(zhǎng)度減1取余得出一個(gè)桶的index。然后取出鏈表實(shí)體技掏,得到當(dāng)前鏈表實(shí)體的下一個(gè)元素铃将,如果元素為null則直接將元素賦值,否則取出舊的元素用新元素替換哑梳,釋放舊元素空間劲阎,返回舊元素。
十三鸠真、Guava Cache的使用與實(shí)現(xiàn)
Guava Cache與ConcurrentMap很相似悯仙,但也不完全一樣。最基本的區(qū)別是ConcurrentMap會(huì)一直保存所有添加的元素吠卷,直到顯式地移除锡垄。相對(duì)地,Guava Cache為了限制內(nèi)存占用撤嫩,通常都設(shè)定為自動(dòng)回收元素偎捎。在某些場(chǎng)景下,盡管LoadingCache 不回收元素序攘,它也是很有用的,因?yàn)樗鼤?huì)自動(dòng)加載緩存寻拂。
通常來(lái)說(shuō)程奠,Guava Cache
適用于:
你愿意消耗一些內(nèi)存空間來(lái)提升速度。
你預(yù)料到某些鍵會(huì)被查詢一次以上祭钉。
緩存中存放的數(shù)據(jù)總量不會(huì)超出內(nèi)存容量瞄沙。(Guava Cache是單個(gè)應(yīng)用運(yùn)行時(shí)的本地緩存。它不把數(shù)據(jù)存放到文件或外部服務(wù)器。如果這不符合你的需求距境,請(qǐng)嘗試Memcached或者Redis等集中式緩存申尼。
Guava Cache是一個(gè)全內(nèi)存的本地緩存實(shí)現(xiàn),它提供了線程安全的實(shí)現(xiàn)機(jī)制垫桂。
Guava Cache有兩種創(chuàng)建方式:
- CacheLoader
- Callable callback
13.1 CacheLoader方式
先看一段示例代碼如下:
public static void main(String[] args) throws ExecutionException, InterruptedException {
//緩存接口這里是LoadingCache师幕,LoadingCache在緩存項(xiàng)不存在時(shí)可以自動(dòng)加載緩存
LoadingCache<Integer, String> strCache
//CacheBuilder的構(gòu)造函數(shù)是私有的,只能通過其靜態(tài)方法newBuilder()來(lái)獲得CacheBuilder的實(shí)例
= CacheBuilder.newBuilder()
//設(shè)置并發(fā)級(jí)別為8诬滩,并發(fā)級(jí)別是指可以同時(shí)寫緩存的線程數(shù)
.concurrencyLevel(8)
//設(shè)置寫緩存后8秒鐘過期
.expireAfterWrite(8, TimeUnit.SECONDS)
//設(shè)置緩存容器的初始容量為10
.initialCapacity(10)
//設(shè)置緩存最大容量為100霹粥,超過100之后就會(huì)按照LRU最近雖少使用算法來(lái)移除緩存項(xiàng)
.maximumSize(100)
//設(shè)置要統(tǒng)計(jì)緩存的命中率
.recordStats()
//設(shè)置緩存的移除通知
.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,在緩存不存在時(shí)通過CacheLoader的實(shí)現(xiàn)自動(dòng)加載緩存
.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++) {
//從緩存中得到數(shù)據(jù)疼鸟,由于我們沒有設(shè)置過緩存后控,所以需要通過CacheLoader加載緩存數(shù)據(jù)
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());
}
運(yùn)行結(jié)果如下:
說(shuō)明:
guava中使用緩存需要先聲明一個(gè)CacheBuilder對(duì)象,并設(shè)置緩存的相關(guān)參數(shù)空镜,然后調(diào)用其build方法獲得一個(gè)Cache接口的實(shí)例浩淘。
13.2 Callable方式
方法原型如下:get(K, Callable<V>)
這個(gè)方法返回緩存中相應(yīng)的值,如果未獲取到緩存值則調(diào)用Callable方法吴攒。這個(gè)方法簡(jiǎn)便地實(shí)現(xiàn)了模式"如果有緩存則返回馋袜;否則運(yùn)算、緩存舶斧、然后返回"欣鳖。
看示例代碼如下:
Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(1000).build();
String resultVal = cache.get("test", new Callable<String>() {
public String call() {
//未根據(jù)key查到對(duì)應(yīng)緩存,設(shè)置緩存
String strProValue="test-value"
return strProValue;
}
});
System.out.println("return value : " + resultVal);
}
13.3 緩存過期刪除
guava的cache數(shù)據(jù)過期刪除的方式有二種茴厉,分別是主動(dòng)刪除和被動(dòng)刪除二種泽台。
被動(dòng)刪除三種方式
基于條數(shù)限制的刪除
使用CacheBuilder.maximumSize(long)方法進(jìn)行設(shè)置。
注意點(diǎn):
1矾缓、這個(gè)size不是容量大小怀酷,而是記錄條數(shù)。
2嗜闻、使用CacheLoader方式加載緩存的時(shí)候蜕依,在并發(fā)情況下如果一個(gè)key過期刪除,正好同時(shí)有一個(gè)請(qǐng)求獲取緩存琉雳,有可能會(huì)報(bào)錯(cuò)样眠。基于過期時(shí)間刪除
在Guava Cache中提供了二個(gè)方法可以基于過期時(shí)間刪除
1、expireAfterAccess(long, TimeUnit):某個(gè)key最后一次訪問后翠肘,再隔多長(zhǎng)時(shí)間后刪除檐束。
2、expireAfterWrite(long, TimeUnit):某個(gè)key被創(chuàng)建后束倍,再隔多長(zhǎng)時(shí)間后刪除被丧。基于引用的刪除
通過使用弱引用的鍵盟戏、或弱引用的值、或軟引用的值甥桂,Guava Cache可以把緩存設(shè)置為允許垃圾回收柿究。
主動(dòng)刪除三種方式
- 個(gè)別清除:Cache.invalidate(key)
- 批量清除:Cache.invalidateAll(keys)
- 清除所有緩存項(xiàng):Cache.invalidateAll()