前言
大型互联网应用,例如门户网站、在线商城以及联机交易系统等等,往往需要处理大批量、高并发的用户访问请求,这对应用程序的性能提出了比较高的要求。性能问题一般可以在开发和部署两个阶段加以解决。在应用部署阶段,通过增加软硬件投入的方式比较常见,但其费用往往较高;而在软件开发阶段如果能够提前定位并解决性能瓶颈,则会减少大量成本。在开发阶段提升性能,一种常用的思路是降低瓶颈资源、业务模块的访问压力,因而在应用程序中加入缓存机制则因成本低、实现方便等优点成为常见解决方案。
缓存(Cache)在计算机科学领域指的是一些数据副本的集合。当原始数据访问速度较慢或代价过高时,可以通过使用在高速存储区域中保存原始数据的常用数据副本,从而提升访问速度。常见的硬盘缓存,CPU 缓存,网页缓存等等都是缓存概念的应用。在企业应用开发时,开发人员也可以基于缓存的概念,使用应用程序级别的缓存,从而来提升应用性能。
常见的开发方式为在 Java 虚拟机里通过 Static 成员变量、Context 对象或者用户 Session 中,保存常用的业务数据。一旦有访问需求,则先从缓存中尝试获取数据,如果尝试失败,再从实际模块获取数据并更新缓存。
此类方法实现简单,但容易遇到扩展性方面的问题:为了满足业务增长需求,用户可能需要在 多台 主机组成的 集群 中部署应用。此时 JVM 级的缓存会面临服务器间缓存同步的问题,处理不好,会导致数据不一致等严重错误。
其他可能的问题还有:
- 应用开发模式的改变:应用程序需要在相关业务处理逻辑处加入缓存相关处理。
- 配置管理:需要在缓存的有效期、大小等方面,如果要求可配置,则需要开发人员一定的工作量来满足此类需求。
针对这些常见问题,IBM WebSphere Application Server 则提供了一套动态缓存框架,能从不同方面加以解决。本文主要将讲述 WebSphere Application Server 的动态缓存机制,并给出具体例子描述如何配置和使用动态缓存来解决实际业务问题。
IBM WebSphere Application Server 动态缓存机制介绍
动态缓存机制是 WebSphere Application Server 为应用程序开发人员提供的一套扩展服务,其主要功能组件如下图所示:
图 1. WebSphere Application Server 动态缓存的主要功能组件
动态缓存机制架构如上图所示。动态缓存机制的核心功能为:在内存中缓存 Java 对象,应用程序可以通过 API 来访问这些对象。为了减少内存消耗,动态缓存服务也采用一些缓存替换机制(例如 LRU- 最近最少使用算法)来实现。对应不同的应用类型,动态缓存机制为应用开发人员提供了不同层面的缓存服务:展示层的 Portlet 和 Servlet 缓存服务和业务层的 WebService 缓存
服务、Java 命令缓存服务和对象缓存服务。
图 2. 动态缓存服务部署示意图
图 2 为多服务器环境下动态缓存机制的部署示意图。应用程序只需通过 API 来调用缓存服务即可,动态缓存服务会 自动 在不同服务器之间根据定义好的策略进行缓存数据同步。当收到应用程序缓存访问请求时,缓存服务会首先在本地缓存中查找相应对象提供服务。一旦被请求的对象位于其他服务器时,动态缓存服务会通过数据复制服务(Data Replication Service, DRS),来从其他服务上获取相应数据。如果对象在本服务器上有改动,如缓存更新或者缓存删除,动态缓存服务也会通过数据复制服务通知其他服务器。
以下为不同种类动态缓存服务之间的功能比较 :
缓存服务功能比较及使用
表 1. 动态缓存服务模块功能比较
服务名称 | 应用范围 | 优点 | 缺点 |
---|---|---|---|
Portlet 缓存 | 对 Portlet 的页面输出进行缓存 | 使用方便,仅需配置即可。 | 适用范围有限。 |
Servlet 缓存 | 对 Servlet/JSP 页面输出进行缓存,包括对 Struts 和 Tiles 标签的输出结果进行缓存。 | 使用方便,仅需配置即可。 | 适用范围有限。 |
Web Service 缓存 | 缓存 Web Service 的执行结果 | 使用方便,仅需配置即可。 | 适用范围有限。 |
命令缓存 (Command Cache) | 缓存某个 Java 类的方法的执行结果。 | 通过 API 访问,适用面广,配置简单。 | 需要修改编程模型,实现特殊的缓存接口,并需要一定的开发工作量。 |
对象缓存(Object Cache) | 缓存应用程序数据。 | 通过 API 访问,适用面广,使用方便。 | 需要一定的开发工作量。 |
如需使用 Portlet 和 Servlet 缓存,需要以下步骤
- 在 WebSphere Application Server 管理控制台中,启动动态缓存服务,并在 Web 或者 Portlet 容器配置界面中,启动相应缓存选项。
- 根据规范,完成缓存策略配置文件 cachespec.xml。该文件中一般包含缓存 id、要缓存的 Servlet url、缓存有效时间等等内容。
在缓存策略配置文件中,以下一些高级属性也可以被使用:
- 缓存输入参数:如果某个 Servlet 的输出是由输入参数决定的(例如;查询汇率的 Servlet 输出,是由源币种、目标币种和时间三个参数决定),因此在获取缓存结果时,需要考虑当前的输入参数值。这些输入参数可以配置在策略配置文件中。
- 缓存失效条件:在某些场景下,缓存需要立刻失效。例如返回产品列表的 Servlet,一旦用户在添加产品 Servlet 中作了某些操作,则需要缓存立刻失效。这些发出失效的动作,也可以配置在缓存配置文件中。
使用命令缓存,简单来讲有以下步骤:
定义命令接口,该接口一般需要扩展 WebSphere Application Server 中的 TargetableCommand 或者 CacheableCommand 接口。
定义命令实现,该实现除了实现第一步定义的接口外,还必须继承 CacheableCommandImpl。
完成缓存配置文件 cachespec.xml。
注意:WebSphere Application Server 中定义好的接口和类位于包 com.ibm.websphere.command
中,如果找不到这些类,可以从 WebSphere Application Server 安装目录下找到AppServer\plugins\com.ibm.ws.runtime.jar
,并加入到项目类路径中。
详情请参阅 WebSphere Application Server V7 的信息中心。
对象缓存服务则提供了一套类似 Map 接口的 API。应用程序使用该 API 来从缓存中读取或者写入数据即可,使用比较方便。
如需使用 Web Service 缓存,请参阅参考资料中的 文章 1 和 WebSphere Application Server V7 信息中心。
动态缓存应用场景分析
应用场景分析 1
需求:
某网上商店应用访问速度过慢,经过分析,是由于产品列表和购物车处理模块由于逻辑复杂,执行效率不高引起的。
解决方案:
在优化相应代码外,也可以利用动态缓存机制,缓存一些常用页面的输出。动态缓存机制能够自动根据配置缓存某些页面的输出结果,这样该页面在下次访问时,Web 容器可以直接返回页面输出结果,而不用再次执行相关业务逻辑。
需要注意的一点是,一些页面的输出内容跟当时的某些数据和环境信息相关,例如:http://www.shop.com/products.jsp?category=books
中,products.jsp 的输出就与 Category 参数决定。因此,在进行缓存定义时,需要将相应的参数作为缓存 key 定义的一部分。
除了请求参数外,WebSphere Application Server 动态缓存的 key 定义还支持将 request 对象的属性、相应页面路径、Session 值、Http Head 信息、Locale 和 Cookie。
应用场景分析 2
需求:
假设在集群环境下的企业应用中,存在一系列 XML 格式的元数据,数据量为 100 条左右。由于该数据经常被访问,项目组希望能够通过某种措施,减少数据访问和 XML 解析的开销,进而提高系统性能。而且一旦元数据被操作员修改(例如访问http://www.app.com/changeMetadata.jsp
),缓存内容需要立即得到更新。
解决方案:
在这种需求下,一种简单的做法是使用 JVM 级 static 对象来做缓存,具体做法为:定义一个 static HashMap 对象,应用程序读取数据时,首先检查该 HashMap 中是否存在缓存,如果存在,则直接从缓存中读取。在这种情况下,一些相应的缓存控制机制也必须被实现,比如缓存大小的控制、缓存过期等等。在数据被修改时,通过修改相应的代码(如在 changeMetadata.jsp)来更新缓存内容。
这种做法的问题在于,在集群环境中,操作员实际的访问请求是被某一个 JVM 上部署的应用所服务的。当操作员更新数据时,相应的更新代码,只会更新当前 JVM 上的缓存内容,而不会通知其他服务器,这样会导致数据不同步现象。而实现该数据同步,可能需要应用程序人员基于网络接口,开发相应的通知功能。
而在 WebSphere Application Server 中,动态缓存服务已经支持数据复制的缓存服务,我们只需要调用相应接口即可。而由于不同层面的应用程序模块,如前台用户界面和后台业务逻辑都需要访问该元数据,因此我们只能在动态缓存中的 Java 命令缓存和对象缓存中选择其一。鉴于应用中已经有类似的命令框架,因此无法简单的扩展该框架以使用 Java 命令缓存,因此对象缓存可以被用来解决此类问题。
基于对象缓存的应用开发
基于对象缓存(Object Cache)服务介绍
对象缓存服务提供了一套类似 Map 接口的 API,来从缓存中读取或者写入数据,使用比较方便。考虑到将来的扩展性,应用程序最好只基于 Map 接口进行编程。
对应用开发人员来讲,WebSphere Application Server 对象缓存提供了以下两个接口:
- DistributedMap:基于 java.util.Map 来读取和修改缓存数据,用于缓存 Java 对象。
- DistributedNioMap:高性能缓存,为 java.nio.Buffer 特别设计。一旦数据对象被从缓存中移出,相应的 DistributedNioMapObject.release() 接口还会被调用,可完成一些额外的应用逻辑。
基于常见的需求,缓存服务接口 DistributedMap 被使用最多。它提供的主要方法有:
表 2. DistributedMap 接口提供的主要方法
java.lang.Object put (java.lang.Object key, java.lang.Object value) | 向缓存中以键值 key 写入数据 value。如果相同键值的数据之前已存在,则返回旧值,否则返回 null。 |
---|---|
java.lang.Object put (java.lang.Object key, java.lang.Object value, int priority, int timeToLive, int inactivityTime, int sharingPolicy, java.lang.Object[] dependencyIds) | 向缓存中以键值 key 写入数据 value,并指定该缓存的优先级、存活时间、非活动时间(在该时间内,如果对象不被访问,则会被从缓存中清除)、共享策略、缓存依赖性等。 |
java.lang.Object put (java.lang.Object key, java.lang.Object value, int priority, int timeToLive, int sharingPolicy, java.lang.Object[] dependencyIds) | 向缓存中以键值 key 写入数据 value,并指定该缓存的优先级、存活时间、共享策略、缓存依赖性等。 |
boolean containsKey (java.lang.Object key, boolean includeDiskCache) | 判断缓存中是否有指定键值对应的对象。如果 includeDiskCache 为空,则也会检查磁盘中的缓存。 |
java.lang.Object get (java.lang.Object key) | 从动态缓存中获取指定的缓存对象。 注意:返回值为 null 不一定代表缓存中没有指定键值的对象,也可能该对象被显式的设置为 null。使用 containsKey 方法可以做出更确切的判断。 |
void invalidate (java.lang.Object key) | 使缓存对象无效。 |
int size (boolean includeDiskCache) | 返回缓存数据集合的大小。如果 includeDiskCache 为 true,则返回大小也会包含磁盘上的缓存对象。 |
如需要详细 API 描述,请在 WebSphere Application Server 信息中心 中搜索相应的类名。
应用程序通过调用缓存服务提供的 put 或者 get 方法即可从缓存服务中读取或者写入缓存数据。底层的实现细节,包括缓存过期处理,缓存在不同服务器其之间的同步等等,都会由 WebSphere Application Server 自动处理。其内部实现流程示意如下:
- 应用程序调用 DistributedMap.put 方法,向缓存写入数据。
- 动态缓存服务更新本地缓存
- 动态缓存服务检查共享策略,判断是否需要通知其他服务器数据更新
- 如果需要通知其他服务器,动态缓存服务会首先对数据进行序列化,并通过数据复制服务 (DRS) 发送到其他服务器。
- 应用程序调用 DistributedMap.get 方法,从缓存读取数据。
- 动态缓存服务检查本地缓存,如果有该数据,则返回该数据。
- 如果本地缓存没有该数据,动态缓存服务会检查共享策略,看数据是否位于其他服务器。
- 如果不在其他服务器上,则会返回 null 给应用程序。
- 如果数据在其他服务器上,则会通过数据复制服务 (DRS) 从其他数据中获取该数据,并通过序列化接口生成该对象返回给应用程序。
在不同服务器之间传输缓存对象时,会涉及到对象的序列化和反序列化操作,因此相应的 class 必须实现 java.io.Serializable 接口。另外,相应的类定义必须在所有的服务器之间都存在,否则会出现 ClassNotFoundException。
如果应用中使用了自定义的缓存 key(比如 put 对象时),则需要使该 key 对象能够被数据复制服务(DRS)所加载到。一种做法是将这些类打包成 jar 文件,作为共享库部署到应用服务器中。如需详细信息,请参阅共享库 说明,不过更为常见的做法是,只用 java 自带类型作为 key 值。
代码实例
从应用程序开发角度来看,程序员要使用缓存服务,仅需要以下几个步骤:
- 构造 InitialContext 对象
- 以 JNDI 查找的方式,从 InitialContext 对象中获取缓存实例对象
- 调用该缓存示例的相应接口。
以下是使用对象缓存服务的代码实例:
清单 1. 使用对象缓存服务
// 获得缓存服务对象 String cacheServiceJndi = "services/cache/distributedmap"; InitialContext ic = new InitialContext(); DistributedMap cacheServiceMap = (DistributedMap)ic.lookup(cacheServiceJndi); // 从缓存中读取数据 // 必须输入缓存 key Object cachedObject = cacheServiceMap.get(cacheKey); // 更新缓存 cacheServiceMap.put(cacheKey, newCachedObject);
如果一个应用服务器中安装了多个应用都需要使用对象缓存,为避免应用间的缓存冲突,可以采用缓存实例机制,每个应用都只访问自己的缓存实例即可。
缓存实例的创建方法有以下几种:
- 在 WebSphere Application Server 管理控制台中左边菜单中,选择资源 - 〉缓存实例 - 〉对象缓存实例,并创建新的缓存实例。
- 使用配置文件 cacheinstances.properties,示例如下:
cache.instance.0=/services/cache/instance_one cache.instance.0.cacheSize=1000 cache.instance.0.enableCacheReplication=true cache.instance.0.replicationDomain=DynaCacheCluster
- 使用资源引用方式,修改相应的 J2EE 配置文件。
- 在 Java 虚机进程定义中,加入自定义配置。
假设我们创建了 JNDI 为“/services/cache/instance_one”的缓存示例,为了使用该示例,我们的代码只需做以下修改:
清单 2. 基于缓存示例,获取和使用对象缓存服务
// 获得缓存服务对象 String cacheServiceJndi = "services/cache/ instance_one"; InitialContext ic = new InitialContext(); DistributedMap cacheServiceMap = (DistributedMap)ic.lookup(cacheServiceJndi); // 从缓存中读取数据 // 必须输入缓存 key Object cachedObject = cacheServiceMap.get(cacheKey); // 更新缓存 cacheServiceMap.put(cacheKey, newCachedObject);
无论在单机环境或者集群环境,以上代码都可以适用。为了避免出现 ClassNotFoundException,建议缓存的 key 采用 String 或者其他原始数据类型,否则您需要将自定义的对象以共享库方式部署到所有服务器中。
集群环境下的动态缓存应用部署
将开发完成的应用部署到集群环境,一般有以下几步:
- 集群环境的安装和部署
- 创建复制域
- 启动动态缓存服务,并指定复制策略
- 安装应用
注意: 只有在 WebSphere Application Server Network Deployment 版本中才支持服务器间的缓存复制。
服务器间缓存复制策略分析
缓存复制策略有以下几种:(EntryInfo.NOT_SHARED)
表 3. 缓存复制策略
not-shared 不共享(1) | 缓存内容不与集群中其他服务器共享。在这种模式下,缓存的数据内容可以不实现序列化接口 java.io.Serializable。 |
---|---|
shared-push 共享 - 推(2) | 缓存中的数据在多台服务器之间共享,且每台服务器均存放一个复本。一旦缓存内容发生变化,会自动同步到其他服务器中。这种模式下,缓存中数据必须实现序列化接口 java.io.Serializable。 |
shared-pull 共享 - 拉(3) | 缓存中的数据在多台服务器之间共享,任何一个被缓存对象在所有服务器中,存在且仅存在一份。当一个服务器读取缓存中数据时,会首先检查本地受否存在复本,如果不存在,则会尝试从其他服务器缓存中读取该数据。如果该对象在所有服务其中都不存在,则该服务器会创建该对象。这种模式下,缓存中数据必须实现序列化接口 java.io.Serializable。该模式已不建议使用。 |
shared-push-pull 共享 - 推拉(4) | 缓存中的数据在多台服务器之间共享,任何一个被缓存对象在被创建时,会保存在本地服务器的动态缓存中,并且将缓存 id 广播到其他服务器中。一旦某个对象被请求,当前服务器会根据本地缓存信息和广播信息立刻得知被缓存对象的位置,从而完成读取工作。这种模式下,缓存中数据必须实现序列化接口 java.io.Serializable。 |
如果应用中要缓存的数据量比较小,则可以采取 shared-push 方式,以空间换取性能。假如被缓存的数据对象比较大,或者服务器内存比较紧张,我们则可以考虑采用 shared-push-pull 模式。
应用部署步骤
完整的部署步骤如下所示:
集群环境的安装和部署
创建复制域
登陆 WebSphere Application Server 管理控制台,从左边菜单中选择“Resource->Replication Domains”。随后在右边点击“New”。按照下图所示,完成创建。
图 3. 创建复制域
启动动态缓存服务,并指定复制策略
在集群 所有 服务器中,完成以下操作:
进入服务器配置页面,点击“Dynamic cache service”,如下图所示:
图 4. 配置服务器的动态缓存服务(查看大图)
随后在配置界面中,根据业务需求,选择缓存大小。在页面下方,需要启用缓存复制功能,具体包括,选择第一步创建的复制域和选择缓存类型:缓存 - 推。基于业务需求,我们也可以以秒为单位,制定复制频率。
图 5. 设置动态缓存服务属性
注:如果使用的是单服务器环境,则上图所示的缓存服务配置中不会出现页面底部的缓存复制选项。
设置完毕,需要重启所有涉及到的服务器。
安装应用
按照正常应用安装方式即可,无特殊步骤。
经过以上几个简单步骤,我们就完成了基于对象缓存技术的应用部署步骤。我们也启动了缓存复制机制,使一台机器上的缓存改动,能够复制到所有其他机器上。
结束语
本文主要基于 WebSphere Application Server V7.0 介绍了高速缓存体系,并对典型缓存服务的优缺点作了分析比较。随后分析典型应用,并详细介绍了对象缓存接口及其应用。通过本文介绍,您已经了解到了动态缓存机制以及其使用范围。
本文包含的完整开发、部署步骤,也适用于其它 WebSphere Application Server 版本。由于篇幅关系,本文并未涉及到客户端缓存和基于 Edge Server 的外部缓存。如需相关信息,请参阅信息中心。
原地址: http://www.ibm.com/developerworks/cn/websphere/library/techarticles/0909_liuy_WASperformance/