第三集:分布式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

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" name="businessCaches">
  4. <diskStore path="java.io.tmpdir/ehcache"/>
  5. <cache name="business1Cache"
  6. maxElementsInMemory="10000000"
  7. eternal="true"
  8. overflowToDisk="false"
  9. memoryStoreEvictionPolicy="LRU">
  10. <cacheEventListenerFactory
  11. class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>
  12. </cache>
  13. <cache name="business2Cache"
  14. maxElementsInMemory="100"
  15. eternal="true"
  16. overflowToDisk="false"
  17. memoryStoreEvictionPolicy="LRU">
  18. <cacheEventListenerFactory
  19. class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>
  20. </cache>
  21. <!-- cache发布信息配置,人工发现peerDiscovery=manual,cacheNames可配置多个缓存名称,以|分割 ) -->
  22. <cacheManagerPeerProviderFactory
  23. class="com.rampage.cache.distribute.factory.DisRMICacheManagerPeerProviderFactory"
  24. properties="peerDiscovery=manual, cacheNames=business1Cache|business2Cache" />
  25. <!-- 接收同步cache信息的地址 -->
  26. <cacheManagerPeerListenerFactory
  27. class="com.rampage.cache.distribute.factory.DisRMICacheManagerPeerListenerFactory"
  28. properties="socketTimeoutMillis=2000" />
  29. </ehcache>

​ spring-cache.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:cache="http://www.springframework.org/schema/cache"
  5. xmlns:context="http://www.springframework.org/schema/context"
  6. xmlns:aop="http://www.springframework.org/schema/aop"
  7. xsi:schemaLocation="http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd
  8. http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  9. http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
  10. http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd"
  11. default-autowire="byName">
  12. <!-- 包扫描 -->
  13. <context:component-scan base-package="com.rampage.cache" />
  14. <!-- 启用Cache注解 -->
  15. <cache:annotation-driven cache-manager="cacheManager"
  16. key-generator="keyGenerator" proxy-target-class="true" />
  17. <!-- 自定义的缓存key生成类,需实现org.springframework.cache.interceptor.KeyGenerator接口 -->
  18. <bean id="keyGenerator" class="com.rampage.cache.support.CustomKeyGenerator" />
  19. <!-- 替换slite的ehcache实现 -->
  20. <bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
  21. <property name="configLocation" value="classpath:spring/cache/sppay-ehcache-cache.xml"/>
  22. <!-- value对应前面ehcache文件定义的manager名称 -->
  23. <property name="cacheManagerName" value="businessCaches" />
  24. </bean>
  25. <bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
  26. <property name="cacheManager" ref="ehCacheManagerFactory"/>
  27. </bean>
  28. <bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
  29. <property name="cacheManagers">
  30. <list>
  31. <ref bean="ehCacheManager" />
  32. </list>
  33. </property>
  34. <property name="fallbackToNoOpCache" value="true" />
  35. </bean>
  36. </beans>

实现自定义转发和监听

​ 细心的读者应该不难发现,前面xml配置中cacheManagerPeerProviderFactorycacheManagerPeerListenerFactory我使用的都是自定义的类。之所以使用自定义的类,是为了在初始化的时候发布的地址和端口,监听的地址端口可以在配置文件配置。具体类的实现如下:

  1. /**
  2. * 分布式EhCache监听工厂
  3. * @author secondWorld
  4. *
  5. */
  6. public class DisRMICacheManagerPeerListenerFactory extends RMICacheManagerPeerListenerFactory {
  7. private static final Logger LOGGER = LoggerFactory.getLogger(DisRMICacheManagerPeerListenerFactory.class);
  8. /**
  9. * 配置文件中配置的监听地址,可以不配置,默认为本机地址
  10. */
  11. private static final String LISTEN_HOST = "distribute.ehcache.listenIP";
  12. /**
  13. * 配置文件中配置的监听端口
  14. */
  15. private static final String LISTEN_PORT = "distribute.ehache.listenPort";
  16. @Override
  17. protected CacheManagerPeerListener doCreateCachePeerListener(String hostName, Integer port,
  18. Integer remoteObjectPort, CacheManager cacheManager, Integer socketTimeoutMillis) {
  19. // xml中hostName为空,则读取配置文件(app-config.properties)中的值
  20. if (StringUtils.isEmpty(hostName)) {
  21. String propHost = AppConfigPropertyUtils.get(LISTEN_HOST);
  22. if (StringUtils.isNotEmpty(propHost)) {
  23. hostName = propHost;
  24. }
  25. }
  26. // 端口采用默认端口0,则去读取配置文件(app-config.properties)中的值
  27. if (port != null && port == 0) {
  28. Integer propPort = null;
  29. try {
  30. propPort = Integer.parseInt(AppConfigPropertyUtils.get(LISTEN_PORT));
  31. } catch (NumberFormatException e) {
  32. }
  33. if (propPort != null) {
  34. port = propPort;
  35. }
  36. }
  37. LOGGER.info(
  38. "Initiliazing DisRMICacheManagerPeerListenerFactory:cacheManager[{}], hostName[{}], port[{}], remoteObjectPort[{}], socketTimeoutMillis[{}]......",
  39. cacheManager, hostName, port, remoteObjectPort, socketTimeoutMillis);
  40. return super.doCreateCachePeerListener(hostName, port, remoteObjectPort, cacheManager, socketTimeoutMillis);
  41. }
  42. }
  43. /**
  44. * 分布式EhCache发布工厂
  45. *
  46. * @author secondWorld
  47. *
  48. */
  49. public class DisRMICacheManagerPeerProviderFactory extends RMICacheManagerPeerProviderFactory {
  50. private static final Logger LOGGER = LoggerFactory.getLogger(DisRMICacheManagerPeerProviderFactory.class);
  51. private static final String CACHENAME_DELIMITER = "|";
  52. private static final String PROVIDER_ADDRESSES = "distribute.ehcache.providerAddresses";
  53. private static final String CACHE_NAMES = "cacheNames";
  54. /**
  55. * rmi地址格式: //127.0.0.1:4447/Cache1|//127.0.0.1:4447/Cache2
  56. */
  57. @Override
  58. protected CacheManagerPeerProvider createManuallyConfiguredCachePeerProvider(Properties properties) {
  59. // 从app-config.properties中读取发布地址列表
  60. String providerAddresses = AppConfigPropertyUtils.get(PROVIDER_ADDRESSES, StringUtils.EMPTY);
  61. // 从ehcache配置文件读取缓存名称
  62. String cacheNames = PropertyUtil.extractAndLogProperty(CACHE_NAMES, properties);
  63. // 参数校验,这里发布地址和缓存名称都不能为空
  64. if (StringUtils.isEmpty(providerAddresses) || StringUtils.isEmpty(cacheNames)) {
  65. throw new IllegalArgumentException("Elements \"providerAddresses\" and \"cacheNames\" are needed!");
  66. }
  67. // 解析地址列表
  68. List<String> cachesNameList = getCacheNameList(cacheNames);
  69. List<String> providerAddressList = getProviderAddressList(providerAddresses);
  70. // 注册发布节点
  71. RMICacheManagerPeerProvider rmiPeerProvider = new ManualRMICacheManagerPeerProvider();
  72. StringBuilder sb = new StringBuilder();
  73. for (String cacheName : cachesNameList) {
  74. for (String providerAddress : providerAddressList) {
  75. sb.setLength(0);
  76. sb.append("//").append(providerAddress).append("/").append(cacheName);
  77. rmiPeerProvider.registerPeer(sb.toString());
  78. LOGGER.info("Registering peer provider [{}]", sb);
  79. }
  80. }
  81. return rmiPeerProvider;
  82. }
  83. /**
  84. * 得到发布地址列表
  85. * @param providerAddresses 发布地址字符串
  86. * @return 发布地址列表
  87. */
  88. private List<String> getProviderAddressList(String providerAddresses) {
  89. StringTokenizer stringTokenizer = new StringTokenizer(providerAddresses,
  90. AppConfigPropertyUtils.APP_ITEM_DELIMITER);
  91. List<String> ProviderAddressList = new ArrayList<String>(stringTokenizer.countTokens());
  92. while (stringTokenizer.hasMoreTokens()) {
  93. String providerAddress = stringTokenizer.nextToken();
  94. providerAddress = providerAddress.trim();
  95. ProviderAddressList.add(providerAddress);
  96. }
  97. return ProviderAddressList;
  98. }
  99. /**
  100. * 得到缓存名称列表
  101. * @param cacheNames 缓存名称字符串
  102. * @return 缓存名称列表
  103. */
  104. private List<String> getCacheNameList(String cacheNames) {
  105. StringTokenizer stringTokenizer = new StringTokenizer(cacheNames, CACHENAME_DELIMITER);
  106. List<String> cacheNameList = new ArrayList<String>(stringTokenizer.countTokens());
  107. while (stringTokenizer.hasMoreTokens()) {
  108. String cacheName = stringTokenizer.nextToken();
  109. cacheName = cacheName.trim();
  110. cacheNameList.add(cacheName);
  111. }
  112. return cacheNameList;
  113. }
  114. @Override
  115. protected CacheManagerPeerProvider createAutomaticallyConfiguredCachePeerProvider(CacheManager cacheManager,
  116. Properties properties) throws IOException {
  117. throw new UnsupportedOperationException("Not supported automatic distribute cache!");
  118. }
  119. }

配置

​ 假设有三台机器,则他们分别得配置如下:

  1. #应用1,在4447端口监听
  2. #缓存同步消息发送地址(如果同步到多台需要配置多台地址,多台地址用英文逗号分隔)
  3. distribute.ehcache.providerAddresses=127.0.0.1:4446,127.0.0.1:4448
  4. #缓存同步监听端口和IP
  5. distribute.ehache.listenPort=4447
  6. distribute.ehcache.listenIP=localhost
  7. #应用2,在4448端口监听
  8. #缓存同步消息发送地址(如果同步到多台需要配置多台地址,多台地址用英文逗号分隔)
  9. distribute.ehcache.providerAddresses=127.0.0.1:4446,127.0.0.1:4447
  10. #缓存同步监听端口和IP
  11. distribute.ehache.listenPort=4448
  12. distribute.ehcache.listenIP=localhost
  13. #应用3,在4446端口监听
  14. #缓存同步消息发送地址(如果同步到多台需要配置多台地址,多台地址用英文逗号分隔)
  15. distribute.ehcache.providerAddresses=127.0.0.1:4447,127.0.0.1:4448
  16. #缓存同步监听端口和IP
  17. distribute.ehache.listenPort=4446
  18. distribute.ehcache.listenIP=localhost

使用

​ 使用的时候直接通过Spring的缓存注解即可。简单的示例如下:

  1. @CacheConfig("business1Cache")
  2. @Component
  3. public class Business1 {
  4. @Cacheable
  5. public String getData(String key) {
  6. // TODO:...
  7. }
  8. }

说明

​ 前面的实现是通过RMI的方式来实现缓存同步的,相对来说RMI的效率还是很快的。所以如果不需要实时的缓存一致性,允许少许延迟,那么这种方式的实现足够。


总结

​ 到这篇完成,分布式改造的第一章算是告一段落了。对于分布式,如果可以选择,必然要选择现在成熟的框架。但是项目有很多时候,由于各种历史原因,必须要在原来的基础上改造。这个时候,希望我写的这个系列对大家有所帮助。造轮子有时候就是这么简单。


相关链接

分布式改造剧集三:Ehcache分布式改造的更多相关文章

  1. 分布式改造剧集2---DIY分布式锁

    前言: ​ 好了,终于又开始播放分布式改造剧集了.前面一集中(http://www.cnblogs.com/Kidezyq/p/8748961.html)我们DIY了一个Hessian转发实现,最后我 ...

  2. 分布式改造剧集之Redis缓存采坑记

    Redis缓存采坑记 ​ 前言 ​ 这个其实应该属于分布式改造剧集中的一集(第一集见前面博客:http://www.cnblogs.com/Kidezyq/p/8748961.html),本来按照顺序 ...

  3. EhCache 分布式缓存/缓存集群

    开发环境: System:Windows JavaEE Server:tomcat5.0.2.8.tomcat6 JavaSDK: jdk6+ IDE:eclipse.MyEclipse 6.6 开发 ...

  4. EhCache 分布式缓存/缓存集群(转)

    开发环境: System:Windows JavaEE Server:tomcat5.0.2.8.tomcat6 JavaSDK: jdk6+ IDE:eclipse.MyEclipse 6.6 开发 ...

  5. EHCache分布式缓存集群环境配置

    EHCache分布式缓存集群环境配置 ehcache提供三种网络连接策略来实现集群,rmi,jgroup还有jms.同时ehcache可以可以实现多播的方式实现集群,也可以手动指定集群主机序列实现集群 ...

  6. ubuntu12.04+Elasticsearch2.3.3伪分布式配置,集群状态分片调整

    目录 [TOC] 1.什么是Elashticsearch 1.1 Elashticsearch介绍 Elasticsearch是一个基于Apache Lucene(TM)的开源搜索引擎.能够快速搜索数 ...

  7. iOS开发——源代码管理——git(分布式版本控制和集中式版本控制对比,git和SVN对比,git常用指令,搭建GitHub远程仓库,搭建oschina远程仓库 )

    一.git简介 什么是git? git是一款开源的分布式版本控制工具 在世界上所有的分布式版本控制工具中,git是最快.最简单.最流行的   git的起源 作者是Linux之父:Linus Bened ...

  8. Apache Spark探秘:三种分布式部署方式比较

    转自:链接地址: http://dongxicheng.org/framework-on-yarn/apache-spark-comparing-three-deploying-ways/     目 ...

  9. Ubuntu16.04.1上搭建分布式的Redis集群

    为什么要集群: 通常为了,提高网站的响应速度,总是把一些经常用到的数据放到内存中,而不是放到数据库中,Redis是一个很好的Cache工具,当然了还有Memcached,这里只讲Redis.在我们的电 ...

随机推荐

  1. [c# 20问] 4.Console应用获取执行路径

    一行代码可以搞定了~ static void GetAppPath() { string path = System.Reflection.Assembly.GetExecutingAssembly( ...

  2. C#委托、事件剖析(上)

    本节对委托.事件做以总结. 一.委托: 1.概念:先来说明变量和函数的概念,变量,以某个地址为起点的一段内存中所存储的值,函数,以某个地址为起点的一段内存中存储的机器语言指令.有了这2个概念以后,我们 ...

  3. .net core 基于Jwt实现Token令牌

    Startup类ConfigureServices中 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJw ...

  4. C#MVC和cropper.js实现剪裁图片ajax上传的弹出层

     首先使用cropper.js插件,能够将剪裁后的图片返回为base64编码,后台根据base64编码解析保存图片. jQuery.cropper: 是一款使用简单且功能强大的图片剪裁jquery插件 ...

  5. 浏览器对HTTP请求的编码行为

    浏览器对请求的URL编码行为 浏览器会对请求的URL中非ASCII码字符进行编码.这里不是指对整个URL进行编码,而是仅仅对非ASCII码字符部分进行编码,详情请看下面的实验记录. 实验一:在URL参 ...

  6. OSLab课堂作业1

        日期:2019/3/16 作业:实现命令cat, cp, echo. myecho命令 #include <stdio.h> int main(int argc, char *ar ...

  7. Android------------------的资源文件的学习

    一.style的学习 用法: 使用: 使用系统自带的style的风格 使用: 效果: 二.drawable的使用 selector是一个xml文件进行加载使用的: 文件名叫做buttonselecto ...

  8. brew - 更换国内源

    brew如果不换成国内源,安装软件时候可能会出问题,不是安装不了就是速度很慢,所以使用它,更换国内游是比较好的选择! 我更换的是清华大学开源软件镜像站,打开shell窗口,依次执行下面命令: cd & ...

  9. jzoj5347

    tj:80pts:維護f[i][j]表示當前第i個方塊必須選,且選了j個的最優解,設w[i]為第i個方塊長度 則可以枚舉上次選了第k個方塊,則f[i][j]=max{f[k][j-1]+w[i]*(i ...

  10. nginx代理websocket协议

    以下是代码段.location /wsapp/ {     proxy_pass http://wsbackend;     proxy_http_version 1.1;     proxy_set ...