框架的主要原理
缓存属性
我们将所有的缓存参数配置在名为cache.ccf的属性文件中。这些参数包括缓存信息如:内存中存储的对象的最大数量,缓存时间(过了时间之后缓存的数据九自动从内存中释放),中断时间(elapsed time since last access time), 内存缓存名称(例如:缓存算法如LRU或MRU)等。在当前版本的JCS中,缓存属性文件是纯文本格式的。SpiritCache framework,一种来自SpiritSoft的Jcache API商业实现,支持XML格式的缓存配置。
确认该属性文件存放在类路径中。注意:如果你需要使用其它不同的文件来存放缓存属性的话,JCS 也提供了方法来指定一个配置文件的名称。请参考JCS的 Javadocs 学习如何从其它非缺省的属性文件中读取缓存配置信息。
下面列出来的是web应用使用缓存功能需要了解的一些Java类。这些类存放在本文的示例代码的common.caching包中。这些类的Javadocs也包括在源代码压缩包中。 (图2 中的类图显示了这些Java类的关系)
ICacheManager
这是客户应用实现所有缓存有关操作(如:存储、访问以及释放缓存中的数据)的主接口(契约)。客户程序可以是JSP、Struts Action类,或者就是一个POJO对象。创建该接口用于对客户端隐藏所有缓存的实现细节,这样当我们将来需要切换另一种的第三方缓存API的时候无需对客户端代码做任何调整。
BaseCacheManager
这是web门户缓存框架的主类。是对ICacheManager 接口的最基本实现。创建BaseCacheManager 用于在一个类中集中所有缓存相关的方法。它被设计为单例模式保证在servlet容器的JVM中有且仅有一个ICacheManager 的实例被创建。在多web服务器/servlet容器实例共同处理web请求的集群环境中,每个JVM将会创建独立的ICacheManager 实例。如果将来你要转换到不同的缓存API ,这是唯一需要为新的缓存API修改的类。如果你切换到JCache-兼容的缓存实现,对缓存管理器的修改将会是很小的。
ICacheLoader
该接口用于在web客户端实现真正的数据访问逻辑。所有需要使用缓存机制的客户端应用必须实现该接口。它包括仅有的一个方法叫做loadCacheObject() ,有两个输入参数:一个String参数制定缓存区域名称,一个对象参数制定缓存键值。这样,缓存管理器将知道在缓存的对象超过指定的“生存时间”的时候,使用哪个客户端程序(运行loadCacheObject 方法)来重载缓存中的对象 。
ICacheKey
ICacheKey 接口创建的目的是为了隐藏特定的创建缓存键值的细节。有时候缓存的键值不是一个简单的字符串。它可能像多个对象组合起来一样复杂,从数据源获取这些值需要多个查找方法而不是单一的方法。在这种情况下, ICacheKey 接口可以被用来定义创建缓存键值的所有复杂的逻辑。这样,缓存键值创建逻辑将会被定义为独立的类。我编写了一个简单的类TestCacheKey 实现了该接口并实现了getCacheKey() 方法来演示使用该接口的方法。
CacheRegions
一个 缓存区域 被定义为一个组织起来的命名空间用于容纳一组缓存对象集合。你需要在配置文件中定义缓存区域来实现在一块单独的内存空间中存储数据,管理缓存数据的有效期限。如果需要的话,对有相似特征的对象(例如:生存时间和业务用途)应该被缓存在相同的缓存区域中,让它们可以在相同的时间失效。我定义了分离的缓存区域来存储静态数据和小变动数据。为了避免同步操作带来的效率影响,我对每个缓存区域使用了单独的Cache (JCS) 实例。
CacheElementInfo
该类用于封装所有的缓存统计信息(例如:命中数、不中数、命中比例等),用来监测在web应用中所有缓存对象的效率。
编译, 构建, 和单元测试
我用Ant创建了一个构建脚本来编译我的对象缓存框架的所有代码。Ant的构建脚本build.xml, 放在WEB-INF\classes 目录下。我还编写了Junit测试客户端来测试使用web门户缓存框架的不同缓存场景。测试脚本CachingTestCase, 放在WEB-INF\classes\common\caching\test 目录下。解压缩示例代码到一个新的web应用目录,如果要验证Junit测试脚本,从命令行运行以下命令:
切换当前目录到%TOMCAT_HOME%/webapps/web-app-name/WEB-INF/classes (在Unix测试环境中,目录应该是$TOMCAT_HOME/webapps/web-app-name/WEB-INF/classes)。
运行以下命令:
ant common.compile
编译缓存框架中所有的Java类。
ant common.runjunit
用于运行Junit测试脚本。测试脚本使用Log4J API来显示所有的输出信息。
考虑何时使用对象缓存的指导方针
当你决定要在你的web应用中缓存一些特定类别的数据的时候,请参照这些指导方针。缓存的应用应该经过谨慎地考虑,只有当其它方法,如:数据访问等,已经无法再进一步改进的时候才需要使用缓存。缓存将会带来复杂性,让维护工作变得更加复杂。因此,必须统筹考虑性能和缓存带来的复杂性的平衡关系。
当考虑使用缓存的时候,需要考虑对象的预定执行时间和刷新率或者叫做对象的生存时间。缓存不能容纳所有我们想要存储的数据,因此缓存使用的内存及时得到释放,即可以通过定义合理的生存时间实现,也可以在数据不再需要的时候显式地释放被缓存的对象。可以指定缓存算法如最近被访问算法(LRU)或者最少被使用算法(LFU)以便缓存基于访问频率来释放对象。Jack Shirazi的著作 Java 性能调整 提供了一个关于缓存主题的非常有趣的讨论,讨论了什么类型的数据应该被缓存,以及何时使用缓存的建议。
注意缓存框架并没有处理在web应用中需要被缓存的对象的创建(例如:从数据源检索数据的数据访问逻辑并没有在缓存类中编写)。这要依赖于客户程序来定义真正的数据访问逻辑。像Java数据对象等技术通常用于在企业级web应用中封装数据访问逻辑。参考O'Reilly的 Java 数据对象 来学习更多的关于如何将数据访问层与业务逻辑层分离的知识。
结论
本文提供了对使用Jakarta的Java缓存系统(JCS)来为web门户应用开发对象缓存框架的概要介绍。该框架非常稳定并可以被重用于其它任何web应用,甚至可以用于客户/服务器模式的Java应用程序。本文详细介绍了web门户缓存框架的工作原理,并提供了Junit测试脚本来对不同场景的缓存框架进行测试。
JCS was built as a system close to JCACHE Java Temporary Caching API (JSR-107), a description of the caching system used in Oracle 9i and other popular caching frameworks. 该规范可能会在将来的JDK发行版本中作为一种Java扩展框架。我的其中一个目的就是让web门户缓存框架与JCS保持松散耦合。这样的话,如果我将来需要转换到另一种框架(例如Jcache),我可以在对web门户应用客户程序代码不做大的调整的情况下完成切换。
我现在通过记录缓存监测信息的日志(使用Log4J API)如:命中数、不中数、命中率来衡量缓存的效率。可能将来有其它参数需要被监测来衡量缓存的效率。同样的,用来测量使用或不使用缓存对数据访问的反馈时间,应该使用一些负载测试工具如:Grinder或者Jmeter来测试其伸缩性和性能效果。
在机群环境下保持缓存同步将是一个挑战,因为每个servlet容器将会在自己的JVM中拥有一个缓存管理器实例。解决该问题的方法就是创建消息驱动Bean(MDB)在需要刷新数据的时候通知所有的缓存管理器。
通常的对象查找方法,如:简单的Hashtable、JNDI甚至是EJB,提供了在内存中存放对象并通过键值查找对象的方法。但是任何一种方法都没有提供当对象不需要的时候从内存中移出的机制,或者当访问对象迟于对象存放期限的时候自动创建对象的机制。HttpSession 对象 (in the servlet package) 也允许对象被缓存,但是它没有共享、失效、单一对象存放期满、自动装载或者spooling 这些缓存框架需要具备的基础机制。
虽然将缓存功能集成到web应用中需要额外的设计和开发工作,但我认为缓存带来的利益大于额外付出的工作。我已经看到在我实现了缓存框架之后 ,我的web应用的性能有很大的提高,特别是在访问静态数据和查找结果方面。该web应用模块目前处于测试阶段。在不久的将来,我会提供一些性能方面的测试数据(包括使用与不使用缓存的情况)来比较缓存如何帮助设计更快、更具伸缩性的web应用。
示例代码
参考资源
Java 性能调整, Jack Shirazi, O'Reilly
Object Caching Service for Java
Srini Penchikala 是一名软件咨询顾问,当前为Computer Consultants of America, Inc. 工作。