分布式改造剧集三: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.mzph.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位以上的数字不匹配。