目錄
簡介
這是本人因爲工作需要研究的關於ehcache的分佈式RMI模式的使用心得已經自己的一些心得。
git 源碼
詳細介紹
話不多說,下面結合demo分步做詳細的介紹
jar依賴
- ehcache所需jar:ehchache-core
- spring註解所需 spring-context
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.2.7.RELEASE</version>
</dependency>
spring.xml文件中引用spring-ehcache.xml
註解使用cacheManager
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache.xsd">
<!-- ehcache config -->
<cache:annotation-driven cache-manager="ehCacheCacheManager"/>
<bean id="ehCacheCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="cacheManagerFactoryBean"/>
<!-- EhCache library setup -->
<bean id="cacheManagerFactoryBean" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="classpath:spring-ehcache.xml" p:shared="true"/>
</beans>
spring-ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd" updateCheck="false"
monitoring="autodetect" dynamicConfig="true">
ehcache標籤的屬性
- name 名稱-可選,唯一。名字用於註解或區別Terracotta集羣緩存。對於
Terracotta集羣緩存,緩存管理器的名字和緩存的名字的組合能夠在Terracotta集羣緩存中標誌一個特定的緩存存儲。 - updateCheck 更新檢查-一個可選的布爾值標誌符,用於標誌緩存管理器是否應該通過網絡檢查Ehcache的新版本。默認爲true。
- dynamicConfig 動態配置 - 可選。用於關閉與緩存管理器相關的緩存的動態配置。默認爲true,即動態配置爲開啓狀態。動態配置的緩存可以根據緩存對象在運行狀態改變自己的TTI,TTL和最大磁盤空間和內在容量
- monitoring 監控 - 可選。決定緩存管理器是否應該自動註冊SampledCacheMBean到系統MBean服務器上。
cacheManagerPeerProviderFactory
它分佈式緩存管理器提供者,指定一個CacheManagerPeerProviderFactory,它將用於創建一個CacheManagerPeerProvider, CacheManagerPeerProvider偵測集羣中的其它事務管理器,實現和分佈式環境下的緩存同步。
相關屬性介紹:
- propertySeparator 拆分上面properties屬性的分隔符
- peerDiscovery=manual 手動的緩存同步
- peerDiscovery=automatic 廣播式的緩存同步
- rmiUrls 指的是手動偵測緩存地址,每一次當請求相應的緩存信息時,程序會先從配置的rmiUrls裏去分別讀取緩存,如果無該緩存信息,則生成緩存,存儲在cacheManagerPeerListenerFactory所配置的地址和端口的對應的緩存裏
地址與地址之間用|(豎線)來分割。 -
- url填寫規則:
//(雙斜槓)+cacheManagerPeerListenerFactory屬性中配置的hostName+:(冒號)+端口+/(斜槓)+緩存屬性名稱
- url填寫規則:
RMI 手動配置
<!-- 手動配置rmi同步的地址信息(這種模式本人親測多服務器試驗還有問題)-->
<cacheManagerPeerProviderFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
properties="peerDiscovery=manual,
rmiUrls=//127.0.0.1:40002/testCache|//127.0.0.1:40002/testCache2 "
propertySeparator="," />
RMI 自動組播
這樣當緩存改變時,ehcache會向230.0.0.1端口4446發RMI UDP組播包
** 坑巨**,組播模式下請勿自己設置 hostName=localhost 因爲這樣解析出來的地址127.0.0.1 當在不同設備上部署的時候根本識別不出來
如果需要區分環境,開發和準生產,可以設置不同的組播地址,避免不同環境相互干擾
- mulicastGroupAddress 組播組地址
-
- 組播地址:稱爲組播組的一組主機所共享的地址。組播地址的範圍在224.0.0.0 —— 239.255.255.255之間(都爲D類地址 1110開頭)
- mulicastGroupPort 廣播組端口
- timeToLive
0是限制在同一個服務器
1是限制在同一個子網 (ip 和 子網掩碼轉換成2進制數進行與操作,得到值想用即爲統一子網)
32是限制在同一個網站
64是限制在同一個region
128是限制在同一個大洲
255是不限制
<!-- 自動廣播式rmi形式(親測可用) -->
<cacheManagerPeerProviderFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
properties="peerDiscovery=automatic,
multicastGroupAddress=230.0.0.1,
multicastGroupPort=4446,
timeToLive=32"/>
<cacheManagerPeerListenerFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
properties="port=40002,socketTimeoutMillis=2000"/>
cacheManagerPeerListenerFactory
每個CacheManagerPeerListener監聽從成員們發向當前CacheManager的消息。配置 CacheManagerPeerListener需要指定一個CacheManagerPeerListenerFactory,它以插件的機制實現, 用來創建CacheManagerPeerListener。
Ehcache有一個內置的基於RMI的分佈系統。它的監聽器是RMICacheManagerPeerListener,這個監聽器可以用RMICacheManagerPeerListenerFactory來配置
<!-- 本機緩存的信息對應的地址和端口配置監聽器的工廠類 -->
<cacheManagerPeerListenerFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
properties="hostName=127.0.0.1, port=40002,socketTimeoutMillis=2000" />
- hostname (可選) – 運行監聽器的服務器名稱。標明瞭做爲集羣羣組的成員的地址,同時也是你想要控制的從集羣中接收消息的接口。
在CacheManager初始化的時候會檢查hostname是否可用。
如果hostName不可用,CacheManager將拒絕啓動並拋出一個連接被拒絕的異常。
如果指定,hostname將用InetAddress.getLocalHost().getHostAddress()來得到。 - port – 監聽器監聽的端口。
- socketTimeoutMillis (可選) – Socket超時的時間。默認是2000ms。當你socket同步緩存請求地址比較遠,不是本地局域網。你可能需要把這個時間配置大些,不然很可能延時導致同步緩存失敗。
具體cache 對象配置
<!-- 緩存最長存在10分鐘後失效,如果5分鐘未訪問,緩存也會失效 -->
<cache name="testCache"
maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600">
<cacheEventListenerFactory
class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
properties="replicateAsynchronously=false, replicatePuts=true,
replicatePutsViaCopy=true, replicateUpdates=true,
replicateUpdatesViaCopy=true, replicateRemovals=true" />
</cache>
<!-- 緩存最長存在99天后失效,如果99天未訪問,緩存也會失效 -->
<cache name="testCache2" maxEntriesLocalHeap="10000" eternal="false"
timeToIdleSeconds="8640000" timeToLiveSeconds="8640000">
<cacheEventListenerFactory
class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
properties="replicateAsynchronously=false, replicatePuts=true,
replicatePutsViaCopy=true, replicateUpdates=true,
replicateUpdatesViaCopy=true, replicateRemovals=true" />
</cache>
</ehcache>
cache屬性介紹
- 必須屬性:
name:設置緩存的名稱,用於標誌緩存,惟一
maxElementsInMemory:在內存中最大的對象數量
maxElementsOnDisk:在DiskStore中的最大對象數量,如爲0,則沒有限制
eternal:設置元素是否永久的,如果爲永久,則timeout忽略
overflowToOffHeap:
overflowToDisk:是否當memory中的數量達到限制後,保存到Disk - 可選的屬性:
timeToIdleSeconds:設置元素過期前的空閒時間,緩存自創建日期起至失效時的間隔時間。值爲零,意味空閒時間爲無窮,默認爲零。
timeToLiveSeconds:設置元素過期前的活動時間,緩存創建以後,最後一次訪問緩存的日期至失效之時的時間間隔。值爲零,意味存活時間爲無窮,默認爲零。
diskPersistent:是否disk store在虛擬機啓動時持久化。默認爲false
diskExpiryThreadIntervalSeconds:運行disk終結線程的時間,默認爲120秒
clearOnFlush:內存數量最大時是否清除。
memoryStoreEvictionPolicy:策略關於Eviction
cacheEventListenerFactory
註冊相應的的緩存監聽類,用於處理緩存事件,如put,remove,update,和expire bootstrap CacheLoaderFactory:指定相應的BootstrapCacheLoader,用於在初始化緩存,以及自動設置。
- replicatePuts=true|false - 默認爲true。新加入的緩存中的元素是否要複製到其它節點中去。
- replicatePutsViaCopy=true|false - 默認爲true。新加入的緩存中的元素是否要複製到其它 緩存中,或者一條刪除消息是否發送。
- replicateUpdates=true|false - 默認爲true。當新加入的元素與已存在的元素鍵值出現衝突時,是否要覆蓋已存在元素。
- replicateRemovals=true - 默認爲true。被移去的元素是否要複製。
- replicateAsynchronously=true | false - 默認爲true。true表示複製是異步的,false表示複製是同步的。
- replicateUpdatesViaCopy=true | false - 默認爲true。
- asynchronousReplicationIntervalMillis= - 默認值爲1000,最小值爲10。只有在replicateAsynchronously=true,該屬性才適用。
bootstrapCacheLoaderFactory
指定相應的BootstrapCacheLoader,用於在初始化緩存,以及自動設置
代碼中緩存標記的使用
注意:想針對統一資源緩存做緩存的增、改、刪,一定要注意,key 必須要設置成一樣的
讀取/生成緩存@Cacheable
能夠根據方法的請求參數對其結果進行緩存。即當重複使用相同參數調用方法的時候,方法本身不會被調用執行,即方法本身被略過了,取而代之的是方法的結果直接從緩存中找到並返回了。
- value:緩存位置名稱,不能爲空,如果使用EHCache,就是ehcache.xml中聲明的cache的name
- key:緩存的key(保證唯一的參數),默認爲空,既表示使用方法的參數類型及參數值作爲key,支持SpEL
- 緩存key還可以用如下規則組成,當我們要使用root作爲key時,可以不用寫root直接@Cache(key=“caches[1].name”)。因爲他默認是使用#root的
1.methodName 當前方法名 #root.methodName
2.method 當前方法 #root.method.name
3.target 當前被動用對象 #root.target
4.targetClass 當前被調用對象 Class#root.targetClass
5.args 當前方法參數組成的數組 #root.args[0]
6.caches 當前被調用方法所使用的Cache #root.caches[0],name
7.方法參數 假設包含String型參數str #str
#p0代表方法的第一個參數
假設包含HttpServletRequest型參數request #request.getAttribute(‘usId32’) 調用入參對象的相關包含參數的方法
假設包含User型參數user #user.usId 調用入參對象的無參方法可以直接用此形式
8.字符串 ‘字符串內容’
- condition:觸發條件,只有滿足條件的情況纔會加入緩存,默認爲空,既表示全部都加入緩存,支持SpEL
- unless: 觸發條件,只有不滿足條件的情況纔會加入緩存,默認爲空,既表示全部都加入緩存,支持SpEL
- #result 可以獲得返回結果對象
/**
* 生成緩存,同時下一次在調用此方法優先從緩存中獲取信息
* 讀取/生成緩存@Cacheable
* 能夠根據方法的請求參數對其結果進行緩存。即當重複使用相同參數調用方法的時候,方法本身不會被調用執行,即方法本身被略過了,取而代之的是方法的結果直接從緩存中找到並返回了。
* @param hosId
* @param request
* @return
*/
@Cacheable(value="testCache",key="#hosId+'_'+'createTestCacheSuccess'", condition="#hosId!=null",unless="#result.result!=true or #result.data==null")
@RequestMapping(value = "/{hosId}/createTestCacheSuccess", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
@ResponseBody
public ResultVo createTestCacheSuccess(@PathVariable Long hosId, HttpServletRequest request) {
ResultVo resultVo = new ResultVo();
resultVo.setKind(SUCCESS_CODE);
resultVo.setResult(true);
resultVo.setData((Object)("createTestCacheSuccess成功生成緩存"+System.currentTimeMillis()));
log.debug("進入實際生成緩存方法體,本次請求未使用緩存,本方法可以生成有效緩存,緩存未失效之前調用該方法將不會進入到方法體");
return resultVo;
}
刪除緩存@CacheEvict
根據value 和key值來唯一找到緩存記錄,並且清理緩存信息
- value:緩存的位置,不能爲空。
- key:緩存的key,默認爲空。
- condition:觸發的條件,只有滿足條件的情況纔會清楚緩存,默認爲空,支持SpEL。
- allEntries:true表示清除value中的全部緩存(可以理解爲清空表),默認爲false(刪除單條數據)。
- beforeInvocation:當我們設置爲true時,Spring會在調用該方法之前進行緩存的清除。清除操作默認是在方法成功執行之後觸發的。
/**
* 刪除緩存
* 刪除緩存@CacheEvict
* 根據value 和key值來唯一找到緩存記錄,並且清理緩存信息
* @param hosId
* @param request
* @return
*/
@CacheEvict(value="testCache",key="#hosId+'_'+'createTestCacheSuccess'", condition="#hosId!=null")
@RequestMapping(value = "/{hosId}/deleteCreateTestCacheSuccess", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
@ResponseBody
public ResultVo deleteCreateTestCacheSuccess(@PathVariable Long hosId, HttpServletRequest request) {
log.debug("刪除createTestCacheSuccess生成的緩存");
ResultVo resultVo = new ResultVo();
resultVo.setKind(SUCCESS_CODE);
resultVo.setResult(true);
resultVo.setData((Object)("刪除緩存成功"+System.currentTimeMillis()));
return resultVo;
}
更新緩存@CachePut
它雖然也可以聲明一個方法支持緩存,但它執行方法前是不會去檢查緩存中是否存在之前執行過的結果,而是每次都執行該方法,並將執行結果放入指定緩存中。
- value:緩存的位置,不能爲空。
- key:緩存的key,默認爲空。
- condition:觸發的條件,只有滿足條件的情況纔會清楚緩存,默認爲空,支持SpEL。
/**
* 生成緩存,同時下一次在調用此方法還是會執行該方法並且同時更新緩存內容
*
* 更新緩存@CachePut
* 它雖然也可以聲明一個方法支持緩存,但它執行方法前是不會去檢查緩存中是否存在之前執行過的結果,而是每次都執行該方法,並將執行結果放入指定緩存中。
* @param hosId
* @param request
* @return
*/
@CachePut(value="testCache",key="#hosId+'_'+'createTestCacheSuccess'", condition="#hosId!=null",unless="#result.result!=true or #result.data==null")
@RequestMapping(value = "/{hosId}/updateTestCacheSuccess", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
@ResponseBody
public ResultVo updateTestCacheSuccess(@PathVariable Long hosId, HttpServletRequest request) {
log.debug("進入實際生成緩存方法體,本方法可以生成有效緩存,下一次調用該方法依然會進入到方法體");
ResultVo resultVo = new ResultVo();
resultVo.setKind(SUCCESS_CODE);
resultVo.setResult(true);
resultVo.setData((Object)("updateTestCacheSuccess成功生成緩存"+System.currentTimeMillis()));
return resultVo;
}
通過EhCacheCacheManager獲取緩存詳情
EhCacheCacheManager (管理CacheManager的工具類)是在上面spring.xml 中配置的緩存管理對象
@Resource
EhCacheCacheManager ehCacheCacheManager;
/**
* 從cache 中獲取實際緩存信息
* @param cacheName
* @param cacheKey
* @return
*/
@RequestMapping(value = "getResult", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
@ResponseBody
public ResultVo getResult(String cacheName,String cacheKey){
ResultVo resultVo = null;
CacheManager cacheManager=ehCacheCacheManager.getCacheManager();
if (cacheManager!=null){
Ehcache ehcache = cacheManager.getEhcache(CACHE_NEMA_TESTCACHE);
if(ehcache!=null){
Element element = ehcache.get(cacheKey);
if(element!=null && element.getObjectValue()!=null
&& element.getObjectValue() instanceof ResultVo){
resultVo = (ResultVo)element.getObjectValue();
}
}
}
return resultVo;
}
簡單信息統計
/**
* 從cache 中獲取緩存簡單的監控信息(數據量少的時候適合這麼幹,數據量大的時候需要注意性能問題,一下子遍歷所有緩存元素這將是一個災難)
* @return
*/
@RequestMapping(value = "getCacheStatistic", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
@ResponseBody
public ResultVo getCacheStatistic(){
ResultVo resultVo = new ResultVo();
resultVo.setResult(false);
CacheManager cacheManager=ehCacheCacheManager.getCacheManager();
if (cacheManager!=null){
String []cacheNames=cacheManager.getCacheNames();
if( null != cacheNames && cacheNames.length>0 ){
StringBuffer ehcacheBuffer = new StringBuffer();
ehcacheBuffer.append(StringUtils.rightPad("CacheName", 15));
ehcacheBuffer.append(" | ");
ehcacheBuffer.append(StringUtils.rightPad("Key", 40));
ehcacheBuffer.append(" | ");
ehcacheBuffer.append(StringUtils.rightPad("HintCount", 10));
ehcacheBuffer.append(" | ");
ehcacheBuffer.append(StringUtils.rightPad("CreationTime", 25));
ehcacheBuffer.append(" | ");
ehcacheBuffer.append(StringUtils.rightPad("LastAccessTime", 25));
ehcacheBuffer.append(" | ");
ehcacheBuffer.append(StringUtils.rightPad("TimeToLive(ms)", 15));
ehcacheBuffer.append(" | ");
ehcacheBuffer.append(StringUtils.rightPad("TimeToIdle(ms)", 15));
//這裏不打印數據值,因爲打印值的話數據量比較大
ehcacheBuffer.append(" | ");
ehcacheBuffer.append("\n");
for (int i = 0; i < cacheNames.length; i++) {
Ehcache ehcache = cacheManager.getCache(cacheNames[i]);
if(ehcache!=null){
List<String> ehcacheKeys = ehcache.getKeys();
if( null!=ehcacheKeys && 0< ehcacheKeys.size() ){
for (String ehcacheKey:ehcacheKeys) {
Element element = ehcache.get(ehcacheKey);
if(element!=null ){
ehcacheBuffer.append(StringUtils.rightPad(ehcache.getName(), 15));//cachenName
ehcacheBuffer.append(" | ");
ehcacheBuffer.append(StringUtils.rightPad(ehcacheKey, 40));//key name
ehcacheBuffer.append(" | ");
ehcacheBuffer.append(StringUtils.rightPad(""+element.getHitCount(), 10));//命中次數
ehcacheBuffer.append(" | ");
ehcacheBuffer.append(StringUtils.rightPad(formatDate(element.getCreationTime()), 25));//創建時間
ehcacheBuffer.append(" | ");
ehcacheBuffer.append(StringUtils.rightPad(formatDate(element.getLastAccessTime()), 25));//最後訪問時間
ehcacheBuffer.append(" | ");
ehcacheBuffer.append(StringUtils.rightPad(""+element.getTimeToLive(), 15)); //存活時間
ehcacheBuffer.append(" | ");
ehcacheBuffer.append(StringUtils.rightPad(""+element.getTimeToIdle(), 15)); //空閒時間
ehcacheBuffer.append(" | ");
ehcacheBuffer.append("\n");
}
}
}
}
}
log.debug("\n"+ehcacheBuffer.toString());
resultVo.setData(ehcacheBuffer);
resultVo.setResult(true);
}
}
return resultVo;
}
日誌效果
手工RMI配置模式下實現rmiUrls更新功能的接口
查看了部分源碼,找到目前rmiUrls的賦值的相關內容,然後用了點反射的小手段來處理這個事。
public static final String URL_DELIMITER = "|";
/**
* 修改spring-ehcache.xml 中的相關屬性類(該文件不是spring常規的加載方式,在spring啓動時通過${xxx}獲取到值)
* @return
*/
@RequestMapping(value = "changeCacheManagerPeerProviderFactory", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
@ResponseBody
public ResultVo changeCacheManagerPeerProviderFactory(@RequestParam(value = "rmiUrls") String rmiUrls){
ResultVo resultVo = null;
//CacheManagerPeerProvider
CacheManager cacheManager=ehCacheCacheManager.getCacheManager();
if (cacheManager!=null){
//此處查看源碼可知返回的是一個ummoifyMap,字面意思不可更改的map(ConcureentHashMap)
Map<String, CacheManagerPeerProvider> map = cacheManager.getCacheManagerPeerProviders();
//默認生成的CacheManagerPeerProvider 對應的key是RMI
CacheManagerPeerProvider cacheManagerPeerProvider = map.get("RMI");
if( null != cacheManagerPeerProvider && cacheManagerPeerProvider instanceof ManualRMICacheManagerPeerProvider){
ManualRMICacheManagerPeerProvider manualRMICacheManagerPeerProvider=(ManualRMICacheManagerPeerProvider)cacheManagerPeerProvider;
StringTokenizer stringTokenizer = new StringTokenizer(rmiUrls, URL_DELIMITER);
while (stringTokenizer.hasMoreTokens()) {
String rmiUrl = stringTokenizer.nextToken();
rmiUrl = rmiUrl.trim();
manualRMICacheManagerPeerProvider.registerPeer(rmiUrls);
log.debug("Registering peer {}", rmiUrl);
}
Map<String, CacheManagerPeerProvider> modifiableMap=null;
Class clazz =null;
try {
clazz = cacheManager.getClass();
Field fields[] = clazz.getDeclaredFields();
for (Field field:fields) {
if( "cacheManagerPeerProviders".equals(field.getName())){
field.setAccessible(true);
//獲取屬性
String name = field.getName();
//獲取屬性值
Object value = field.get(cacheManager);
modifiableMap=(ConcurrentHashMap)value;
modifiableMap.put("RMI",manualRMICacheManagerPeerProvider);
log.debug("");
break;
}
}
log.debug("");
} catch (Exception e) {
log.error("",e);
}
ehCacheCacheManager.setCacheManager(cacheManager);
}
}
return resultVo;
}
參考資料
[1]: EhCache 緩存系統簡介 https://www.ibm.com/developerworks/cn/java/j-lo-ehcache/
[2]: Ehcache配置文件譯文 https://dreamzhong.iteye.com/blog/1161954
[3]: EhCache 系統簡介 https://www.cnblogs.com/duwanjiang/p/6230113.html
[3]: 本人其他平臺早期的文檔 https://blog.51cto.com/tianyang10552/1899550