一、 引言
内存缓冲在大规模企业应用软件开发过程中是一个关键的技术,其中往往都有可伸缩性和高性能的需求。一个内存缓冲能存储应用程序状态信息(如,一个WEB应用程序中的HttpSession)或数据库查询结果(也即,实体数据)。由于许多企业应用软件运行在一个簇环境下,所以缓存需要跨越簇进行复制。而且,如果需要更高的可靠性的话,内存缓冲也应该被持续性存储到硬盘或数据库中去。
大多数内存缓冲解决方案都属于我们所称的"普通"缓存系统类——其中存储和缓冲直接参考的对象。既然一个普通缓存直接处理参考对象,那么它就象一个详尽的HashMap结构一样,并因此使用起来非常直观。当一对象需要被复制或持续存储到一个普通缓存系统中时,对象必须实现Serializable接口。然而,普通缓存在复制或持续存储方面也存在一些明显的限制:
·用户必须具体地管理该缓存。例如,当一对象被更新时,用户需要执行一相应的API来更新缓存内容。
·Java对象串行化的需要可能会对性能有所妨碍。如果对象是巨大的,甚至单个的字段更新也将会激活整个对象的串行化与跨整个簇的复制。这样可能带来不必要的昂贵的代价。
·Java对象串行化不可能保存缓冲对象之间的关系。特别地,该缓冲对象不可能被其它对象参考多次(多参考),或到其自身有一个间接参考(循环)。否则,在串行化时该关系将被打破。例如,图1说明在复制期间的这个问题。如果我们有两个共享同一个Address对象的Person实例,那么在复制时它将被拆分成两个独立的Address实例(而不是一个)。
图1.普通缓存在串行化期间不保留对象关系
|
相对于上面普通缓冲系统中存在的问题,还有另外一种新型的缓冲系统——POJO(简单Java对象)缓存。一个POJO缓存是一个系统——它担当一个"面向对象的"分布式的缓存。在这个系统中,一旦一个用户把POJO依附到该缓存上,那么缓冲方面(例如复制和持续性)应该对用户是透明的。一个用户只需简单地在该POJO上操作而不须担心更新该缓存内容或维持对象关系的问题。不存在显式的API调用可用来管理该缓存。另外,它还有三个特征:
·不需要为POJO实现Serializable接口。
·复制(或甚至持续性)是基于字段级的(这与普通缓存中的基于整个对象的二进制级形成对照)-这将导致潜在的性能推进。
·对象关系和身份被自动地保存在一分布式复制环境中,这带来透明的使用体验并且提高了软件性能。
一个领先的内存POJO缓存解决方案就是开源JBoss缓存。JBoss缓存是第一个Java库——它支持可复制的,持续性的,事务性的和良好粒度的缓冲,它可以被使用作为POJO缓存和普通缓存。既然JBoss缓存是百分之百基于Java的,那么它就可以运行在任何Java SE环境中——包括应用程序服务器内部或作为独立的进程。例如,JBoss缓存已经被应用到针对EJB 3.0有状态的会话bean簇和HPP会话复制的应用程序服务器中。
在本文中,我们将说明怎样把JBoss缓存用作一个POJO缓存(通过它的JBossCacheAop组件)。同时,还将给出一个应用案例来说明在分布环境中的一些关键特性。
二、 JBoss缓存概述
(一) 普通缓存
JBoss缓存中默认的普通缓存模块称为TreeCache。你可以通过编程方式或通过一外部XML文件对它进行设置。下面是你可以设置的一些特性:
1. 缓存模式:它可以是本地的或者是可复制的。如果它是可复制的,那么你可以进一步指定同步的或异步的模式。
2. TransactionManager:你可以为JBoss缓存查询指定一个与JTA相匹配的事务管理器。如果它发现一个正在进行中的事务上下文,那么它就会参予到该事务中并且相应地执行提交或回滚。
3. 可插入的驱逐策略:该缓存驱逐策略参考该缓存使用的算法来终止它的内容。在JBoss缓存中,你可以经由一个可插入的接口来实现你自己的驱逐策略。JBoss缓存当前与一个基于地域化的LRUEvictionPolicy一起发行。
4. 隔离级别:JBoss缓存使用JDBC风格的语义来实现锁定行为。你不是专门地指定读/写锁,而是为了易于使用起见,可以指定不同的隔离级别。
5. 可插入的CacheLoader:CacheLoader允许你把持续性缓存内容装载回内存中。JBoss缓存当前支持文件装载器和基于SleepyCat和JDBC的装载器。
6. 溢出:通过与一个缓存装载器和一个驱逐策略相联合,它提供了可以在EJB中见到的钝化/激活特性。无论什么时候驱逐一项,它都将被钝化-这样它就会一直是持续的。
(二) POJO缓存
JBoss缓存中的POJO缓存模块称作TreeCacheAop。为了使用该POJO缓存,你必须在这些对象被缓冲以前"准备"这些对象(这个过程也称作对象运行时字节码重构)。系统在拦截该POJO操作时正需要这样做。对象运行时字节码重构过程由JBoss AOP库来实现。JBoss AOP允许你经由一个XML文件或注解来指定将被进行字节码重构重构的类。当前,我们仅仅支持JDK-1.4风格的注解(一个特定的JBoss AOP特征)。JDK 5.0注解支持将要在下一个版本中才能出现并且它将使得运行时字节码重构过程几乎是透明的!
TreeCacheAop是一个TreeCache的子类,因此它使用相同的XML文件进行配置并且提供与它的超类部分相同的缓冲功能。该JBoss POJO缓存还提供基于POJO的驱逐策略。
三、 应用案例分析
在本文的后面,我们将使用一个"传感器网络监控系统"示例来说明JBoss POJO缓存在提供即时的良好粒度的状态复制和自动保存状态对象关系方面的能力。
(一) 问题描述
沿着一条高速铁路有一些不同的站点,这里有几千个传感器需要被监视和监控。这些仪器的一些示例包括温度,风和雨传感器-它们对于高速铁路的正常运转至关重要。如果一个特别的传感器正在失灵,那么在监督的系统中的管理器计算机应该向超级用户警报并且关掉该单元并/或调度它-为维护之目的。
既然该操作是任务第一性的,那么就要求监督系统必须是高度可用的-这意味着无论何时在系统中的一台传感器管理器计算机出了问题,超级用户都应该能够无缝地切换到另一台机器以实现监视和积极的管理。因此,所有的管理器计算机必须实时地复制相同的传感器网络状态信息。注意,这种类型系统的特征-也就是,高可用性的要求和几千个(甚至更大数字)元素的存在-在现代网络管理中在其它一些情况下也是普通存在的。图2说明如此一个系统的概述-其中包含了簇能力。
图2.传感器监控系统概述
|
因为传感器网络的层次特性,典型情况下,在管理器方面一般都需要用一个复杂的域对象图来建模该传感器网络。当域对象状态没有被复制(或持续性存储时),对象关系的管理(例如,添加结点和遍历图添加)将由JVM本身来提供,并因此而对终端用户透明。然而,因为Java串行化过程不能够识别对象关系,所以当状态是复制或持续性时,这个看上去相当简单的对象-图关系将会垮掉。结果是,它导致管理器端组件的一个简单的持续性问题都难于实现。
传统地,为了提供完全的持续性能力,必须设计专门的系统来显式地管理对象关系,就象在一个现代对象关系映射(ORM)的解决方案一样。并且在一个传统型实体持续性层风格设计中,你必须做到:
· 外在化关系映射(例如,在一个XML文件中)。
· 把对象分解成一些原型。
· 建立对象身份的概念以保留关系。
· 追踪使用中的字段(如果你想要避免整个的对串行化的代价)。
· 最后,把原始类型装配成对象。
然而,通过使用JBoss POJO缓存来管理你的对象状态,根本不存在上面提及的问题!说得更详细些,好处如下:
· 即时持续性(通过状态复制)-不修改你的域对象模型或指定复杂的对象关系映射。
· 可选的状态持续性也能够保存对象图。
· 可以批量实现良好粒度的复制(或持续性)-为优化网络交通。
如我们在上面描述的,在POJO缓存中的复制和/或持续性方面对用户是完全透明的。注意,对整个簇来说,还存在另一个方面-装载平衡或定位主和/或辅管理器-客户端GUI(对超级用户来说)和传感器都应该连接到其上。在此我们不讨论这些问题,而将只集中于讨论跨管理器的传感器网络状态的复制问题。
(二) 拓扑与对象建模
图3是我们的传感器监控系统示例的拓扑结构。基本上,在这个简化了的实例中,我们有两个站(Tokyo和Yokohama),而且在每一个站中,我们将有一个Wind和一个Rain传感器。对于每一种传感器,存在许多组件需要监控-例如,电源供应和传感器单元本身。我们的目标是高效地监控传感器-这是通过1)监视单个项的状态(例如,一个StateItem)以及它们的整个的状态(例如,Wind Summary);2)能够在运行时刻调入和调出单个传感器而不用重启簇管理器结点。
图3.传感器监控系统的拓扑
|
图4展示了使用一个类图的这个域模型。实质上,我们在最顶层有一个PropagationManager-它管理整个传感器网络。它总是有一个根Node。该根Node可能递归地拥有众多的Node。每一个Node可以有多个StateItems。一个StateItem是最小的管理单元-例如,描述该单元的电源供给。
图4.传感器监控系统的类图
|
对于图3中的当前拓扑来说,Japan代表根结点,而Tokyo和Yokohama分别为站结点。不同的传感器(Rain或Wind)各用一个Node对象来描述。然后,每个传感器有两个StateItems,也就是Power Supply和Sensor Unit。
这些结点之间的关系是双向的;我们可以从根结点一直导航到叶结点,或者我们也可以经由父结点往回导航。另外,还有一个WindSensor Summary和一个RainSensor Summary StateItem实例-它们参考各自的传感器。Summary项的目的是监控整个的Wind和Rain传感器单元的健康状态。结果是,该传感器的对象图中的对象是多参考的(显示于图2和3中)。
(三) 配置
在我们可以使用POJO缓存功能之前,我们需要使用JBoss AOP工具来重构这些POJO(也就是,PropagationManager,Node和StateItem类)。为此,我们可以经由XML声明或注解来实现。下面我们将展示经由JBoss AOP JDK 1.4注解(JDK 5.0注解将会在下一个JBoss缓存发行版本中得到支持)对POJO进行字节码重构。下面是针对三个主要接口的声明注解的代码片断:
/*** @@org.jboss.cache.aop.InstanceOfAopMarker*/ public interface PropagationManager { public void setRootNode(String rdn); public void addNode(String parentFdn, String rdn); public void addStateItem(String parentFdn,long itemId, String name, long defaultState); public Node findNode(String fdn); public void stateChange(String fdn, long itemId,long newState); ... } /*** @@org.jboss.cache.aop.InstanceOfAopMarker*/ public interface Node { public void addChildNode(Node child); public List getChildren(); public void setParentNode(Node parent); public Node getParentNode(); public void addStateItem(StateItem stateItem); public void setSummaryStateItem(StateItem stateItem); public StateItem getSummaryStateItem(); public List getStateItems(); public StateItem findStateItem(long itemId); public void setPropagationRule(PropagationRule rule); ... } /*** @@org.jboss.cache.aop.InstanceOfAopMarker*/ public interface StateItem { public long getItemId(); public boolean setState(long state); public long getState(); public void setName(String name); public String getName(); ... } |
注意,在该JavaDoc中的注解-@@org.jboss.cache.aop.InstanceOfAopMarker是一个JBoss POJO缓存注解-它实质上声明这个接口的所有实例都将被进行字节码重构,因此不需要注解单个类。如果你想要注解一特定的类(而不涉及到它的子类),你也可以使用@@org.jboss.cache.aop.AopMarker注解。
在注解接口之后,我们使用一个JDK-1.4风格的JBoss AOP注解预编译器-annoc和一个AOP预编译器-aopc来执行编译时刻字节码重构。一旦完成这些步骤也就完成了字节码重构过程,然后我们就可以开始运行这个示例程序了。
(四) 代码片断
下面是代码片段-它实例化一个PropagationManager实例并设置站点和传感器结点之间的正确关系。最后,我们使用TreeCacheAop API putObject()来把POJO(在这种情况中,是PropagationManager实例)放入缓存管理之中。之后,任何POJO操作都将被良好地粒度地复制;例如,一个setState()操作将只复制相应的状态字段(它是一个整数)。
protected void setUp() throws Exception { cache1_ = createCache("TestCluster"); cache2_ = createCache("TestCluster"); initPm(); } protected void tearDown() throws Exception { cache1_.remove("/"); cache1_.stop(); cache2_.stop(); } private TreeCacheAop createCache(String name) throws Exception { //通过注入来配置缓存 PropertyConfigurator config = new PropertyConfigurator(); //读取replSync xml. //这里我们使用同步复制模式. config.configure(tree, "META-INF/replSync-service.xml"); //我们可以设置一个不同的簇组. tree.setClusterName(name); tree.start(); //开始缓存 return tree; } /***填充繁殖树*/ protected void initPm() throws Exception { pm_ = new PropagationManagerImpl(); pm_.setRootNode("Japan"); pm_.addNode("Japan", "Tokyo"); //东京站 // Wind传感器设备 pm_.addNode("Japan.Tokyo", "WindSensor1"); pm_.addStateItem("Japan.Tokyo.WindSensor1", 1000,"power supply", 1040); //电源供应 pm_.addStateItem("Japan.Tokyo.WindSensor1", 1001, "sensor unit", 1040); //传感器单位 //Rain传感器设备 pm_.addNode("Japan.Tokyo", "RainSensor1"); pm_.addStateItem("Japan.Tokyo.RainSensor1", 1002,"power supply", 1040); //电源供应 pm_.addStateItem("Japan.Tokyo.RainSensor1", 1003,"sensor unit", 1040); //传感器单位 pm_.addNode("Japan", "Yokohama"); // 横滨站 //Wind传感器设备 pm_.addNode("Japan.Yokohama", "WindSensor2"); pm_.addStateItem("Japan.Yokohama.WindSensor2", 1000,"power supply", 1040); //电源供应 pm_.addStateItem("Japan.Yokohama.WindSensor2", 1001,"sensor unit", 1040); //传感器单位 // Rain传感器设备 pm_.addNode("Japan.Yokohama", "RainSensor2"); pm_.addStateItem("Japan.Yokohama.RainSensor2", 1002,"power supply", 1040); //电源供应 pm_.addStateItem("Japan.Yokohama.RainSensor2", 1003, "sensor unit", 1040);//传感器单位 //网络中的Wind传感器的摘要结点 pm_.createNode("WindSummary", "WindSummary"); pm_.setUpperNode("WindSummary","Japan.Tokyo.WindSensor1"); //关联 pm_.setUpperNode("WindSummary","Japan.Yokohama.WindSensor2"); //关联 //网络中的Rain传感器的摘要结点 pm_.createNode("RainSummary", "RainSummary"); pm_.setUpperNode("RainSummary", "Japan.Tokyo.RainSensor1"); //关联 pm_.setUpperNode("RainSummary","Japan.Yokohama.RainSensor2"); //关联 } /**主启动点,由main来调用*/ public void testPropagation() throws Exception { //在此我们让POJO缓存来管理pm cache1_.putObject("/monitor", pm_); //输出 printStatus("Initial state", pm_); // 从Manager #2检索POJO PropagationManager pm2 = (PropagationManager) cache2_.getObject("monitor"); System.out.println( "---------------------------------------------"); System.out.println("Modified on Manager #1"); //该项中的一个状态被修改. //这将是良好粒度的复制 pm_.stateChange("Japan.Tokyo.RainSensor1", 1003, 1041); printStatus("Japan.Tokyo.RainSensor1: id: 1003 state: 1040->1041 (retrieved from Manager #2!)", pm2); System.out.println( "---------------------------------------------"); System.out.println("Modified on Manager #2"); //该项中的一个状态被修改. //这将是良好粒度的复制. pm2.stateChange("Japan.Yokohama.WindSensor2", 1001, 1041); //在缓存#2上修改状态 printStatus("Japan.Yokohama.WindSensor2: id: 1001 state: 1040->1041 (retrieved from Manager #1!)", pm1); System.out.println( "---------------------------------------------"); System.out.println( "Add a new VibrationSensor on Tokyo station"); // Vibration传感器设备 pm_.addNode("Japan.Tokyo", "VibrationSensor1"); pm_.addStateItem("Japan.Tokyo.VibrationSensor1", 1004,"power supply", 1040); // 电源供应 pm_.addStateItem("Japan.Tokyo.VibrationSensor1", 1005,"sensor unit", 1040); //传感器单位 printStatus("Japan.Tokyo.VibrationSensor1: (retrieved from cache #2)", pm2); } public static void main(String[] args) throws Exception { PropagationReplAopTest pmTest =new PropagationReplAopTest(); pmTest.setUp(); pmTest.testPropagation(); pmTest.tearDown(); } |
在上面的实例片断中,请注意下列几点:
1.两个缓存实例被通过XML注入配置成功并且在随后启动。它们可以运行在两个独立的JVM中(并且在不同的机器上)。
2.一个PropagationManager的实例经由一个putObject() API调用被放入到缓存管理中。此后,只需要单纯的POJO操作。例如,我们可以把额外的结点添加到PropagationManager实例上并且它将会相应地进行复制。
3.经由一个getObject()调用,我们在缓存#2上检索PropagationManager的一个新实例。
4. 各种setState()调用只会激活到其它的缓存实例的良好粒度的复制。
(五) 结果输出
最后,当运行这个实例时,结果的输出如下所示:
Initial state --------------------------------------------- Japan (Summary : 2000 [ok]) + Tokyo (Summary : 2000 [ok]) + + WindSensor1 (Summary : 2000 [ok]) + + | ( name = power supply, id = 1000, state =1040) + + | ( name = sensor unit, id = 1001, state =1040) + + RainSensor1 (Summary : 2000 [ok]) + + | ( name = power supply, id = 1002, state =1040) + + | ( name = sensor unit, id = 1003, state =1040) + Yokohama (Summary : 2000 [ok]) + + WindSensor2 (Summary : 2000 [ok]) + + | ( name = power supply, id = 1000, state =1040) + + | ( name = sensor unit, id = 1001, state =1040) + + RainSensor2 (Summary : 2000 [ok]) + + | ( name = power supply, id = 1002, state =1040) + + | ( name = sensor unit, id = 1003, state =1040) --------------------------------------------- Modified on Manager #1 StateItem.setState(): id: 1003 state changed \ from 1040 to 1041 ……(篇幅所限省略) |
请注意其中加粗的几行。基本上,我们首先在管理器#1上进行了一个POJO操作setState,然后打印输出propagation(繁殖)树来验证状态被更新了,并且反过来也如此。如此重复是值得的,尽管没有显示在复制层数据流的输出结果中,但是每一个setState()操作也将会只激活具有良好粒度的字段级复制。另外,如果该调用是在一个事务上下文中,那么更新将会被批量地进行;也就是说,只要准备好提交时才进行复制。最后,还要注意,我们已经能够轻松地把一个新的传感器添加到网络中。一个传统型的系统一般都要求某种类型的重启机制。
四、 总结
在本文中,我们分析了通过平衡TreeCacheAop组件来实现用JBoss缓存担任POJO缓存的能力。通过使用POJO缓存功能,它能够为POJO提供无缝的持续性存储的能力-在保留对象-图关系的同时又能实现良好粒度的复制。