分布式改造剧集三:Ehcache分布式改造
第三集:分布式Ehcache緩存改造
前言
? 好久沒有寫博客了,大有半途而廢的趨勢。忙不是借口,這個好習(xí)慣還是要繼續(xù)堅持。前面我承諾的第一期的DIY分布式,是時候上終篇了---DIY分布式緩存。
探索之路
? 在前面的文章中,我給大家大致說過項目背景:項目中的緩存使用的是Ehcache。因為前面使用Ehcache的應(yīng)用就一臺,所以這種單機的Ehcache并不會有什么問題。現(xiàn)在分布式部署之后,如果各個應(yīng)用之間的緩存不能共享,那么其實各自就是一個孤島。可能在一個業(yè)務(wù)跑下來,請求了不同的應(yīng)用,結(jié)果在緩存中取出來的值不一樣,
造成數(shù)據(jù)不一致。所以需要重新設(shè)計緩存的實現(xiàn)。
? 因為盡量不要引入新的中間件,所以改造仍然是圍繞Ehcache來進(jìn)行的。搜集了各種資料之后,發(fā)現(xiàn)Ehcache實現(xiàn)分布式緩存基本有以下兩種思路:
-
客戶端實現(xiàn)分布式算法: 在使用Ehcache的客戶端自己實現(xiàn)分布式算法。
算法的基本思路就是取模:即假設(shè)有三臺應(yīng)用(編號假設(shè)分別為0,1,2),對于一個要緩存的對象,首先計算其key的hash值,然后將hash值模3,得到的余數(shù)是幾,就將數(shù)據(jù)緩存到哪臺機器。
同步冗余數(shù)據(jù): Ehcache是支持集群配置的,集群的各個節(jié)點之間支持按照一定的協(xié)議進(jìn)行數(shù)據(jù)同步。這樣每臺應(yīng)用其實緩存了一整份數(shù)據(jù),不同節(jié)點之間的數(shù)據(jù)是一致的。
? 雖然冗余的辦法顯得有點浪費資源,但是我最終還是選擇了冗余。具體原因有以下幾點:
- 分布式算法的復(fù)雜性: 前面所講的分布式算法只是最基本的實現(xiàn)。事實上實現(xiàn)要比這個復(fù)雜的多。需要考慮增加或者刪除節(jié)點的情況,需要使用更加復(fù)雜的一致性hash算法
- 可能導(dǎo)致整個應(yīng)用不可用: 當(dāng)刪除節(jié)點之后,如果算法不能夠感知進(jìn)行自動調(diào)整,仍然去請求那個已經(jīng)被刪除的節(jié)點,可能導(dǎo)致整個系統(tǒng)不可用。
Demo
? 最終我的實現(xiàn)采用RMI的方式進(jìn)行同步
配置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發(fā)布信息配置,人工發(fā)現(xiàn)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生成類,需實現(xiàn)org.springframework.cache.interceptor.KeyGenerator接口 --><bean id="keyGenerator" class="com.rampage.cache.support.CustomKeyGenerator" /><!-- 替換slite的ehcache實現(xiàn) --><bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"><property name="configLocation" value="classpath:spring/cache/sppay-ehcache-cache.xml"/><!-- value對應(yīng)前面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>實現(xiàn)自定義轉(zhuǎn)發(fā)和監(jiān)聽
? 細(xì)心的讀者應(yīng)該不難發(fā)現(xiàn),前面xml配置中cacheManagerPeerProviderFactory和cacheManagerPeerListenerFactory我使用的都是自定義的類。之所以使用自定義的類,是為了在初始化的時候發(fā)布的地址和端口,監(jiān)聽的地址端口可以在配置文件配置。具體類的實現(xiàn)如下:
/*** 分布式EhCache監(jiān)聽工廠* @author secondWorld**/ public class DisRMICacheManagerPeerListenerFactory extends RMICacheManagerPeerListenerFactory {private static final Logger LOGGER = LoggerFactory.getLogger(DisRMICacheManagerPeerListenerFactory.class);/*** 配置文件中配置的監(jiān)聽地址,可以不配置,默認(rèn)為本機地址*/private static final String LISTEN_HOST = "distribute.ehcache.listenIP";/*** 配置文件中配置的監(jiān)聽端口*/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;}}// 端口采用默認(rèn)端口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發(fā)布工廠* * @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中讀取發(fā)布地址列表String providerAddresses = AppConfigPropertyUtils.get(PROVIDER_ADDRESSES, StringUtils.EMPTY);// 從ehcache配置文件讀取緩存名稱String cacheNames = PropertyUtil.extractAndLogProperty(CACHE_NAMES, properties);// 參數(shù)校驗,這里發(fā)布地址和緩存名稱都不能為空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);// 注冊發(fā)布節(jié)點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;}/*** 得到發(fā)布地址列表* @param providerAddresses 發(fā)布地址字符串* @return 發(fā)布地址列表*/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!");} }配置
? 假設(shè)有三臺機器,則他們分別得配置如下:
#應(yīng)用1,在4447端口監(jiān)聽 #緩存同步消息發(fā)送地址(如果同步到多臺需要配置多臺地址,多臺地址用英文逗號分隔) distribute.ehcache.providerAddresses=127.0.0.1:4446,127.0.0.1:4448 #緩存同步監(jiān)聽端口和IP distribute.ehache.listenPort=4447 distribute.ehcache.listenIP=localhost#應(yīng)用2,在4448端口監(jiān)聽 #緩存同步消息發(fā)送地址(如果同步到多臺需要配置多臺地址,多臺地址用英文逗號分隔) distribute.ehcache.providerAddresses=127.0.0.1:4446,127.0.0.1:4447 #緩存同步監(jiān)聽端口和IP distribute.ehache.listenPort=4448 distribute.ehcache.listenIP=localhost#應(yīng)用3,在4446端口監(jiān)聽 #緩存同步消息發(fā)送地址(如果同步到多臺需要配置多臺地址,多臺地址用英文逗號分隔) distribute.ehcache.providerAddresses=127.0.0.1:4447,127.0.0.1:4448 #緩存同步監(jiān)聽端口和IP distribute.ehache.listenPort=4446 distribute.ehcache.listenIP=localhost使用
? 使用的時候直接通過Spring的緩存注解即可。簡單的示例如下:
@CacheConfig("business1Cache") @Component public class Business1 {@Cacheablepublic String getData(String key) {// TODO:...} }說明
? 前面的實現(xiàn)是通過RMI的方式來實現(xiàn)緩存同步的,相對來說RMI的效率還是很快的。所以如果不需要實時的緩存一致性,允許少許延遲,那么這種方式的實現(xiàn)足夠。
總結(jié)
? 到這篇完成,分布式改造的第一章算是告一段落了。對于分布式,如果可以選擇,必然要選擇現(xiàn)在成熟的框架。但是項目有很多時候,由于各種歷史原因,必須要在原來的基礎(chǔ)上改造。這個時候,希望我寫的這個系列對大家有所幫助。造輪子有時候就是這么簡單。
相關(guān)鏈接
- https://www.cnblogs.com/Kidezyq/p/8748961.html
- https://www.cnblogs.com/Kidezyq/p/8977750.html
總結(jié)
以上是生活随笔為你收集整理的分布式改造剧集三:Ehcache分布式改造的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 梦到别人吃肉是什么意思
- 下一篇: TP单字母函数