1.问题
众所周知,标准的Servlet API中,有一个HttpSession的接口。本来HTTP协议是无状态的,通过session机制,就能把无状态的变成有状态的。有了session的支持,WEB应该就能够跟踪一个用户的操作状态。在一个WEB应用中,你可以这样使用session:
// 取得session对象
HttpSession session = request.getSession();
// 在session中保存用户状态
session.setAttribute(“loginId”, “myName”);
// 在另一个请求中,取出session的状态
String myName = (String) session.getAttribute(“loginId”);
如何保存session中的状态?一般的做法,是将session对象保存在内存里,然后用一Session ID来索引。同一时间,会有很多session被保存在服务器的内存里,但每个session的ID都是不一样的。对于Java Servlet,常见的做法是把这个Session ID保存在cookie里。这样一来,凡是cookie值相同的所有的请求,就被看作是在同一个session中的请求。由于内存是有限的,较先进的服务器会把session对象交换到文件中,以确保内存中的session数保持在一个合理的范围内。
为了提高系统扩展性和可用性,我们会使用集群技术 —— 就是一组独立的机器共同运行同一个应用。对用户来讲,集群相当于一台服务器。而实际上,同一用户的两次请求可能被分配到两台不同的服务器上来处理。这样一来,怎样保证两次请求中存取的session值一致呢?
多数的服务器会使用session复制的方法:当session的值被改变时,将它复制到其它机器上。这个方案又有两种具体的实现,一种是广播的方式。这种方式下,任何一台服务器都保存着所有服务器所接受到的session对象。服务器之间随时保持着同步,因而所有服务器都是等同的。可想而知,当访问量增大的时候,这种方式花费在广播session上的带宽有多大,而且随着机器增加,网络负担成指数级上升,不具备高度可扩展性。另一种是TCP-ring的方式,也就是把集群中所有的服务器看成一个环,A->B->C->D->A,首尾相接。把A的session复制到B,B的session复制到C,……,以此类推,最后一台服务器的session复制到A。这样,万一A宕机,还有B可以顶上来,用户的session数据不会轻易丢失。但这种方案也有缺点:一是配置复杂;二是每增添/减少一台机器时,ring都需要重新调整,这将成为性能瓶颈;三是要求前端的Load Balancer具有相当强的智能,才能将用户请求分发到正确的机器上。
另一种保存session的思路是,将session保存在单一的数据源中。这个数据源可被集群中所有的机器所共享。这样一来,复制就不是问题了。然而性能成了问题。每个用户请求,都需要访问后端的数据源(很可能是数据库)来存取用户的数据。这种思路的第二个问题是:缺少应用服务厂商的支持 —— 很少有应用服务器支持这种方案,更不用说数据源有很多种(MySQL、Oracle、Hsqldb等各种数据库、专用的session server等)了。第三个问题是:数据源成了系统的瓶颈,一但这个数据源崩溃,所有的应用都不可能正常运行了。
综上所述,session看起来很简单,实际上是一个大问题!犹其是对一个访问量极高的大型网站而言。
2.解决方案
2.1.将session数据保存在客户端
前面所说的通过复制,或者通过单一数据源,都可以解决集群中分布式session的问题,然而存在前面所说的很多问题。另一种思路,是把session保存在客户端。这样一来,服务器就变成无状态了,就能够做到线性可扩展和极高的可用性。怎么保存呢?目前唯一的方法,恐怕就是cookie了。
Cookie方案也不是完美的,有以下限制:
1. Cookie数量和长度的限制。每个domain最多只能有20条cookie,每个cookie长度不能超过4KB,否则会被截掉。
2. 安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。即使加密也与事无补,因为拦截者并不需要知道cookie的意义,他只要原样转发cookie就可以达到目的了。
3. 有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。
虽然有上述缺点,但是对于其优点(极高的扩展性和可用性)来说,就显得微不足道。然而我们仍然要设法回避上述的缺点,方法是:
1. 通过良好的编程,控制保存在cookie中的session对象的大小。
2. 通过加密和安全传输技术(SSL),减少cookie被破解的可能性。
3. 只在cookie中存放不敏感数据,即使被盗也不会有重大损失。
4. 控制cookie的生命期,使之不会永远有效。偷盗者很可能拿到一个过期的cookie。
2.2.将session保存在专用的服务器上
前面提到,由于cookie机制本身的限制,我们不可能将所有的数据都放在cookie中,势必要有服务器端的方案来辅助。相比复制的方法,单一的数据源更简单更可靠。我们可以使用数据库来保存这部分session。然而我倾向于使用更廉价、更轻量的存储。事实证明,BerkeleyDB是一种很好的选择。
由于我们只把少量关键的信息保存在服务端,因而这个数据源的压力不会非常大。事实证明,BerkeleyDB在保持良好的可用性的前提下,具有相当高的系统容量,可以高效处理大量的数据请求。
2.3.创建通用的session框架
不是Java Servlet API的session机制不够灵活,而是多数应用服务器并没有留出足够的余地,来让你自定义session的存储方案。根据上面的讨论,任何一种session方案都有其优缺点。最好的方法是把它们结合起来。例如:结合cookie-based session和berkeleyDB-based session,就可以解决我们的绝大部分问题。
纵使某个应用服务器提供了对外扩展的接口,可以自定义session的方案,我们也不大可能使用它。为什么呢?因为我们希望保留选择应用服务器软件的自由。
因此,最好的方案,不是在应用服务器上增加什么新功能,而是在WEB应用框架上做手术。一但我们在WEB应用框架中实现了这种灵活的session框架,那么我们的应用可以跑在任何标准的JavaEE应用服务器上。
除此之外,一个好的session框架还应该做到对应用程序透明。具体表现在:
1. 使用标准的HttpSession接口,而不是增加新的API。这样任何WEB应用,都可以轻易在两种不同的session机制之间切换。
2. 应用程序不需要知道session中的对象是被保存到了cookie中还是别的什么地方。
3. Session框架可以把同一个session中的不同的对象分别保存到不同的地方去,应用程序同样不需要关心这些。例如,把一般信息放到cookie中,关键信息放到berkeleyDB中。甚至同是cookie,也有持久和临时之分,有生命期长短之分。