大型servlet编程

大型Servlet编程模型

 

Kyle Brown (brownkyl@us.ibm.com), 高级顾问, IBM WebSphere Service
Rachel Reinitz (rreinitz@yahoo.com), 高级软件工程师, B2B 提供商 Ventro(前 Chemdex)
Skyler Thomas (tskyler@us.ibm.com), 高级顾问, IBM WebSphere Service

 

简介: 本文研究在服务器上存储客户机数据的可扩展性的关键问题和使您的 servlet 在高流量的环境下正常运行的一些方法.

发布日期: 2001 年 11 月 01 日  级别: 初级  访问情况 : 1637 次浏览  评论: 0 (查看 | 添加评论 - 登录)

平均分 5 星 共 1 个评分 平均分 (1个评分) 为本文评分

 

这篇文章首先在 Java report中出现,得到允许后重新发表在这里。

Kyle Brown 和 Skyler Thomas 是北卡罗来纳州 Research Triangle Park 的 IBM WebSphere Service 中的高级 Java 顾问。可以通过brownkyl@us.ibm.com tskyler@us.ibm.com分别联系他们两人。Rachel Reinitz 是 San Jose 的 CA 公司的独立 Java 顾问/设计师。可以通过 rreinitz@yahoo.com与她联系。 

Java Servlet API 和 Java 服务器页面(Java Server Pages,JSP)对开发用于 Web 的高性能的服务器端 Java 程序有很大帮助。然而,当处理非常大的、高流量的 Web 站点时可能会影响到您对 servlet 的设计这一因素是必须要考虑的。接下来我们会研究在服务器上存储客户机数据的可扩展性的关键问题和使您的 servlet 在高流量的环境下正常运行的一些方法。

会话(Session)数据到底是什么?  通常,Web 环境中的 会话是指用户/客户机的 Web 浏览器和一个特定的 Web 服务器之间的一组交互。会话从最初浏览器调用 Web 服务器的 URL 开始,到 Web 服务器结束会话,这个会话“超时”,或当用户关闭浏览器时结束。 会话数据是指在永久保存之前用户提供的在多个页面上使用的信息。会话数据和事务数据(Transaction data)之间的区别在于,会话数据是暂时的 - 只用于一组相连接的页面 - 而事务数据是用于永久的存储。会话数据通常在一组 Web 页面之后被转换成 事务数据(当用户选择“提交”事务,或为购物车“付帐”时)。

考虑下面的情节:我们的用户 Rob 输入了他最喜欢的(假定的)酒类购物站点的 URL,www. winesRus.com。Rob 浏览页面寻找要买的酒,并在购物车里添加了一些。与站点的所有交互发生在一个由 www.winesRus.com Web 服务器控制的单一会话中。当 Rob 逐屏浏览,购买更多酒时,他要购买的物品的信息、他的地址,还有 Rob 提供的其它任何信息都被保存为会话数据。站点给他要购买的物品定价,并请求他确认。当 Rob 确认时,会话数据被用来执行购买事务,然后数据变成永久性的。

Servlet 与特定的 HTTP URL 一一对应,这很象传统的 Web 编程中每个 URL 要有自己的 CGI 脚本一样。例如,http://winesRus/servlet/purchaseWinesServlet 脚本对应于 http://winesRus.com/servlet/purchaseWinesServlet。

就象 CGI 脚本一样,servlet 自己是 无状态的。和 CGI 脚本相似,servlet 也从 HTTP 参数和 HTML 表单获得客户机数据。在一个特定的应用服务器上,每个 servlet 类的一个单独实例为它的特定 URL 处理所有的doGet()和doPost()请求。每一个 HTTP 请求都在一个运行该实例的service()方法的唯一线程上处理。因为每个 servlet 实例都是共享资源,您不能在 servlet 本身存储客户机会话数据(比如顾客的购物车)。会话数据必须存储在 servlet 之外。

我们在本文中会研究几种存储会话数据的方法。会话数据怎样存储是影响一个 Web 站点的可扩展性的关键因素。我们会谈到每一种方法的优缺点,并对关于特定情况下的最好的处理方法提一点建议。

应用服务器工具  存储会话数据最简单的方法可能就是通过使用应用服务器为此提供的内置的工具。 * Sun 的 Java Servlet API 在 2.0 版本中引入了一种存储会话信息的方法。这是通过HTTPSession接口实现的,该接口提供了通过关键字在字典中存储、查找和移去对象的方法。应用服务器提供了一个类实现HTTPSession。

最重要的HTTPSession方法有:

        
public abstract void putValue(java.lang.String param1, java.lang.Object param2);
public abstract java.lang.Object getValue(java.lang.String param1);
public abstract void removeValue(java.lang.String param1);

    
      

(注意, param1 是要存储的对象的键或名称; param2 是要存储的对象。)

应用服务器通过分配一个存储在用户浏览器里的一个专用 cookie 中的会话标识符来确定哪个HTTPSession实例属于一个特定的用户。要使会话工作,浏览器必须启用 cookie。会话 cookie 不是永久保存的,在浏览器关闭的时候就会过期。通常,打开 cookie 是没问题的。许多高容量站点都需要 cookie,包括 Yahoo 和 Amazon。万一在客户机浏览器的 cookie 被关闭的情况下,另一种作法是:使用 URL 重写技术存储会话标识符。要使用 URL 重写,您要使用HTTPResponse接口的encodeUrl()方法向您的 servlet 或 JSP 生成的 URL 附加一个唯一的会话标识符。不管使用哪种存储会话标识符的方法,HTTPSession实例最初都保存在应用服务器的 Java 虚拟机的内存中。只有标识符保存在客户机上。

现在的问题是,HTTPSessions怎样存储在服务器上?缺省的实现(在 Servlet JSDK 的参考实现中)是在 servlet 的 Java 虚拟机的内存中存储会话数据。这样,在需要时访问个别的HTTPSession实例,效率就高多了。然而,当我们需要扩展应用程序以处理更多用户的请求和开始使用运行相同 servlet 的多个服务器时,这种机制就变得复杂起来。要理解这种困难,请参看图 1,它示范了一个高流量 Web 站点的一般设置。

图 1. 负载平衡配置。  

在大多数高流量 Web 站点中,进入的 HTTP 请求总量对于一个应用服务器来说太大了,很难处理。所以,路由器(可以是硬件路由器,也可以是软件路由器,如 IBM 的 e-Network dispatcher)被用来将进入的 HTTP 请求分给多个应用服务器。路由算法(如循环路由或随机路由)选择哪个服务器将处理每一个特定的请求。这种应用服务器之间的路由会影响到我们的应用程序需要如何管理会话数据。

如果HTTPSessions只存储在创建它们的服务器的内存中,该服务器就必须接收来自那个客户机的 所有后续请求。这种要求被称为服务器亲缘关系。这个会话信息对在其它应用服务器上运行的 servlet 将不可用。对于一些 Web 站点来说,服务器亲缘关系可能不会引发问题。然而,路由器确定服务器亲缘关系的方式可能会在较高容量的 Web 站点上引发问题。

在许多路由器中,通过检查进入请求的 IP 地址,并总是把特定客户机地址的请求分配给特定的服务器,客户机被“分配”给一个特定的应用服务器。然而,今天因特网的实际情况是,许多 ISP 都有代理服务器配置,这会让路由器认为来自这个 ISP 的数据包都是从同一个 IP 地址来的。在最坏的情况下,这意味着所有从 AOL(American Online,美国在线,它可能构成了您站点上超过 60% 的流量)的数据包最后都到了同一个应用服务器。这会让负载平衡的目的无法实现,因为一个服务器最终仍然占了处理任务的一大部分。而且,很多公司现在随机分配对外的 IP 地址,所以不能保证同一个客户机两个请求有同样的 IP 地址。在这种情况下,服务器亲缘关系不能被保证。

服务器亲缘关系对于在很少的机器上运行的站点来说可能是可以接受的,还有对于那些允许用户“丢失”会话的情况来说也是可以接受的。

客户机端解决方案  对于在客户机而不是服务器上存储会话数据,有两种主要的解决方案 - cookie 和隐藏域(hidden field)- 每一种方案都有各自的优势和劣势。

隐藏域出现的第一种保留会话数据的机制之一是使用“隐藏域”。这种选择依赖于 HTML 的一个特殊功能来保存会话信息。HTML INPUT标记有几种不同的类型,可以让 Web 作者指定如何接收输入。例如,“TEXT”类型会导致浏览器显示一个文本域。然而,有一种输入类型不符合任何特定的 UI 部件:“HIDDEN” 类型。隐藏域没有 UI 表现形式,所以不能被浏览器用户改变。隐藏域的值可以在 Web 页面上由应用服务器设置,以后再通过HTTPServletRequest接口的getParameter()方法读取回来,就好像其它所有 HTML 域一样。这正是我们在客户机 HTML 中记录会话信息所需要的。使用隐藏域存储会话数据的一个主要缺点是,我们必须改写 HTML 使其包括这些新的信息。下面的 Java 代码指出要做下面的工作:

        
  out.print("<input type=\"HIDDEN\");
  out.print("NAME=\"FieldName\" value=\"");
  out.print(fieldValue);
  out.println("\">");

    
      

隐藏域使用很简单,正因如此,它们也存在一些问题以至于对很多系统来说不适用。第一个问题是,JSDK 没有提供一种将任意对象移出和移入隐藏域的方法。在 JSDK 中,处理隐藏域的方法和处理其它 HTTP 参数的方法是一样的,也就是说隐藏域是作为字符串来处理的。如果您想在自己的程序中使用这些字符串里面的信息,就必须建立一个架构用来生成合适的 HTML 和重新解析信息。

另一个缺点是,您的会话信息最终会在网络上发送很多次。为了理解这是怎样发生的,考虑下面的情况:

我们的酒类购物站点有一个两页的“频繁顾客程序”。第一页接收用户信息(姓名、地址等),而第二页接收选择信息(您更喜欢加州酒还是法国酒?您喜欢夏敦埃酒还是墨尔乐红葡萄酒?诸如此类等等)。第一个 servlet 从前一个 HTML 表单中解析出用户信息,并将其记录在隐藏域中。第二个 servlet 不仅必须解析出新的 HTML 表单信息,还要解析出前一个 servlet 重新写入隐藏域的信息。这样,每一个后续的页面不断地增加要解析的信息,当您进一步继续处理时就会使下载时间变长。

使用隐藏域的一个最大的缺点存在于混合型的站点中。新的 servlet 实现必须与旧的 CGI 和 HTML 共存很多次。如果您不能修改这些页面,当用户在 servlet 和旧的页面之间遍历时,隐藏数据就会丢失。

假定我们的酒类购物站点一直是用 CGI 程序实现的。并且假定旧的购物车是作为 www.winesRus.com/cgi-bin/shoppingcart 来实现的。站点中目前所有的 HTML 页面都指向这个 URL。我们可以改变所有的页面让它们指向 www.winesRus.com/servlets/shoppingcart,或者我们可以简单地用旧的链接给 servlet 起一个别名。如果您有数百个目录页指向这个链接,这个别名可以省掉您很多时间。然而,如果您是用隐藏域遍历站点,这种解决方法就不起作用。隐藏域必须添加到旧的 HTML 中,而且您可能还要改变这些链接。

使用隐藏域还有其它的缺点(请参阅下面章节有关安全性的讨论),但是对于某些情况,他们还是不错的。在安全性不高的某些站点中可以考虑它们,那里旧页面浏览最少,而且页面不是大量地“建立”在彼此之上,还有在不能接受服务器亲缘关系的地方也可以考虑它们。在为隐藏域的生成而建立框架和解析这两个步骤上付出的一些简单的努力以后可能会带来很多好处。然而,正如我们所见到的,对于大多数情况,都会有更好的解决方案。

Cookie我们要研究的下一个存储会话数据的方法是直接在 cookie 中存储数据。正如我们在HTTPSessions的讨论中已经看到的,cookie 也可以用于在客户机浏览器上存储信息。它们不仅可以用于存储客户机身份标识,还可以存储实际的会话数据本身。cookie 有巨大的优势。

首先,cookie 不需要 HTML 重写。您将会话数据转换成如隐藏域示例所示的字符串,然后将其添加到您的HttpServletResponse对象中,如下所示:

        
package com.winesrus.tests;
import javax.servlet.http.*;
public class CookieTest {
public CookieTest(HttpServletResponse resp, String state) {
String warning = Please accept this Cookie or bad things will happen to you!"
Cookie cookie = new Cookie("winecookie", state);
cookie.setDomain(".winesrus.com");
cookie.setPath("/");
cookie.setComment(warning);
cookie.setVersion(0); 
resp.addCookie(cookie);
}
}

    
      

您可以用以下语句检索 cookie:

        
javax.servlet.http.HttpServletRequest.getCookies();

    
      

就是这样。您没有必要重写 HTML 页面来获取和设置 cookie 数据。

关于 cookie 的另一个优点就是,您可以和非 Java 资源共享您的会话数据。您的 JavaScript 和 CGI 程序可以利用这种状态信息,因为它是通过每一个客户机请求发送的。

然而,容量限制可能是使用 cookie 存储会话数据的致命弱点。一个 cookie 头最多可以存储 4K 的文本。这就让存储大的数据集合很不实际。您还需要注意您决定在 cookie 中存储的内容。这个 cookie 头包括了您 Web 站点中的所有 cookie。如果超过了最大的 cookie 容量,就会出现不好的后果。例如,依照用户浏览器的不同,不是旧 cookie 会丢失就是新 cookie 不能写入。要避免这种情况,要确保您的 cookie 不接近 4K 的限制。您要给自己足够的空间,来应付用户在自己的浏览器中已经有别的来自您的域的 cookie 的情况。这样就必须编写代码检查 cookie 是否被成功写入。您当然不能盲目地将对象序列化后将它们放到 cookie 中去。您必须非常有选择性的组织例程。

另一个主要的缺点是用户可以随意关掉 cookie。现在大多数浏览器都支持 cookie。然而,有少数 Web 站点用户希望在自己的浏览器中禁用 cookie。这就强迫您要在 HTML 页面中编写 JavaScript 或在 servlet 中编码以检测用户浏览器中的 cookie 是否被打开。所以,如果您使用 cookie 作为会话数据存储机制,您就必须有另一种机制作为“退路”,或者通报您的用户该站点没有 cookie 无法正常运行。

cookie 还有另一个缺点,就是关于它们怎样在 Web 中传送是有限制的。它们不能传送给同级域名。

这么说吧,WinesRus 买了一个新的域名:BeerIsUs.com。我们希望用户能够使用同样的购物车付帐(www.winesRus.com/servlets/shoppingcart)。问题在于,使用 cookie 的话,我们站点的 WinesRus.com 部分无法看到 BeerIsUs.com 创建的 cookie,反过来也一样。注意,如果我们有一个名为 Commerce.WinesRus.com 的特定的商业服务器,它就能够看到在 WinesRus.com 创建的 cookie,但是 WinesRus.com 看不到 Commerce.WinesRus.com 创建的 cookie。

在单独域名上可以存在的 cookie 数目是有限制的。某些域名限制是与浏览器有关的,所以您必须用多个浏览器来作全面的测试以确保 cookie 按照设计运行。使用 cookie 会使域名问题非常复杂。

cookie 和隐藏域都有另一个主要的缺点: 安全性。在客户机上存储的状态信息是不安全的。除非您花时间将数据加密,您在因特网上来回发送的数据都是明文。即使您手工或使用 SSL 将数据加密,您仍然不会愿意在客户机上存储敏感的商业数据。

所以,使用客户机保存会话的不利情况可能会使会话数据存储机制的优势黯然失色。客户机端解决方案最初看起来可能很简单,但复杂性很快就会成倍增长。相反,我们需要寻找那些最初比较复杂但会带来更好的整体效果的解决方案。

后端持久性方法  让我们来研究问题的另一方面:服务器端解决方案。服务器端的方法依靠客户机端解决方案起作用。通过 cookie 或隐藏域存储很少信息并把它从一个请求带到下一个请求,这些信息通常是用户标识或键值,用来唯一标识发出请求的客户机浏览器。在站点上所有 Web 应用服务器共享的第三层数据存储中保存更多的会话数据,然后用客户机端“用户标识”来“查找”会话数据。很多商业应用服务器现在都支持第三层数据存储作为会话管理。

实现这种方法的一个很好的示例就是 WebSphere 3.0 的共享HttpSession实现。WebSphere 3.0 的共享HttpSession是这样操作的:HttpSession的每一次使用都是第三层关系型数据库上的一个事务。事务是在HttpServletRequest.getSession()方法被调用时开始的。事务在 servlet 的service()方法结束时终止,或当在 WebSphere 中实现HttpSession的类上调用sync()方法时终止。

图 2 分步阐明了共享的HttpSessions在 WebSphere 中如何工作的基本要点。注意,这个示例是作为对所涉及角色的高级描述,而不是这些类运作方式的实际描述。

图 2. 共享的HttpSessions在 WebSphere 中工作。  

如图 2 所示,过程由getSession()方法开始。在此方法中,WebSphere 的实现类在共享的Session数据库上开始数据库事务,然后检索用于存储全局唯一会话标识的 cookie(名为sessionid)的值。它使用这个会话标识在共享的 Session 数据库中查找合适的行,然后从该行检索一个长的二进制列或者 二进制大型对象(Binary Large Object,BLOB)列。该列包含(以二进制形式)对应于会话标识的序列化的HttpSession实现对象。WebSphere 将HttpSession对象反序列化再返回给请求的 servlet。

然后 servlet 就可以在 HTTP 会话中作获取或设置值。客户机事务保持打开状态,直到sync()方法被调用或 servlet 的service()方法结束为止。不管是哪种情况,HttpSession对象都被序列化成数据库行的 BLOB 列,然后事务被提交。

当然如果您的应用服务器供应商还没有实现持久性会话,这种方法也可以使用,不过您的工作将变得比较困难。您可以使用 JDBC API 在任何主要的 RDBMS 供应商的数据库中以 BLOB 格式存储序列化的对象。虽然将对象序列化后抛至 BLOB 中是一种相当简单的过程,但 BLOB 的性能会变得越来越差。(注意,不同数据库厂商的 BLOB 性能会有很大差异,所以您的具体情况可能会不一样。)

一种使用更困难但性能也更好的解决方案就是使用 JDBC 和 SQL 在非 BLOB 字段存储会话数据。这意味着您要将数据库模式映射为会话数据。您不能只是将对象序列化,而需要编写自己的组织例程来加工和补充您的会话数据。

还有一个更特殊的解决方案,就是使用一个面向对象的数据库来作为会话信息的缓存。这将使您的对象的持久性稍微容易一点。您将会看到这种解决方案在性能上的优点。OODMS 方法的不利之处在于数据库的管理、成本、容错,和使用了一项被认为是“非标准”的技术。

另一种选择是使用 EJB 存储会话状态。这样做有两种可能的方式。第一,您可以在 cookie 或重写的 URL 中存储实体 EJB 的关键字,然后使用findByPrimaryKey()定位合适的 EJB 来检索状态。这很可能是整体上最好的选择,因为您的客户端和服务器端都是可扩展的;例如,如果您的应用服务器在 EJB 之间使用了 Work Load Management,效果就会更好。这种选择 必依靠 JDBC,因为您可以使用其它的数据资源来存储您的会话数据(例如 CICS)。然而,处理复杂的会话数据时也会有很多问题,因为对 EJB 的关联调用代价会很高。

EJB 的第二种选择是在 cookie 或隐藏域中存储有状态的会话 EJB 句柄。这将不需要前一种解决方案的数据库查询开销,但不幸的是,大多数 EJB 服务器不为有状态的会话 EJB 提供故障排除功能。在这种情况下,您需要提供自己的代码来进行差错处理,这很容易使代码变得复杂和庞大起来。

结论  那么,我们学到了什么呢?表 1 详细列举了我们评价过的处理会话状态的几种不同选择,还有每种选择的优势和劣势。对这个问题没有最好的解决方案。您必须仔细考虑每种解决方案的优势和劣势,然后选择一种最适合您特定问题的方案。

表 1. 处理会话状态的选择。
解决方案 最初的易用性 性能 会话容量 安全性 可扩展性 总共开发时间
内存中的会话 很简单 很好 很短
隐藏域 容易 中等 中等
Cookie 容易 中等 中等
厂商 BLOB 会话 很容易 中等偏差 很短
自定义 BLOB 会话 中等 中等偏差 中等偏高 中等
自定义 SQL 组织会话 中等 中等偏高 很高
OODBMS 中等 中等偏好 中等 中等
EJB 中等 中等偏差 很高 中等偏高

感谢  我们要感谢 IBM 的 Patrick LiVecchi,他就服务器亲缘关系的问题耐心地指导我们,还要感谢 IBM WebSphere Service 的 David Williams,他帮助我们深入了解那些有关 cookie 的繁琐细节。我们还要感谢 IBM WebSphere 开发小组的 Gabe Montero,他使我们消除了对于 WebSphere 3.0 中会话群集问题的疑问。

脚注  * 这里假定您的应用服务器实现 Java servlet 开发包(JSDK)2.0 版本,这是大多数商业服务器(JRun、WebSphere、Weblogics等)已实现的。 

作者简介

Kyle Brown 是 IBM WebSphere Service 的高级顾问。他从 1989 年起就开始从事面向对象设计和编程,1996 年起开始开发大规模服务器端 Java 系统。他经常在关于 Pattern、J2EE 和 WebSphere 主题的会议上发言。过去十年里,他一直是 The Java Report 和其它一些杂志的积极的供稿人,还是 The Design Patterns Smalltalk Companion 和即将推出的 Enterprise Java Programming with IBM WebSphere (将于 2000 年由 Addison-Wesley 出版)的合著者之一。

Rachel Reinitz 是 B2B 提供商 Ventro(前 Chemdex)的高级软件工程师。在 1999 年加入 Ventro 之前,她是 IBM WebSphere Service 的高级顾问,主要负责因特网/企业网应用程序体系和产品故障问题。在 Ventro,Rachel 领导一个开发小组使用极限编程(eXtreme Programming,XP)技术开发基于 Web 的供应商工具,现在致力于贸易伙伴集成。

Skyler Thomas 是 IBM WebSphere Service 的高级顾问。Skyler 在初露倪端的 Web 个性化领域是个先驱者。Skyler 给国际上银行业、电子商务和保险业的很多公司做过顾问。他经常在主要的面向对象会议上(如 JavaOne、OOPSLA 和 WebSphere 2000)发言。

posted on 2013-09-07 20:48  荣锋亮  阅读(244)  评论(0编辑  收藏  举报

导航