在多层应用程序中使用缓存: 最佳实践
 

2009-02-18 作者:Andrei Cioroianu 来源:oracle.com

 

了解缓存技术的战略性应用能如何提高多层应用程序的性能以及如何在集群环境中使多个缓存同步。

本文相关下载:
示例代码
Oracle TopLink
Oracle 应用服务器 10g
Oracle BPEL 流程管理器

多层体系结构有助于使复杂的企业应用程序具备可管理性和可伸缩性。 但随着服务器数和层数的增加,它们之间的通信量也随之增加,进而降低了应用程序的总体性能。

在多层模型的战略性位置使用缓存技术可以帮助减少通信量。 此外,尽管缓存信息库需要内存和 CPU 资源,但由于使用缓存可以减少开销较高的操作(如数据库访问和网页执行),因此可以提高总体性能。 然而,确保缓存保留新内容并使陈旧数据失效是一个挑战,而在集群环境中使多个缓存同步则更加困难。

本文分析了多层应用程序中使用的缓存类型,并介绍了解决缓存相关问题(如“陈旧”数据)的方法。 此外,本文还介绍了 Oracle 提供的缓存框架,即 Oracle Web 缓存、Web 对象缓存、Java 对象缓存和 Oracle TopLink。 您还将了解“no-cache”HTTP 头部、动态内容缓存、数据版本控制和乐观锁定。

缓存框架

对于独立应用程序而言,使用 Java 集合框架(为常用对象创建信息库)实施您自己的缓存机制非常容易。 例如,只要对象数目相对较小且未占用太多内存,Java Map 数据结构( java.util 程序包中提供)就可以很好的胜任工作。

然而,为持久性对象或 Web 内容(尤其是为大型分布式应用程序)创建缓存的难度要远远超过为自包含的小型应用程序创建缓存,其原因是您必须:

  • 限制缓存的大小,因为您无法将整个数据库或动态内容的每个部分都保留在内存中
  • 根据需要更新缓存对象并从缓存中删除陈旧内容
  • 同步分布在不同服务器的多个缓存(例如,对于在集群中部署的应用程序)。

缓存框架可以解决这些问题以及许多其他问题。 下面,我们将在典型的基于 Web 的应用程序体系结构(图 1)环境中简单了解一下 Oracle 提供的几个缓存框架。

Oracle 缓存框架概述

Oracle Web 缓存、Web 对象缓存、Java 对象缓存和 Oracle TopLink 不是相互排斥而是相互补充的,它们在企业应用程序的不同层中使用,如图 1 所示。

Web 浏览器通过 Oracle Web 缓存(可能通过代理缓存)连接到 Web 服务器,后者从 JSP 容器中获取它的动态内容。 Servlet 和 JSP 使用 Web 对象缓存,而应用程序的业务逻辑可以借助于 Java 对象缓存缓存常用对象。 业务逻辑层可以通过 Oracle TopLink(用于缓存数据对象)访问 Oracle 数据库。

图 1

图 1: 简单体系结构

Web 缓存框架有助于在 Web 应用程序环境中快速检索内容。 检索静态内容非常简单 - 实际上,HTTP 协议规范 (RFC 1945) 定义了一些内在机制来通过在 HTTP 客户端上缓存内容或通过使用代理缓存最大限度地减少 Web 浏览器和服务器之间的通信。

HTTP 缓存对静态内容很有效,但它无法处理针对每个用户个性化的动态内容。 对于缓存动态内容,您需要一个可以缓存页面片段并迅速从这些片段组装成文档的解决方案。Oracle Web 缓存就提供了该功能,如下所示:

Oracle Web 缓存是一个在应用程序外部维护的 HTTP 级缓存。 它利用支持缓存静态内容(例如,HTML、GIF 或 JPG 文件)的通用 Web 服务器(即,它是一个反向代理缓存)的内置缓存功能,但它还可以缓存动态内容,包括应用程序数据(如 SOAP 响应)。

Oracle Web 缓存使用一种名为 Edge Side Includes (ESI)(一种基于 XML 的语言,用于定义模板页面)的标准支持部分页面缓存。 (ESI 模板页面由 Oracle Web 缓存这样的 ESI 处理器用于从可缓存的和不可缓存的片段组装文档。)

与代理缓存和 Web 浏览器缓存不同,Oracle Web 缓存被设计为在 Web 应用程序和 Web 管理员可以使用 API 和工具控制缓存的服务器上运行。 Oracle Web 缓存非常快,但您无法在提交缓存内容之前处理该内容(使用 Java 代码)。 如果需要这样做,可以使用 Oracle Web 对象缓存 (WOC)。

Oracle Java 对象缓存 (JOC)(它是 Oracle Application Server Containers for J2EE (OC4J) 10g 的一个特性)是一个易于使用的通用缓存框架,可在进程内部、跨进程以及在磁盘上管理 Java 对象。 应用程序指定缓存的容量以及可以在缓存中保留的最大对象数。

通过 JOC,可以定义命名空间调用的区域、在区域内分组对象、在缓存中存储对象、检索缓存的对象以及随时替换它们。 您可以为每个对象指定生存时间,并可以使用事件监听器在缓存对象失效时通知您。 由于可以并发访问缓存对象,因此不应直接修改它们。 您必须创建缓存对象的专用副本,修改该副本,然后用修改后的副本替换此缓存对象。

Oracle Web 对象缓存 (WOC)(也是一个 OC4J 10g 特性)是一个用于缓存 Web 内容、可序列化 Java 对象和 XML 对象(DOM 树)的 Java 框架。 它是一个用于 Java 对象的应用程序级缓存,与 Servlet 和 JSP 页面运行在同一 JVM 上。

WOC 提供了一个 Java API 和一个 JSP 标记库,您可以使用它们管理在 J2EE 应用程序的 Web 层中缓存的动态内容和对象。 JOC 是 WOC 的默认缓存信息库,但您可以根据需要插入其他缓存信息库。

Oracle TopLink 提供了缓存和映射框架;缓存从数据库检索的数据可以提高应用程序的性能,而将关系数据映射到对象可以减少查询和更新数据库所需的手写代码的数量。

Oracle TopLink 提供了一个 Java API,您可以使用它构建与数据库无关的查询,该框架将这些查询转换为可以利用每个数据库服务器提供的特性的 SQL 语句。 执行一个查询后,TopLink 从结果集检索数据,并将此数据存储到被缓存的对象中。

要更新数据库,您可以使用 Oracle TopLink API 从缓存中检索对象的克隆,然后使用它们的 get() 和 set() 方法轻松地更新这些克隆的属性。 Oracle TopLink 执行繁重的操作、更新缓存的对象并生成 SQL 语句(将新数据存储到数据库中)。

集群缓存

当在多个服务器或节点上(如分布式和集群环境中)伸缩应用程序时,您也可以伸缩大多数 Oracle 缓存。 图 2 显示了部署到多个 J2EE 服务器并通过多个 Oracle Web 缓存节点访问的应用程序的体系结构。

Oracle Web 缓存集群充当一个逻辑缓存,它将缓存的内容分到构成集群的所有节点上。 有规律使用的页面将缓存到单个节点上,而“频繁使用的”内容自动在集群中进行复制。 对分区和复制的支持提高了性能并增加了 Web 缓存集群的可靠性。

Oracle JOC 和 Oracle TopLink 可以同步在不同 J2EE 服务器上运行的多个缓存。

图 2

控制 Web 缓存

假设两个并发用户(用户 A 和用户 B)正试图使用基于 Web 的界面更新同一段数据。 假定用户 A 首先提交了更改的信息,随后应用程序将此信息存储到数据库中。 此时,用户 B 很有可能在他的 Web 浏览器中看到的是陈旧数据,且对此数据的更改可能覆盖用户 A 所做的修改。即使应用程序禁止并发用户访问同一数据,但如果用户单击浏览器的“后退”按钮,某个用户仍然可以看到陈旧内容。 如果应用程序开发人员忽略这些问题,它们可能导致信息不一致或数据丢失。

在以下各部分中,我概述了几个确保所提供内容的新鲜性的策略,从而避免出现陈旧数据问题。

使用 No-Cache 头部 为了尽量减少网络通信量,Web 浏览器和代理必须缓存静态页面、JavaScript、CSS 文件和像。 而缓存动态内容可能产生不良的负面影响,尤其是当 Web 窗体包含从数据库中提取的数据时。

幸运的是,使用分别由 HTTP/1.0 和 HTTP/1.1 标准定义的“Pragma:no-cache”和“Cache-Control:no-cache”头部可以很方便地禁用 HTTP 缓存。 例如,可以使用一个简单的过滤器设置这两个头部:

package caches;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class NoCacheFilter implements Filter {
    private FilterConfig config;

    public void init(FilterConfig config)
            throws ServletException {
        this.config = config;
    }

    public void doFilter(ServletRequest request,
            ServletResponse response,
            FilterChain chain)
            throws IOException, ServletException {
        HttpServletResponse httpResponse
            = (HttpServletResponse) response;
        httpResponse.addHeader("Pragma", "no-cache");
        httpResponse.addHeader("Cache-Control", "no-cache");
        chain.doFilter(request, response);
    }

    public void destroy() {
    }

}

可以在应用程序的 web.xml 文件中为所有 JSP 页面、JSP 页面的子集或只为使用 JSF 和 ADF Faces 的网页配置此过滤器,如以下示例演示:

<filter>
    <filter-name>NoCacheFilter</filter-name>
    <filter-class>caches.NoCacheFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>NoCacheFilter</filter-name>
<servlet-name>FacesServlet</servlet-name>
</filter-mapping>

可以根据在应用程序中更新数据的方式修改此过滤器以设置其他 Cache-Control 头部以及 Age、Date、Expires 和 Last-Modified。 有关这些头部的更多详细信息,请参阅 HTTP 规范。

缓存动态内容正如前面所提到的,Oracle 提供了两个互补的 Web 缓存框架: Web 对象缓存 (WOC) 和 Oracle Web 缓存。 仅当必须在提交前使用 Java 代码对每个请求的缓存内容进行后处理时才应使用 WOC。 但在大多数情况下,某些页面片段或整个页面在生成后不需要任何类型的后处理。

Web 缓存是这些内容的理想解决方案,即使缓存内容依赖于请求参数或 cookie。 Web Cache 将为每组参数维护一个不同内容版本,并将替换用于个性化或用作会话 ID 的 cookie。 您只须正确配置 Web 缓存和使用 ESI 标记标识可缓存和不可缓存的片段。 在 JSP 页面中,可以使用生成 ESI 标记的 Java Edge Side Includes (JESI) 标记库。

JESI 有两个使用模型: “control/include”和“template/fragment”。 选择 control/include 时,应使用 <jesi:control> 设置每个页面的缓存属性,并使用 <jesi:include> 包含内容片段。 <jesi:control> 标记用于指定 JSP 生成的动态内容是否可以缓存。 如果可以缓存,则还可以指定一个以秒为单位的有效时间,如以下示例:

<%@taglib prefix="jesi" 
    uri="http://xmlns.oracle.com/j2ee/jsp/tld/ojsp/jesitaglib.tld" %>

<jesi:control cache="yes" expiration="3600"/>

<jesi:include page="header.jsp"/>
<br>
<jesi:include page="content.jsp"/>
<br>
<jesi:include page="footer.jsp"/>

可缓存页面的控制属性对包含的页面(它们也必须使用 <jesi:control> 标记指定它们的有效时间)没有影响。 Web 缓存将分别调用容器页面和包含的页面,这意味着这些页面不像 <jsp:include> 那样共享 JSP request 和 response 对象。 因此,容器页面和包含的页面无法通过 JSP request 作用域中存储的属性和 bean 进行通信。

使用“template/fragment”使用模型,您可以维护所有可以缓存和不可缓存的片段以及在同一 JSP 页中将它们动态连接在一起的标记。 Web 缓存将调用一个多次使用 <jesi:template> 和 <jesi:fragment> 以分别获取模板内容和片段的页面。 尽管该模型使用起来很困难,但它的优点是您不必将页面内容拆分到多个文件中。

Web 缓存使用 HTTP 协议接受使缓存内容失效的请求。 这些请求使用基于 XML 的格式,但您不必亲自构建它们,这是因为 Web 缓存提供了一个 Java API 和 JESI 标记,通过它们您可以指定缓存中必须失效的内容。 您还可以使用管理工具手动执行此操作。

关于数据版本控制和锁定策略

浏览器缓存、代理、WOC 和 Web 缓存提高了应用程序的 Web 性能,但也产生了陈旧数据和陈旧内容问题,不过可以使用框架的 NoCacheFilter 过滤器和缓存失效特性最大限度地减少此类问题。

由于当内容陈旧时在服务器上运行的应用程序无法通知 Web 浏览器,因此无法完全解决这些问题。 最有效的办法是确保陈旧数据不会通过 Oracle TopLink(支持数据版本控制)写入数据库。

至此,您已经了解了各种缓存方法中的某些方法,下面我们将了解一些将版本控制对象保存到数据库、检索这些对象以及更新或删除它们的方法。 您还将看到由 TopLink 生成和执行的 SQL 语句。

使用持久性对象当执行数据库查询或当某个工作单元成功提交了事务时,Oracle TopLink 将向共享会话缓存中添加持久性对象。 保存缓存的对象的身份映射可以使用强引用、弱引用或软引用,这些引用用于确定是否以及何时对这些对象进行垃圾回收。 除了在执行查询时由 Oracle TopLink 创建的对象以外,UnitOfWork 的 registerObject() 方法还返回您在代码中使用的对象克隆以修改持久性对象的属性。

Web 框架(如 JSF)还创建和管理 bean 实例。 要将 Oracle TopLink 与 JSF 一起使用,您需要一个将 JSF 创建的视图 bean 的属性复制到 Oracle TopLink 方法返回的对象克隆的方法,反之亦然。 Apache 的 Commons BeanUtil 提供了这样一个方法,应包装该方法以使它更易于使用:

package caches;

import org.apache.commons.beanutils.PropertyUtils;
import java.lang.reflect.InvocationTargetException;

public class MyUtils {

public static void copy(Object source, Object dest) {
try {
            PropertyUtils.copyProperties(dest, source);
} catch (IllegalAccessException x) {
throw new RuntimeException(x);
} catch (InvocationTargetException x) {
            throw new RuntimeException(
                x.getTargetException());
} catch (NoSuchMethodException x) {
throw new RuntimeException(x);
        }
    }

}

使用乐观锁定有两种方法可以防止并发工作单元(事务)修改同一持久性对象:

  • 一种方法是悲观锁定,即每当访问对象(即使仅读取对象)时将对象锁定,它用于在释放锁定前防止其他事务读取或更新对象;
  • 另一种方法是乐观锁定,即当读取对象(或行)时不锁定对象(或行);TopLink 只在工作单元提交更改时验证行的数据。

使用悲观锁定,当有人使用 Web 窗体更新数据库行时,将锁定该数据行。 悲观锁定的问题之一是行可以在无限长的时间内锁定,例如,用户在未单击提交按钮的情况下离开页面、浏览器崩溃或存在网络问题。

使用乐观锁定,Oracle TopLink 可以确认行的数据在从数据库中检索出来后是否被修改过。 执行此验证的方法之一是将所有或某些原始对象的字段与行数据进行比较。 该解决方案可以确保在工作单元工作期间没有其他人修改过该对象,如果在业务逻辑层中修改了该对象,且该过程未涉及任何用户界面,则该方法可行。

当使用基于 Web 的界面更新对象时,工作单元无法等到用户单击提交按钮,这是由于使悲观锁定不切合实际的同一原因而造成的。 唯一的解决方案是使用版本字段,每次更新行时该字段都会递增。 应用程序在读取对象时获取当前版本,然后将该版本作为隐藏的窗体域传递给 Web 浏览器。 可以使用 JSF 很轻松地成该操作:

<h:inputHidden id="version" value="#{myBean.version}"/>

当 Web 浏览器提交窗体数据时,JSF 将它与隐藏窗体域中的版本一起存储到视图 bean 中。 然后,Oracle TopLink 将修改后的数据版本与该行的版本进行比较。 如果两者不一致,Oracle TopLink 将抛出 OptimisticLockException。 否则,将更新行并递增版本。 以下示例使用一个简单 bean(包含一个用作主键的 id 属性、一个 version 属性和一个 data 属性)演示了该方法的工作原理:

package caches;

public class MyBean {
    private String id;
    private int version;
    private String data;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
    ...
}

在您自己的应用程序中,可以依需要使用任意多个数据属性,并可以根据需要重命名 id 和 version 属性。

保存新对象 要使用 Oracle TopLink 将 bean 存储到数据库中,必须获取一个客户端会话和一个工作单元。 然后,创建一个新的 bean 实例并使用 registerObject() 方法(返回一个可用于编辑的克隆)将它注册到此工作单元。

使用 MyUtils.copy() 将视图 bean 的属性复制到该克隆后,调用 commit() 方法,该方法将克隆的所有更改保存到数据库中。 这意味着 Oracle TopLink 将该视图 bean 的属性插入到数据库中。 此外,Oracle TopLink 设置克隆对象的版本并保存新的版本号。 因此,必须更新视图 bean 的版本属性:

public void insert(MyBean viewBean) {
    ClientSession session = server.acquireClientSession();
    UnitOfWork uow = session.acquireUnitOfWork();
    MyBean newBean = new MyBean();
    MyBean beanClone = (MyBean) uow.registerObject(newBean);
    MyUtils.copy(viewBean, beanClone);
uow.commit();
    viewBean.setVersion(beanClone.getVersion());
}

Oracle TopLink 生成并执行一个 INSERT 语句:

INSERT INTO MyTable (id, version, data) 
    VALUES (&apos;someID&apos;, 1, &apos;someData&apos;)

检索对象 您可以使用 Oracle TopLink 的查询 API 从数据库中读取一个持久性对象。 ReadObjectQuery 方法用于选择一个对象,而 ReadAllQuery 用于获取一个对象集合。 在这两种方法下都可以使用表达式生成器定义 SELECT 语句的 WHERE 子句。 在此示例中,id 字段必须等于 id 参数:

private MyBean read(Session session, String id) {
ReadObjectQuery query
        = new ReadObjectQuery(MyBean.class);
ExpressionBuilder myBean = new ExpressionBuilder();
query.setSelectionCriteria(
        myBean.get("id").equal(id));
return (MyBean) session.executeQuery(query);
}

read() 方法用于从数据库中选择一个对象,但当您要更新或删除现有对象时它也会对您有所帮助。 select() 方法获取一个客户端会话并调用 read():

public MyBean select(String id) {
    ClientSession session = server.acquireClientSession();
    return read(session, id);
}

Oracle TopLink 尝试从缓存中获取持久性对象。 如果缓存不包含该对象,则使用以下 SELECT 语句从数据库中检索持久性对象:

SELECT id, version, data FROM MyTable 
    WHERE (id = &apos;someID&apos;)

更新现有对象 当您要更新持久性对象时,可以使用选择对象时使用的同一 read() 方法。 这样,您获取了一个工作单元和持久性对象的克隆。 现在,您就可以修改此克隆的属性了,例如,使用 MyUtils.copy()。

正如前面所介绍的,当对象的版本与数据库中行的版本不同时,commit() 将抛出 OptimisticLockException。 这种情况下,可以使用 refreshObject() 刷新 bean 的属性:

public void update(MyBean viewBean) {
    ClientSession session = server.acquireClientSession();
    MyBean cachedBean = read(session, viewBean.getId());
    UnitOfWork uow = session.acquireUnitOfWork();
    MyBean beanClone = (MyBean) uow.registerObject(cachedBean);
    MyUtils.copy(viewBean, beanClone);
try {
uow.commit();
        viewBean.setVersion(beanClone.getVersion());
} catch (OptimisticLockException x) {
        Object staleBean = x.getObject();
        Object freshBean = session.refreshObject(staleBean);
        MyUtils.copy(freshBean, viewBean);
        throw x;
    }
}

Oracle TopLink 执行以下UPDATE 语句:

UPDATE MyTable SET data = &apos;modifiedData&apos;, version = 2 
    WHERE ((id = &apos;someID&apos;) AND (version = 1))

在下一个更新中,此 SQL 语句如下所示:

UPDATE MyTable SET data = &apos;changedData&apos;, version = 3 
    WHERE ((id = &apos;someID&apos;) AND (version = 2))

如果出现OptimisticLockException,可以使用 JSF API 向用户发出错误信号:

import oracle.toplink.exceptions.OptimisticLockException;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
...
try {
    myDAO.update(myBean);
}
catch (OptimisticLockException x) {
FacesContext context
= FacesContext.getCurrentInstance();
FacesMessage message
= new FacesMessage(x.getMessage());
message.setSeverity(FacesMessage.SEVERITY_FATAL);
context.addMessage(null, message);
}

此代码演示了网页中的异常消息,它对验证乐观锁定是否正常工作很有用。 在将应用程序部署到生产服务器上之前,必须使用用户可以理解的错误消息替换此异常消息。

删除对象 删除持久性对象非常容易。 只需像在以上示例中那样获取它们,然后调用 deleteObject():

public void delete(MyBean viewBean) {
    ClientSession session = server.acquireClientSession();
    UnitOfWork uow = session.acquireUnitOfWork();
    MyBean cachedBean = read(uow, viewBean.getId());
    uow.deleteObject(cachedBean);
uow.commit();
}

以下是 Oracle TopLink 执行的 DELETE 语句:

DELETE FROM MyTable 
    WHERE ((id = &apos;someID&apos;) AND (version = 3))

总结

使用缓存可以将拥有大用户群的复杂企业应用程序部署到常见硬件上。 但管理缓存却变得重要起来。 因此,您应使用易于使用的可靠缓存框架,而非构建您自己的缓存机制。

本文介绍了一些由 Oracle 开发的框架,并介绍了它们的使用场合。 Java 对象缓存是一个可以在业务逻辑层中使用的常规解决方案,但它还可以在 JSP 容器中用作 Web 对象缓存的缓存信息库。 类似 Oracle TopLink 和 Web 缓存这样的专门解决方案在持久层和表示层中提供了许多其他好处。 注意,您不必在同一应用程序中使用所有这些框架。 在许多情况下,仅仅其中的一到两个框架便可以提供足够的性能收益。


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织