集群间部署 Ehcache 实战

本文参考网址:

《Spring+EhCache缓存实例》
《集群环境中使用 EhCache 缓存系统》
《EhCache 系统简介》
《ehcache 集群使用 rmi方式》
《ehcache缓存配置说明》


一. Ehcache 的简介

EhCache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider。Ehcache 是一种广泛使用的开源 Java 分布式缓存。主要面向通用缓存,Java EE 和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个 gzip 缓存 servlet 过滤器,支持 REST 和 SOAP api 等特点。

优点

  • 快速
  • 简单
  • 多种缓存策略
  • 缓存数据有两级:内存和磁盘,因此无需担心容量问题
  • 缓存数据会在虚拟机重启的过程中写入磁盘
  • 可以通过 RMI、可插入 API 等方式进行分布式缓存
  • 具有缓存和缓存管理器的侦听接口
  • 支持多缓存管理器实例,以及一个实例的多个缓存区域
  • 提供 Hibernate 的缓存实现

缺点

  • 使用磁盘 Cache 的时候非常占用磁盘空间:这是因为 DiskCache 的算法简单,该算法简单也导致 Cache 的效率非常高。它只是对元素直接追加存储。因此搜索元素的时候非常的快。如果使用 DiskCache 的,在很频繁的应用中,很快磁盘会满。
  • 不能保证数据的安全:当突然 kill 掉 Java 的时候,可能会产生冲突,EhCache 的解决方法是如果文件冲突了,则重建 cache。这对于 Cache 数据需要保存的时候可能不利。当然,Cache 只是简单的加速,而不能保证数据的安全。如果想保证数据的存储安全,可以使用 Bekeley DB Java Edition 版本。这是个嵌入式数据库。可以确保存储安全和空间的利用率。

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

EhCache 从 1.7 版本开始,支持五种集群方案,分别是:

  • RMI
  • Terracotta
  • JMS
  • JGroups
  • EhCache Server

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

RMI 集群部署方式

在 RMI 集群模式中,集群中每个节点都是平等关系,并不存在主节点或从节点的概念。因此节点间必须有一个机制能够互相认识对方,必须知道其它节点的信息,包括主机地址、端口号等。

在 EhCache 中提供了两种节点的发现方式:手动发现自动发现。手动发现配置的方式需要在每个节点中配置其他所有节点的 IP, 端口等连接信息,如果集群中某节点发生变化,则需要对缓存重新配置。由于笔者工程的集群环境 IP 不是稳定不变的,而且为了追求更高的动态性,所以笔者主要对自动发现方式进行学习和总结。

二. EhCache 自动发现工程的构建

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

笔者将以自己的工程环境进行略微修改,然后进行说明。
笔者想搭建一个简单的 EhCache 项目:在 IP 为 192.168.22.2(称为 LSL 的主机)与 IP 为 192.168.22.3(称为 GRQ 的主机)之间建立 EhCache 缓存,使得在两主机之间可以将元素 put 到缓存中,也可以通过 get 方法将缓存中的元素取出来(包括自己 put 进缓存的,也包括其他主机 put 进入的)。

2.1 Maven 配置

EhCache 依赖的 jar 包不多,仅仅依赖于一个 junit。Maven 配置方法很简繁,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.grq.mySpringMvc</groupId>
    <artifactId>mySpringMvc</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>2.8.3</version>
        </dependency>
    </dependencies>
</project>

2.2 EhCache 配置文件编写

EhCache 项目的使用,核心在于 EhCache 的 xml 配置文件的编写。如果使用手动配置,两个主机的 xml 配置文件会略有不同,但差别不大。但如果设置为自动发现方式,则用完全相同的设置即可。
以 GRQ 主机的 xml 配置文件 ehcache-cluster.xml 文件为例,对 ehcache 配置文件内容逐个部分进行讲解。

<?xml version="1.0" encoding="utf-8"?>
<ehcache>
    <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=40001,socketTimeoutMillis=2000" />

    <!--demo 缓存-->
    <cache name="auto_cache" maxElementsInMemory="1000" eternal="false"
           timeToIdleSeconds="500" timeToLiveSeconds="500" overflowToDisk="true"
           diskSpoolBufferSizeMB="30" maxElementsOnDisk="1000000"
           diskPersistent="false" diskExpiryThreadIntervalSeconds="120"
           memoryStoreEvictionPolicy="LRU">
        <cacheEventListenerFactory
                class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
                properties="replicateAsynchronously=true, replicatePuts=true, replicateUpdates=true,
                            replicateUpdatesViaCopy=true, replicateRemovals=true,asynchronousReplicationIntervalMillis=1000" />
        <!--用于初始化缓存,以及自动设置-->
        <bootstrapCacheLoaderFactory
                class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"/>
    </cache>
</ehcache>

2.2.1 cacheManagerPeerProviderFactory

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

cacheManagerPeerProviderFactory 属性如下:

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

2.2.2 cacheManagerPeerListenerFactory

cacheManagerPeerListenerFactory 是用来监听从集群发送过来的信息,有 class 与 properties 两个属性。配置 CacheManagerPeerListener 需要指定一个 CacheManagerPeerListenerFactory,它以插件的机制实现。
Ehcache 有一个内置的基于 RMI 的分布系统。它的监听器是 RMICacheManagerPeerListener,这个监听器可以用 RMICacheManagerPeerListenerFactory 来配置。配置如下:

<cacheManagerPeerListenerFactory
    class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
    properties="port=40001,socketTimeoutMillis=2000" />

属性内容如下:

  • hostname: 运行监听器的服务器名称。该项是可选项,标明了做为集群群组的成员的地址,同时也是你想要控制的从集群中接收消息的接口。
    • 在 CacheManager 初始化的时候,会检查 hostname 是否可用;
    • 如果 hostName 不可用,CacheManager 将拒绝启动并抛出一个连接被拒绝的异常。
    • 如果没有指定 hostName,hostName 将用 InetAddress.getLocalHost().getHostAddress() 来得到。
  • port: 监听器监听的端口。
  • socketTimeoutMillis: Socket 超时的时间,默认值是 2000ms。
    • 注:当 socket 同步缓存请求地址比较远,即不是本地局域网时,可能需要把这个时间配置大些,不然很可能延时导致同步缓存失败。

2.2.3 cache

<cache name="auto_cache" maxElementsInMemory="1000" eternal="false"
    timeToIdleSeconds="500" timeToLiveSeconds="500" overflowToDisk="true"
    diskSpoolBufferSizeMB="30" maxElementsOnDisk="1000000"
    diskPersistent="false" diskExpiryThreadIntervalSeconds="120"
    memoryStoreEvictionPolicy="LRU">
    ....
</cache>

必填属性如下:

  • name: 缓存名称;
  • maxElementsInMemory: 缓存最大个数;
  • eternal: 对象是否永久有效。一旦设置了,timeout 将不起作用;
  • overflowToDisk:当内存中对象数量达 maxElementsInMemory 时,Ehcache 将会对象写到磁盘中;
  • diskSpoolBufferSizeMB:这个参数设置 DiskStore 即磁盘缓存的缓存区大小。默认是 30 MB。每个 Cache 都应该有自己的一个缓冲区;
  • maxElementsOnDisk:硬盘最大缓存个数;

可选的属性如下:

  • timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。
    • 仅当 eternal=false 时,对象才是有时效性的,否则缓存永久存储;
    • 默认值是 0,也就是可闲置时间无穷大;
  • timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒);
    • 仅当 eternal=false 时,对象才是有时效性的;
    • 默认值是 0,也就是对象存活时间无穷大;
    • 最大时间介于创建时间和失效时间之间;
  • diskPersistent:是否 disk store 在虚拟机启动时持久化;
    • 默认值是 false;
  • memoryStoreEvictionPolicy:当达到 maxElementsInMemory 限制时,Ehcache 将会根据指定的策略去清理内存。
    • LRU: 最近最少使用,默认值;
    • FIFO: 先进先出;
    • LFU: 较少使用;
  • diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是 120 秒。
  • clearOnFlush:内存数量最大时是否清除。
  • bootstrapCacheLoaderFactory: 子选项,指定相应的 BootstrapCacheLoader,用于在初始化缓存,以及自动设置。

2.2.4 cacheEventListenerFactory

在 cache 标签中,还有一个子标签 cacheEventListenerFactory。它用来注册相应的的缓存监听类,用于处理缓存事件,如 put, remove, update, expire。配置文件中如下所示:

<cache ...>
    <cacheEventListenerFactory
        class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
        properties="replicateAsynchronously=true, replicatePuts=true, replicateUpdates=true,
        replicateUpdatesViaCopy=true, replicateRemovals=true,asynchronousReplicationIntervalMillis=1000" />
</cache>

具体元素如下所示:

cacheEventListenerFactory 用于注册相应的的缓存监听类,这些类用于处理缓存事件。

  • replicateAsynchronously: 复制模式的同步/异步,值取 true / false
    • true: 异步复制操作
    • false: 同步复制操作
    • 默认值: true
  • replicatePuts: 当新对象被放入缓存,集群内其他缓存也放入。值为 true / false,默认为 true;
  • replicateUpdates: 对于具有相同 key 对象,新对象是否覆盖这个具有相同 key 的对象。值为 true / false,默认值为 true
  • replicateUpdatesViaCopy: 是否直接将更新后的对象复制到集群中的其他缓存;
    • true: 将更新后的对象复制到集群中其他缓存;
    • false: 不复制对象,只向集群中的其他缓存发布一个对象更新的消息;
    • 默认值: true
    • <font color=red>注:笔者在参考其他网站的设置时,有些教程是将该项设置为 false 的。对于自动发现方式的集群部署,应该设置为 true,这样才能令不同机器之间的缓存同步复制删除更新。</font>
    • 笔者设置该项为 false 时,会出现意外情况:第一次在集群中任意一台机器上 put 一个数据对 (key, value),缓存中正常;第二次在集群中的一台机器 put 一个拥有相同 key 值,不同 value 值的数据对 (key, newValue),集群中其他机器的缓存中的 (key, value) 数据对也消失了。后来将该项设置为 false,该现象得以解决。
  • replicateRemovals: 集群中在某机器上将对象移除后,是否复制状态到集群中其他缓存;默认值为 true.

三. EhCache 集群间测试程序

笔者在两台机器上部署同样的工程,工程中的 ehcache 配置文件在上面已经完整给出,测试程序如下:

package com.grq.xStreamTest;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;

import java.net.URL;
import java.util.Scanner;

public class ehcacheTest {
    public static void main(String[] args) throws InterruptedException {
        URL url = ehcacheTest.class.getClassLoader().getResource("ehcache-cluster-auto.xml");
        CacheManager manager = new CacheManager(url);

        Cache cache = manager.getCache("auto_cache");
        // 主机 1 的标识: GRQServer
        Element element = new Element("GRQServer-" + System.currentTimeMillis(), "GRQServer");
        // 主机 2 的标识: LSLServer
//        Element element = new Element("LSLServer-" + System.currentTimeMillis(), "LSLServer");
        cache.put(element);

        // 键盘输入流
        Scanner in = new Scanner(System.in);
        int i = 0;

        while (true) {
            String inputKey = null;
            String inputValue = null;
            Element inputElement = null;

            // 输入方法
            System.out.println();
            System.out.println("Method: ");
            String method = in.nextLine();
            switch (method) {
                case "query":
                    break;
                case "add":
                case "update":
                    System.out.print("Key: ");
                    inputKey = in.nextLine();
                    System.out.print("Value: ");
                    inputValue = in.nextLine();

                    Element outputElement = new Element(inputKey, inputValue);
                    cache.put(outputElement);
                    break;
                case "delete":
                    System.out.print("Key: ");
                    inputKey = in.nextLine();
                    cache.remove(inputKey);
                    break;
                default:
                    System.out.println("ERROR: Can't Recognize this Method \"" + method + "\"");
                    break;
            }

            // 输出缓存中所有 (Key, Value) 值
            for(Object key : cache.getKeys()) {
                Element obj = cache.get(key);
                if(obj == null) {
                    continue;
                }
                Object value = obj.getObjectValue();
                if(value == null) {
                    continue;
                }
                System.out.println(key + " : " + value);
            }
        }
    }
}

两台主机上同时运行程序,通过试验,发现调用不同的 EhCache 方法可以实现集群之间缓存的增删改查操作。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • 原文连接:https://my.oschina.net/coolfire368/blog/123377 ehcac...
    晴天哥_王志阅读 1,334评论 0 1
  • feisky云计算、虚拟化与Linux技术笔记posts - 1014, comments - 298, trac...
    不排版阅读 3,815评论 0 5
  • Ehcache是现在最流行的纯Java开源缓存框架,配置简单、结构清晰、功能强大,最初知道它,是从Hibernat...
    安易学车阅读 2,026评论 0 11
  • 理论总结 它要解决什么样的问题? 数据的访问、存取、计算太慢、太不稳定、太消耗资源,同时,这样的操作存在重复性。因...
    jiangmo阅读 2,838评论 0 11
  • 还记得那条广告么? 这世上最大的谎言就是“你不行”! 还记得那几个人么? 曾经的他,唱歌比赛惨遭淘汰,众评委觉得“...
    宋杉杉阅读 433评论 0 1