Spring-ehcache RMI形式的分布式缓存配置

简介

这是本人因为工作需要研究的关于ehcache的分布式RMI模式的使用心得已经自己的一些心得。

git 源码

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+:(冒号)+端口+/(斜杠)+缓存属性名称

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

发布了22 篇原创文章 · 获赞 1 · 访问量 3253

猜你喜欢

转载自blog.csdn.net/tian_111222333/article/details/97267625