分布式改造劇集三:Ehcache分布式改造

第三集:分布式Ehcache緩存改造

前言

? 好久沒有寫博客了,大有半途而廢的趨勢。忙不是借口,這個好習慣還是要繼續堅持。前面我承諾的第一期的DIY分布式,是時候上終篇了---DIY分布式緩存。


探索之路

? 在前面的文章中,我給大家大致說過項目背景:項目中的緩存使用的是Ehcache。因為前面使用Ehcache的應用就一臺,所以這種單機的Ehcache并不會有什么問題。現在分布式部署之后,如果各個應用之間的緩存不能共享,那么其實各自就是一個孤島。可能在一個業務跑下來,請求了不同的應用,結果在緩存中取出來的值不一樣,

造成數據不一致。所以需要重新設計緩存的實現。

? 因為盡量不要引入新的中間件,所以改造仍然是圍繞Ehcache來進行的。搜集了各種資料之后,發現Ehcache實現分布式緩存基本有以下兩種思路:

  • 客戶端實現分布式算法: 在使用Ehcache的客戶端自己實現分布式算法。

    算法的基本思路就是取模:即假設有三臺應用(編號假設分別為0,1,2),對于一個要緩存的對象,首先計算其key的hash值,然后將hash值模3,得到的余數是幾,就將數據緩存到哪臺機器。

  • 同步冗余數據: Ehcache是支持集群配置的,集群的各個節點之間支持按照一定的協議進行數據同步。這樣每臺應用其實緩存了一整份數據,不同節點之間的數據是一致的。

? 雖然冗余的辦法顯得有點浪費資源,但是我最終還是選擇了冗余。具體原因有以下幾點:

  • 分布式算法的復雜性: 前面所講的分布式算法只是最基本的實現。事實上實現要比這個復雜的多。需要考慮增加或者刪除節點的情況,需要使用更加復雜的一致性hash算法
  • 可能導致整個應用不可用: 當刪除節點之后,如果算法不能夠感知進行自動調整,仍然去請求那個已經被刪除的節點,可能導致整個系統不可用。

Demo

? 最終我的實現采用RMI的方式進行同步

配置ehcache

? spring-ehcache-cache.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" name="businessCaches"><diskStore path="java.io.tmpdir/ehcache"/><cache name="business1Cache"maxElementsInMemory="10000000"eternal="true"overflowToDisk="false"memoryStoreEvictionPolicy="LRU"><cacheEventListenerFactoryclass="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/></cache><cache name="business2Cache"maxElementsInMemory="100"eternal="true"overflowToDisk="false"memoryStoreEvictionPolicy="LRU"><cacheEventListenerFactoryclass="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/></cache><!-- cache發布信息配置,人工發現peerDiscovery=manual,cacheNames可配置多個緩存名稱,以|分割 ) --><cacheManagerPeerProviderFactoryclass="com.rampage.cache.distribute.factory.DisRMICacheManagerPeerProviderFactory"properties="peerDiscovery=manual, cacheNames=business1Cache|business2Cache" /><!-- 接收同步cache信息的地址 --><cacheManagerPeerListenerFactoryclass="com.rampage.cache.distribute.factory.DisRMICacheManagerPeerListenerFactory"properties="socketTimeoutMillis=2000" />     
</ehcache>

? spring-cache.xml

<?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:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsdhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd"default-autowire="byName"><!-- 包掃描 --><context:component-scan base-package="com.rampage.cache" /><!-- 啟用Cache注解 --><cache:annotation-driven cache-manager="cacheManager"key-generator="keyGenerator" proxy-target-class="true" /><!-- 自定義的緩存key生成類,需實現org.springframework.cache.interceptor.KeyGenerator接口 --><bean id="keyGenerator" class="com.rampage.cache.support.CustomKeyGenerator" /><!-- 替換slite的ehcache實現 --><bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"><property name="configLocation" value="classpath:spring/cache/sppay-ehcache-cache.xml"/><!-- value對應前面ehcache文件定義的manager名稱 --><property name="cacheManagerName" value="businessCaches" /></bean><bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"><property name="cacheManager" ref="ehCacheManagerFactory"/></bean><bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager"><property name="cacheManagers"><list><ref bean="ehCacheManager" /></list></property><property name="fallbackToNoOpCache" value="true" /></bean>    
</beans>

實現自定義轉發和監聽

? 細心的讀者應該不難發現,前面xml配置中cacheManagerPeerProviderFactorycacheManagerPeerListenerFactory我使用的都是自定義的類。之所以使用自定義的類,是為了在初始化的時候發布的地址和端口,監聽的地址端口可以在配置文件配置。具體類的實現如下:

/*** 分布式EhCache監聽工廠* @author secondWorld**/
public class DisRMICacheManagerPeerListenerFactory extends RMICacheManagerPeerListenerFactory {private static final Logger LOGGER = LoggerFactory.getLogger(DisRMICacheManagerPeerListenerFactory.class);/*** 配置文件中配置的監聽地址,可以不配置,默認為本機地址*/private static final String LISTEN_HOST = "distribute.ehcache.listenIP";/*** 配置文件中配置的監聽端口*/private static final String LISTEN_PORT = "distribute.ehache.listenPort";@Overrideprotected CacheManagerPeerListener doCreateCachePeerListener(String hostName, Integer port,Integer remoteObjectPort, CacheManager cacheManager, Integer socketTimeoutMillis) {// xml中hostName為空,則讀取配置文件(app-config.properties)中的值if (StringUtils.isEmpty(hostName)) {String propHost = AppConfigPropertyUtils.get(LISTEN_HOST);if (StringUtils.isNotEmpty(propHost)) {hostName = propHost;}}// 端口采用默認端口0,則去讀取配置文件(app-config.properties)中的值if (port != null && port == 0) {Integer propPort = null;try {propPort = Integer.parseInt(AppConfigPropertyUtils.get(LISTEN_PORT));} catch (NumberFormatException e) {}if (propPort != null) {port = propPort;}}LOGGER.info("Initiliazing DisRMICacheManagerPeerListenerFactory:cacheManager[{}], hostName[{}], port[{}], remoteObjectPort[{}], socketTimeoutMillis[{}]......",cacheManager, hostName, port, remoteObjectPort, socketTimeoutMillis);return super.doCreateCachePeerListener(hostName, port, remoteObjectPort, cacheManager, socketTimeoutMillis);}
}/*** 分布式EhCache發布工廠* * @author secondWorld**/
public class DisRMICacheManagerPeerProviderFactory extends RMICacheManagerPeerProviderFactory {private static final Logger LOGGER = LoggerFactory.getLogger(DisRMICacheManagerPeerProviderFactory.class);private static final String CACHENAME_DELIMITER = "|";private static final String PROVIDER_ADDRESSES = "distribute.ehcache.providerAddresses";private static final String CACHE_NAMES = "cacheNames";/*** rmi地址格式: //127.0.0.1:4447/Cache1|//127.0.0.1:4447/Cache2*/@Overrideprotected CacheManagerPeerProvider createManuallyConfiguredCachePeerProvider(Properties properties) {// 從app-config.properties中讀取發布地址列表String providerAddresses = AppConfigPropertyUtils.get(PROVIDER_ADDRESSES, StringUtils.EMPTY);// 從ehcache配置文件讀取緩存名稱String cacheNames = PropertyUtil.extractAndLogProperty(CACHE_NAMES, properties);// 參數校驗,這里發布地址和緩存名稱都不能為空if (StringUtils.isEmpty(providerAddresses) || StringUtils.isEmpty(cacheNames)) {throw new IllegalArgumentException("Elements \"providerAddresses\" and \"cacheNames\" are needed!");}// 解析地址列表List<String> cachesNameList = getCacheNameList(cacheNames);List<String> providerAddressList = getProviderAddressList(providerAddresses);// 注冊發布節點RMICacheManagerPeerProvider rmiPeerProvider = new ManualRMICacheManagerPeerProvider();StringBuilder sb = new StringBuilder();for (String cacheName : cachesNameList) {for (String providerAddress : providerAddressList) {sb.setLength(0);sb.append("//").append(providerAddress).append("/").append(cacheName);rmiPeerProvider.registerPeer(sb.toString());LOGGER.info("Registering peer provider [{}]", sb);}}return rmiPeerProvider;}/*** 得到發布地址列表* @param providerAddresses 發布地址字符串* @return 發布地址列表*/private List<String> getProviderAddressList(String providerAddresses) {StringTokenizer stringTokenizer = new StringTokenizer(providerAddresses,AppConfigPropertyUtils.APP_ITEM_DELIMITER);List<String> ProviderAddressList = new ArrayList<String>(stringTokenizer.countTokens());while (stringTokenizer.hasMoreTokens()) {String providerAddress = stringTokenizer.nextToken();providerAddress = providerAddress.trim();ProviderAddressList.add(providerAddress);}return ProviderAddressList;}/*** 得到緩存名稱列表* @param cacheNames 緩存名稱字符串* @return 緩存名稱列表*/private List<String> getCacheNameList(String cacheNames) {StringTokenizer stringTokenizer = new StringTokenizer(cacheNames, CACHENAME_DELIMITER);List<String> cacheNameList = new ArrayList<String>(stringTokenizer.countTokens());while (stringTokenizer.hasMoreTokens()) {String cacheName = stringTokenizer.nextToken();cacheName = cacheName.trim();cacheNameList.add(cacheName);}return cacheNameList;}@Overrideprotected CacheManagerPeerProvider createAutomaticallyConfiguredCachePeerProvider(CacheManager cacheManager,Properties properties) throws IOException {throw new UnsupportedOperationException("Not supported automatic distribute cache!");}
}

配置

? 假設有三臺機器,則他們分別得配置如下:

#應用1,在4447端口監聽
#緩存同步消息發送地址(如果同步到多臺需要配置多臺地址,多臺地址用英文逗號分隔)
distribute.ehcache.providerAddresses=127.0.0.1:4446,127.0.0.1:4448
#緩存同步監聽端口和IP
distribute.ehache.listenPort=4447
distribute.ehcache.listenIP=localhost#應用2,在4448端口監聽
#緩存同步消息發送地址(如果同步到多臺需要配置多臺地址,多臺地址用英文逗號分隔)
distribute.ehcache.providerAddresses=127.0.0.1:4446,127.0.0.1:4447
#緩存同步監聽端口和IP
distribute.ehache.listenPort=4448
distribute.ehcache.listenIP=localhost#應用3,在4446端口監聽
#緩存同步消息發送地址(如果同步到多臺需要配置多臺地址,多臺地址用英文逗號分隔)
distribute.ehcache.providerAddresses=127.0.0.1:4447,127.0.0.1:4448
#緩存同步監聽端口和IP
distribute.ehache.listenPort=4446
distribute.ehcache.listenIP=localhost

使用

? 使用的時候直接通過Spring的緩存注解即可。簡單的示例如下:

@CacheConfig("business1Cache")
@Component
public class Business1 {@Cacheablepublic String getData(String key) {// TODO:...}
}

說明

? 前面的實現是通過RMI的方式來實現緩存同步的,相對來說RMI的效率還是很快的。所以如果不需要實時的緩存一致性,允許少許延遲,那么這種方式的實現足夠。


總結

? 到這篇完成,分布式改造的第一章算是告一段落了。對于分布式,如果可以選擇,必然要選擇現在成熟的框架。但是項目有很多時候,由于各種歷史原因,必須要在原來的基礎上改造。這個時候,希望我寫的這個系列對大家有所幫助。造輪子有時候就是這么簡單。


相關鏈接

  • https://www.cnblogs.com/Kidezyq/p/8748961.html
  • https://www.cnblogs.com/Kidezyq/p/8977750.html
黎明前最黑暗,成功前最絕望!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/390184.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/390184.shtml
英文地址,請注明出處:http://en.pswp.cn/news/390184.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

85. 最大矩形

85. 最大矩形 給定一個僅包含 0 和 1 、大小為 rows x cols 的二維二進制矩陣&#xff0c;找出只包含 1 的最大矩形&#xff0c;并返回其面積。 示例 1&#xff1a; 輸入&#xff1a;matrix [[“1”,“0”,“1”,“0”,“0”],[“1”,“0”,“1”,“1”,“1”],[“1”,“1”…

TP單字母函數

A方法 A方法用于在內部實例化控制器 調用格式&#xff1a;A(‘[項目://][分組/]模塊’,’控制器層名稱’) 最簡單的用法&#xff1a; $User A(User); 表示實例化當前項目的UserAction控制器&#xff08;這個控制器對應的文件位于Lib/Action/UserAction.class.php&#xff09;…

Angular問題03 @angular/material版本問題

1 問題描述 應用使用 angular4在使用angular/material時&#xff0c;若果在導入模塊時使用mat開頭&#xff0c;就會報錯。 2 問題原因 angular/material版本出現問題&#xff0c;angular/material 從版本5開始就必須要angular5的核心依賴&#xff1b;想要在angular5之前版本中的…

onclick判斷組件調用_從子組件Onclick更新狀態

onclick判斷組件調用How to update the state of a parent component from a child component is one of the most commonly asked React questions.如何從子組件更新父組件的狀態是最常見的React問題之一。 Imagine youre trying to write a simple recipe box application, …

Python 列表List的定義及操作

# 列表概念&#xff1a;有序的可變的元素集合# 定義 # 直接定義 nums [1,2,3,4,5]# 通過range函數構造&#xff0c;python2 和python3 版本之間的差異&#xff1b; # python3 用的時候才會去構造 nums range(1,101)# 列表嵌套 # 注意和C語言中數組的區別,是否可…

遞歸分解因數

題目總時間限制: 1000ms 內存限制: 65536kB描述給出一個正整數a&#xff0c;要求分解成若干個正整數的乘積&#xff0c;即a a1 * a2 * a3 * ... * an&#xff0c;并且1 < a1 < a2 < a3 < ... < an&#xff0c;問這樣的分解的種數有多少。注意到a a也是一種分解…

劍指 Offer 51. 數組中的逆序對

劍指 Offer 51. 數組中的逆序對 在數組中的兩個數字&#xff0c;如果前面一個數字大于后面的數字&#xff0c;則這兩個數字組成一個逆序對。輸入一個數組&#xff0c;求出這個數組中的逆序對的總數。 示例 1: 輸入: [7,5,6,4] 輸出: 5 限制&#xff1a; 0 < 數組長度 &…

react 圖像識別_無法在React中基于URL查找圖像

react 圖像識別If youre new to React and are having trouble accessing images stored locally, youre not alone.如果您不熟悉React&#xff0c;并且無法訪問本地存儲的圖像&#xff0c;那么您并不孤單。 Imagine you have your images stored in a directory next to a co…

html單行元素居中顯示,多行元素居左顯示

有很多的業務需要元素或者文字如果單行&#xff0c;居中顯示&#xff0c;如果數據增多&#xff0c;居中顯示代碼&#xff08;直接復制到編輯器可用&#xff09;&#xff1a;<!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8&q…

ML.NET 0.2版增加了集群和新示例

在今年的Build大會上&#xff0c;微軟首次發布了ML.NET。ML.NET是開源的、跨平臺的以及運行在.NET上的機器學習框架。微軟的Ankit Asthana宣布該項目已經完成了第二版的開發。第二版增加了幾個新功能&#xff0c;包括名為集群的新機器學習任務&#xff0c;交叉驗證和訓練-測試&…

如何變得井井有條-來之不易的秘訣來組織您的生活

Because of the changes brought about by COVID-19, many people have had to find healthy and productive ways of working remotely. 由于COVID-19帶來的變化&#xff0c;許多人不得不尋找健康有效的遠程工作方式。 Some have been sent home and can continue doing thei…

被未知進程占用端口的解決辦法

echo off echo 這是用來結束一個未知進程占用端口的批處理可執行文件ipconfig /allnetstat -anoecho 請查看以上信息&#xff0c;輸入被占用的端口號:set /p port請輸入port:tasklist|findstr %port%echo 請結合上述程序進行輸入&#xff0c;請**謹慎輸入**set /p program請輸入…

怎樣在減少數據中心成本的同時不犧牲性能?

2019獨角獸企業重金招聘Python工程師標準>>> 導讀雖然組織對數據中心提出了更高的要求&#xff0c;但IT管理人員確實有辦法在嚴格的預算內展開工作。如今&#xff0c;組織認為即使性能預期不斷提高&#xff0c;其數據中心預算也在縮減。盡管2018年IT支出總體預計增長…

賽普拉斯 12864_如何使用賽普拉斯自動化輔助功能測試

賽普拉斯 12864In my previous post, I covered how to add screenshot testing in Cypress to ensure components dont unintentionally change over time. 在上一篇文章中 &#xff0c;我介紹了如何在賽普拉斯中添加屏幕截圖測試&#xff0c;以確保組件不會隨時間變化。 Now…

anaconda在win下和在mac下的安裝區別

1. 在win下安裝anaconda后會提示你選擇環境變量&#xff0c;但是建議使用默認。 于是CMD進入終端和使用navigator進入終端不一樣&#xff0c;前者會提示無此命令&#xff0c;只能通過navigator進入終端 即使在系統變量變量Path里添加了路徑&#xff0c;使用CMD還是不能使用pyth…

fcn從頭開始_如何使用Go從頭開始構建區塊鏈

fcn從頭開始介紹 (Introduction) With Web 3.0 and blockchain becoming more mainstream every day, do you know what blockchain is? Do you know its technical advantages and use-cases?隨著Web 3.0和區塊鏈每天變得越來越主流&#xff0c;您知道什么是區塊鏈嗎&#x…

java實現無序數組結構

一、數組的2種定義方式 數據類型 [] 數組名稱 new 數據類型[數組長度]; 這里 [] 可以放在數組名稱的前面&#xff0c;也可以放在數組名稱的后面&#xff0c;一般放在名稱的前面 數據類型 [] 數組名稱 {數組元素1&#xff0c;數組元素2&#xff0c;......} 這種方式聲明數組的…

Android App 的主角:Activity

Android App 程序主要由4種類型組成&#xff1a; 1.Activity&#xff08;活動&#xff09;&#xff1a;主要負責屏幕顯示畫面&#xff0c;并處理與用戶的互動。每個Android App至少都會有一個Activity&#xff0c;在程序一啟動時顯示主畫面供用戶操作。 2.Service&#xff08;后…

通過構建Paint App學習React Hooks

According to people in the know, React Hooks are hot, hot, hot. In this article, we follow Christian Jensens 14-part tutorial to find out about the basics of this new feature of React. Follow along to find out more! 據知情人士稱&#xff0c;React Hooks很熱&…

正則表達式 匹配常用手機號 (13、15\17\18開頭的十一位手機號)

原文:正則表達式 匹配常用手機號 &#xff08;13、15\17\18開頭的十一位手機號&#xff09;^1[3578]\d{9}$ ^1表示以1開頭&#xff0c;[3578]表示第二位的數字為3578中的任意一個&#xff0c;\d{9}表示0~9范圍內的數字匹配九次,$表示結束&#xff0c;12位以上的數字不匹配。