集群环境中使用 EhCache 缓存系统

EhCache 缓存系统 :

 本章节将要介绍EhCache及EhCache实现分布式的一些解决方案、并针对于这些解决性方案做一个实现,后续将出一个提供项目模块化、服务化、插件化的VieMall快速开发平台,同时集成Dubbo服务化、Zookeeper(分布式调度/分布式配置管理服务)、Redis分布式缓存技术及Memcache/Ehcache 二级缓存切换、FastDFS分布式文件系统、ActiveMQ异步消息中间件、Solr搜索、Nginx负载均衡等分布式及读写分离、如果有时间可以深入分表分库(mycat /sharding-jdbc),并且将这每一项技术细化分析,一起来学习和分享。

EhCache 缓存系统简介:

 EhCache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider。
  下图是 EhCache 在应用程序中的位置:

   

   

 EhCache 的主要特性有:

1.快速;

2.简单;

3.多种缓存策略;

4.缓存数据有两级:内存和磁盘,因此无需担心容量问题;

5.缓存数据会在虚拟机重启的过程中写入磁盘;

6.可以通过 RMI、可插入 API 等方式进行分布式缓存;

7.具有缓存和缓存管理器的侦听接口;

8.支持多缓存管理器实例,以及一个实例的多个缓存区域;

9.提供 Hibernate 的缓存实现;

10.等等 …   

     由于 EhCache 是进程中的缓存系统,一旦将应用部署在集群环境中,每一个节点维护各自的缓存数据,当某节点对缓存数据进行更新,这些更新的数据无法在其它节点中共享,这不仅会降低节点运行的效率,而且会导致数据不同步的情况发生。例如某个网站采用 A、B 两个节点作为集群部署,当 A 节点的缓存更新后,而 B 节点缓存尚未更新就可能出现用户在浏览页面的时候,一会是更新后的数据,一会是尚未更新的数据,尽管我们也可以通过 Session Sticky 技术来将用户锁定在某个节点上,但对于一些交互性比较强或者是非 Web 方式的系统来说,Session Sticky 显然不太适合。所以就需要用到 EhCache 的集群解决方案。

EhCache 从 1.7 版本开始,支持五种集群方案,分别是:
   • Terracotta
   • RMI
   • JMS
   • JGroups
   • EhCache Server

   

   本文主要介绍其中的三种最为常用集群方式,分别是 RMI、JGroups 以及 EhCache Server 。

 RMI 集群模式:

   RMI 是 Java 的一种远程方法调用技术,是一种点对点的基于 Java 对象的通讯方式。EhCache 从 1.2 版本开始就支持 RMI 方式的缓存集群。在集群环境中 EhCache 所有缓存对象的键和值都必须是可序列化的,也就是必须实现 java.io.Serializable 接口,这点在其它集群方式下也是需要遵守的。
下图是 RMI 集群模式的结构图:

  图 2. RMI 集群模式结构图:

     

    

   采用 RMI 集群模式时,集群中的每个节点都是对等关系,并不存在主节点或者从节点的概念,因此节点间必须有一个机制能够互相认识对方,必须知道其它节点的信息,包括主机地址、端口号等。EhCache 提供两种节点的发现方式:手工配置和自动发现。手工配置方式要求在每个节点中配置其它所有节点的连接信息,一旦集群中的节点发生变化时,需要对缓存进行重新配置。
由于 RMI 是 Java 中内置支持的技术,因此使用 RMI 集群模式时,无需引入其它的 Jar 包,EhCache 本身就带有支持 RMI 集群的功能。使用 RMI 集群模式需要在 ehcache.xml 配置文件中定义cacheManagerPeerProviderFactory 节点。假设集群中有两个节点,分别对应的 RMI 绑定信息是:
    

  自动方式:

      自动发现方式使用tcp广播来建立和包含一个广播组,它的特征是最小配置和对成员组的自动添加和管理。没有那个服务器是有优先级的。对等点每一秒中向广播组发送心跳,如果一个对等点在五秒钟内没发送过来,则此对等点将会被删除,如果有新的,则会被加入集群。 

  

 <cacheManagerPeerProviderFactory  
    class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"   
    properties="peerDiscovery=automatic, multicastGroupAddress=224.1.1.1, multicastGroupPort=1000, timeToLive=32" />

 解析:

    peerDiscovery 方式:atutomatic 为自动 ;mulicastGroupAddress 广播组地址:230.0.0.1;mulicastGroupPort 广播组端口:40001;timeToLive 搜索某个网段上的缓存:0是限制在同一个服务器,1是限制在同一个子网,32是限制在同一个网站,64是限制在同一个region,128是同一块大陆,还有个256,我就不说了;hostName:主机名或者ip,用来接受或者发送信息的接口。同时组播地址可以指定 D 类 IP 地址空间,范围从 224.0.1.0 到 238.255.255.255 中的任何一个地址。

 如下是详细的配置:

   Server1- 1000:   

 <cacheManagerPeerProviderFactory  
class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"  
properties="peerDiscovery=automatic, multicastGroupAddress=224.1.1.1,  
            multicastGroupPort=1000, timeToLive=32" />  
<cacheManagerPeerListenerFactory  
        class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"  
        properties="hostName=127.0.0.1,port=1000,socketTimeoutMillis=120000" />  
<!-- 默认缓存 -->  
    <defaultCache maxElementsInMemory="1000" eternal="true"  
        timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true"  
        diskSpoolBufferSizeMB="30" maxElementsOnDisk="10000000"  
        diskPersistent="true" diskExpiryThreadIntervalSeconds="120"  
        memoryStoreEvictionPolicy="LRU">  
    </defaultCache>  
      
    <!-- demo缓存 -->  
    <cache name="user" maxElementsInMemory="1000" eternal="false"  
        timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true"  
        diskSpoolBufferSizeMB="30" maxElementsOnDisk="10000000"  
        diskPersistent="false" diskExpiryThreadIntervalSeconds="120"  
        memoryStoreEvictionPolicy="LRU">  
        <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" />  
        <!-- 用于在初始化缓存,以及自动设置 -->  
        <bootstrapCacheLoaderFactory  class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory" />  
    </cache>

 Server2 -2000:  

 
<cacheManagerPeerProviderFactory  
class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"  
properties="peerDiscovery=automatic, multicastGroupAddress=224.1.1.1,  
            multicastGroupPort=1000, timeToLive=32" />  
<cacheManagerPeerListenerFactory  
        class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"  
        properties="hostName=127.0.0.1,port=2000,socketTimeoutMillis=120000" />  
          
     <!-- 默认缓存 -->  
     <defaultCache maxElementsInMemory="1000" eternal="true"  
        timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true"  
        diskSpoolBufferSizeMB="30" maxElementsOnDisk="10000000"  
        diskPersistent="true" diskExpiryThreadIntervalSeconds="120"  
        memoryStoreEvictionPolicy="LRU">  
     </defaultCache>  
      
      
    <!-- demo缓存 -->  
    <cache name="user" maxElementsInMemory="1000" eternal="false"  
        timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true"  
        diskSpoolBufferSizeMB="30" maxElementsOnDisk="10000000"  
        diskPersistent="false" diskExpiryThreadIntervalSeconds="120"  
        memoryStoreEvictionPolicy="LRU">  
        <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" />  
        <!-- 用于在初始化缓存,以及自动设置 -->  
        <bootstrapCacheLoaderFactory  class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory" />  
    </cache>

手动配置:

   手动成员发现需要给监听器配置ip地址和端口。任何成员不能在运行的时候添加和移除。手动的成员发现推荐在组播不能实现的时候使用,比如集群的路由不允许发送组播报文的时候。你也可以单向的缓存数据同步,比如让server2可以同步server1的数据,但sever1不能同步server2的。

配置手动发现成员时需要配置cacheManagerPeerProviderFactory的如下属性
      peerDiscovery=manual
      rmiUrls=//server:port/cacheName, …
      rmiUrls是缓存成员的列表,但是不包括自己,也就是被配置的这台服务器的地址。

   案例:

     比如你的集群里有server1和server2两台服务器,你希望同步 user
     下面是server1-2000的配置:
      

   <cacheManagerPeerProviderFactory   
            class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"   
            properties="hostName=127.0.0.1,  
            port=2000,  
            socketTimeoutMillis=12000,  
            peerDiscovery=manual,  
            rmiUrls=//127.0.0.1:1000/user"  
     />   
 <cacheManagerPeerListenerFactory  
        class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"  
        properties="hostName=127.0.0.1,port=2000,socketTimeoutMillis=120000" />  
<!-- demo缓存 -->  
    <cache name="user" maxElementsInMemory="1000" eternal="false"  
        timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true"  
        diskSpoolBufferSizeMB="30" maxElementsOnDisk="10000000"  
        diskPersistent="false" diskExpiryThreadIntervalSeconds="120"  
        memoryStoreEvictionPolicy="LRU">  
        <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" />  
        <!-- 用于在初始化缓存,以及自动设置 -->  
        <bootstrapCacheLoaderFactory  class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory" />  
    </cache>  
  
  
 如果想配置多个:rmiUrls=//127.0.0.1:1000/test1| rmiUrls=//127.0.0.1:1000/test2  
  
下面是server2-1000的配置:  
<cacheManagerPeerProviderFactory   
class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"   
properties="hostName=127.0.0.1,port=1000,socketTimeoutMillis=12000,  
            peerDiscovery=manual, rmiUrls=//127.0.0.1:2000/user"  
        />  
<cacheManagerPeerListenerFactory  
        class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"  
        properties="hostName=127.0.0.1,port=1000,socketTimeoutMillis=120000" />  
<!-- demo缓存 -->  
    <cache name="user" maxElementsInMemory="1000" eternal="false"  
        timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true"  
        diskSpoolBufferSizeMB="30" maxElementsOnDisk="10000000"  
        diskPersistent="false" diskExpiryThreadIntervalSeconds="120"  
        memoryStoreEvictionPolicy="LRU">  
        <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" />  
        <!-- 用于在初始化缓存,以及自动设置 -->  
        <bootstrapCacheLoaderFactory  class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory" />  
    </cache>

JGroups 集群模式:

 Jgroups入门与实践

  EhCache 从 1.5. 版本开始增加了 JGroups 的分布式集群模式。与 RMI 方式相比较, JGroups 提供了一个非常灵活的协议栈、可靠的单播和多播消息传输,主要的缺点是配置复杂以及一些协议栈对第三方包的依赖。

JGroups 也提供了基于 TCP 的单播 ( Unicast ) 和基于 UDP 的多播 ( Multicast ) ,对应 RMI 的手工配置和自动发现。使用单播方式需要指定其它节点的主机地址和端口,下面是两个节点.

 单播方式配置: 

  

<cacheManagerPeerProviderFactory  
        class="net.sf.ehcache.distribution.jgroups.JGroupsCacheManagerPeerProviderFactory"  
        properties="connect=TCP(bind_addr=127.0.0.1;bind_port=1000):  
         TCPPING(initial_hosts=127.0.0.1[1000],127.0.0.1[2000];port_range=1;timeout=5000;num_initial_members=2):  
         MERGE2(min_interval=3000;max_interval=5000):  
         FD_ALL(interval=5000;timeout=20000):  
         FD(timeout=5000;max_tries=48;):  
         VERIFY_SUSPECT(timeout=1500):  
         pbcast.NAKACK(retransmit_timeout=100,200,300,600,1200,2400,4800;discard_delivered_msgs=true):  
         pbcast.STABLE(stability_delay=1000;desired_avg_gossip=20000;max_bytes=0):  
         pbcast.GMS(print_local_addr=true;join_timeout=5000)"  
        propertySeparator="::" />

使用多播方式配置如下:    

EhCache 的 Groups 集群模式还有另外一种节点发现方式,  
              就是通过多播( multicast )来维护集群中的所有有效节点。  
              这也是最为简单而且灵活的方式,与手工模式不同的是,  
              每个节点上的配置信息都相同,大大方便了节点的部署,  
              集群启动时会自动发现同一局域网的缓存服务器  
              以jgroups 多播方式配置集群,自动发现集节点  
<cacheManagerPeerProviderFactory  
        class="net.sf.ehcache.distribution.jgroups.JGroupsCacheManagerPeerProviderFactory"  
        properties="connect=UDP(mcast_addr=224.1.1.1;mcast_port=45678;ip_ttl=32;mcast_send_buf_size=120000;mcast_recv_buf_size=80000):   
        PING(timeout=2000;num_initial_members=2):   
        MERGE2(min_interval=5000;max_interval=10000):   
        FD_SOCK:VERIFY_SUSPECT(timeout=1500):   
        pbcast.NAKACK(retransmit_timeout=3000):   
        UNICAST(timeout=5000):   
        pbcast.STABLE(desired_avg_gossip=20000):   
        FRAG:   
        pbcast.GMS(join_timeout=5000;print_local_addr=true)"  
        propertySeparator="::" />

 从上面的配置来看,JGroups 的配置要比 RMI 复杂得多,但也提供更多的微调参数,有助于提升缓存数据复制的性能。详细的 JGroups 配置参数的具体意义可参考 JGroups 的配置手册。

  JGroups 方式对应缓存节点的配置信息如下:      

<!-- demo缓存   
       replicateAsynchronously  对象同步是否异步完成,默认为true。如果比较紧急就设为false。 在一致性时间性要求不强的时候,设为异步可大大提供性能,因为它是异步立即返回的,而且可以批量提交。   
       replicateUpdatesViaCopy 是否将对象变更复制到所有节点,还是只是发送一个失效信息,让对方该缓存失效,当对方需要该缓存时重新计算载入。   
默认为true。鉴于对象复制的消耗挺大的,又有锁的问题,而且对方也未必需要该对象,所以此属性建议设为false。如果业务上真的需要设为true时,就可考虑使用Terracotta了。   
       replicatePuts、replicateUpdates、replicateRemovals  增删改是否同步,默认都为true。但因为我们前面选择了失效算法,所以replicatePuts 要设为false。   
    -->  
    <cache name="user" maxElementsInMemory="1000" eternal="false"  
        timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true"  
        diskSpoolBufferSizeMB="30" maxElementsOnDisk="10000000"  
        diskPersistent="false" diskExpiryThreadIntervalSeconds="120"  
        memoryStoreEvictionPolicy="LRU">  
        <cacheEventListenerFactory  
            class="net.sf.ehcache.distribution.jgroups.JGroupsCacheReplicatorFactory"  
            properties="replicateAsynchronously=true, replicatePuts=true,  
        replicateUpdates=true, replicateUpdatesViaCopy=false, replicateRemovals=true" />  
        <!-- 用于在初始化缓存,以及自动设置 -->  
        <bootstrapCacheLoaderFactory  
            class="net.sf.ehcache.distribution.jgroups.JGroupsBootstrapCacheLoaderFactory" />  
    </cache>

 使用组播方式的注意事项:

  使用 JGroups 需要引入 JGroups 的 Jar 包以及 EhCache 对 JGroups 的封装包 ehcache-jgroupsreplication-xxx.jar 。
在一些启用了 IPv6 的电脑中,经常启动的时候报如下错误信息:
java.lang.RuntimeException: the type of the stack (IPv6) and the user supplied addresses (IPv4) don't match: /231.12.21.132.
解决的办法是增加 JVM 参数:-Djava.net.preferIPv4Stack=true。如果是 Tomcat 服务器,可在 catalina.bat 或者 catalina.sh 中增加如下环境变量即可:
    SET CATALINA_OPTS=-Djava.net.preferIPv4Stack=true

EhCache Server:

与前面介绍的两种集群方案不同的是, EhCache Server 是一个独立的缓存服务器,其内部使用 EhCache 做为缓存系统,可利用前面提到的两种方式进行内部集群。对外提供编程语言无关的基于 HTTP 的 RESTful 或者是 SOAP 的数据缓存操作接口。
下面是 EhCache Server 提供的对缓存数据进行操作的方法:

  OPTIONS /{cache}}

获取某个缓存的可用操作的信息。

  HEAD /{cache}/{element}

获取缓存中某个元素的 HTTP 头信息,例如:

   curl –head  http://localhost:8080/ehcache/rest/sampleCache2/2

EhCache Server 返回的信息如下:

HTTP/1.1 200 OK 

X-Powered-By: Servlet/2.5 

Server: GlassFish/v3 

Last-Modified: Sun, 27 Jul 2008 08:08:49 GMT 

ETag: "1217146129490"

Content-Type: text/plain; charset=iso-8859-1 

Content-Length: 157 

Date: Sun, 27 Jul 2008 08:17:09 GMT

   GET /{cache}/{element}

读取缓存中某个数据的值。

    PUT /{cache}/{element}

 写缓存。

   由于这些操作都是基于 HTTP 协议的,因此你可以在任何一种编程语言中使用它,例如 Perl、PHP 和 Ruby 等等。

下图是 EhCache Server 在应用中的架构:

     图 3. EhCache Server 应用架构图:

      

 

    EhCache Server 同时也提供强大的安全机制、监控功能。在数据存储方面,最大的 Ehcache 单实例在内存中可以缓存 20GB。最大的磁盘可以缓存 100GB。通过将节点整合在一起,这样缓存数据就可以跨越节点,以此获得更大的容量。将缓存 20GB 的 50 个节点整合在一起就是 1TB 了。

  

额外补充:

 ehcache.xml 属性说明:

    属性解释:
    必须属性:
        name:设置缓存的名称,用于标志缓存,惟一
        maxElementsInMemory:在内存中最大的对象数量
        maxElementsOnDisk:在DiskStore中的最大对象数量,如为0,则没有限制
        eternal:设置元素是否永久的,如果为永久,则timeout忽略
        overflowToDisk:是否当memory中的数量达到限制后,保存到Disk
    可选的属性:
        timeToIdleSeconds:设置元素过期前的空闲时间
        timeToLiveSeconds:设置元素过期前的活动时间
        diskPersistent:是否disk store在虚拟机启动时持久化。默认为false
        diskExpiryThreadIntervalSeconds:运行disk终结线程的时间,默认为120秒
        memoryStoreEvictionPolicy:缓存满了之后的淘汰算法
    缓存子元素:
      cacheEventListenerFactory:注册相应的的缓存监听类,用于处理缓存事件,如put,remove,update,和expire
      bootstrapCacheLoaderFactory:指定相应的BootstrapCacheLoader,用于在初始化缓存,以及自动设置。

 同步的属性说明:

   RMI缓存分布同步查找 class使用net.sf.ehcache.distribution.RMICacheReplicatorFactory 
   Jroups缓存同步查找 class使用 net.sf.ehcache.distribution.jgroups.JGroupsCacheReplicatorFactory
  这个工厂支持以下属性:
   replicatePuts=true | false – 当一个新元素增加到缓存中的时候是否要复制到其他的peers。默认是true。
   replicateUpdates=true | false – 当一个已经在缓存中存在的元素被覆盖时是否要进行复制。默认是true。
   replicateRemovals= true | false – 当元素移除的时候是否进行复制。默认是true。
   replicateAsynchronously=true | false – 复制方式是异步的指定为true时,还是同步的,指定为false时。默认是true。
   replicatePutsViaCopy=true | false – 当一个新增元素被拷贝到其他的cache中时是否进行复制指定为true时为复制,默认是true。
   replicateUpdatesViaCopy=true | false – 当一个元素被拷贝到其他的cache中时是否进行复制指定为true时为复制,默认是true。
   asynchronousReplicationIntervalMillis=1000


个人源码地址:  http://git.oschina.net/gz-tony/viemall-cached

参考资料:http://www.ibm.com/developerworks/cn/java/j-lo-ehcache/

个人CSDN文章地址: http://blog.csdn.net/tang06211015/article/details/52281551


发表评论