可移植的、云就绪的HTTP会话

工程 | Josh Long | 2015年3月1日 | ...

一个适用于所有季节(和架构)的框架

Spring 走的是一条有趣的路线。无论你在何处运行它,它都能提供很多价值,而且因为它构建在依赖注入层之上,所以在底层与运行在其上的应用程序之间提供了一种自然的间接性。这种间接性通过解耦提高了代码的可移植性:你的应用程序代码不知道它正在使用的 javax.sql.DataSource(或任何其他)句柄来自何处,无论是 JNDI 查找、环境变量,还是 Spring 提供的一个简单的通过 new 创建的 bean。这种解耦以及 Spring 之上支持各种用例(批处理、集成、流处理、Web 服务、微服务、运维、Web 应用程序、安全性等)的丰富工具箱,使得 Spring 成为开发人员部署到(有时是嵌入式)Web 容器(如 Apache Tomcat 或 Eclipse Jetty)、应用服务器(如 WebSphere 和 WildFly),以及云运行时环境(如 Google App Engine、Heroku、OpenShift 和(我最近个人最喜欢的)Cloud Foundry)的合理选择。这种可移植性使得将大多数(合理编写的!)应用程序从应用服务器迁移到更轻量级的 Web 容器,并最终迁移到云端变得容易。

有状态的障碍

那么,问题出在哪里?为什么还要写这篇博客呢?

然而,对于使用 HTTP 会话的应用程序来说,情况并不理想。扩展 HTTP 会话的地方变得——请原谅这里的 HTTP 会话术语双关语——棘手(sticky)。你会发现,你的应用程序需要通过两件事情来扩展 HTTP 会话:会话亲和性(session affinity)和会话复制(session replication)。会话亲和性(或称粘滞会话(sticky sessions))意味着发送到集群 Web 应用程序的请求将被路由到最初发布 HTTP 会话 cookie 的节点。如果该应用程序实例下线,那么会话复制会确保相关状态在另一个节点上可用。客户端可以无缝地被路由到那里,保留所有会话状态。在流行的容器中配置 HTTP 会话复制并难。以下是如何在 Apache Tomcat 中进行设置 的页面,以及如何在 Jetty 中进行设置 的页面。典型的会话复制策略包括使用多播网络通知集群中其他节点状态变化。会话亲和性和会话复制在只有少量节点的小型环境中运行良好。除非你使用嵌入式 Web 容器,否则配置 HTTP 会话复制是容器中需要配置的另一件事,超出了应用程序的控制范围。

没关系,云会解决这个问题,对吗?

你可能会认为——至少——一旦将应用程序迁移到云端,这种配置会变得更容易、更可预测,但实际上它可能更令人痛苦!多播网络在大多数云环境中是禁止的,包括 Amazon Web Services。即使在像 Heroku 或 Cloud Foundry 这样更高级、更以应用程序为中心的平台即服务环境中,会话复制也并非易事。例如,Heroku 不提供会话亲和性或会话复制。这种限制是可以理解的:除了多播网络的限制外,应用程序应尽可能最小化服务器端状态。请记住,Heroku 将应用程序的内存限制为 512MB!如果你不尝试将多余的内存当作数据库或持久层,这已经绰绰有余!Cloud Foundry 方面,它服务于更大的开发者社区,并在各种数据中心本地运行,因此必须更实际一些。例如,Pivotal Web Services(运行 Cloud Foundry)为应用程序提供 1GB 内存,并且提供会话亲和性已有几年了。直到去年晚些时候,当构建包的一个更改使得任何基于 .war 的 Web 应用程序 部署到 Apache Tomcat Web 服务器的默认独立配置 时,它才提供会话复制。然而,这种支持不使用多播网络。相反,它利用配置任何绑定的 Redis 后备服务以供 Tomcat 容器的会话复制策略使用的约定。使用像 Redis 或某些共享文件系统这样的后备服务,实际上是云中会话复制唯一明智的方法。

所有这些方法都有不同的权衡

  • 有些是容器特定的,这意味着它们不容易从一个环境迁移到另一个环境。
  • 它们可能会给运维带来额外的复杂性(如果你还有那个团队的话!),这只会增加应用程序和生产环境之间的摩擦。
  • 它们可能使用多播网络,这在云环境中运行不佳。
  • 它们可能依赖于一些“魔法”,例如 Cloud Foundry Java 构建包,它只知道部署到独立 Apache Tomcat 的 .war 包,而不知道嵌入式 .jar 包或像 Jetty 这样的其他 Web 容器。
  • 所有这些其他点的隐含限制是持久化策略不可插拔。多播不适合你?没关系,使用 Redis。Redis 不适合你,想使用 Memcache 或其他不容易支持的东西?哦……

Spring Session 登场

Spring Session 为所有这些问题提供了一个非常好的解决方案。它是标准 Servlet HTTP 会话抽象的包装器。无论是基于 Spring 的应用程序还是非 Spring 的应用程序,都可以轻松地将其集成进去。它充当 HTTP 会话前面的一种代理,将请求转发给策略实现。开箱即用,有一个实现支持使用 java.util.Map<K,V>,另一个直接与 Redis 配合使用。使用 java.util.Map<K,V> 的实现起初听起来不是很有趣,但请记住,所有你喜欢的分布式数据网格(Pivotal GemFire、Hazelcast、Oracle Coherence 等)都可以提供一个由数据网格内存支持的 Map 实现的引用。

如果可用,Redis 特定的实现会利用 Redis 中的一些效率优势。让我们看看如何使用 Redis 设置一个非常简单的 Spring Session 应用程序。为什么选择 Redis?因为它确实是“Web 级”的——看看这篇关于 Twitter 如何使用它在 High Scalability 博客上扩展到 105TB RAM、3900 万 QPS 和 10000+ 实例的文章

为了使这个示例能够工作,我在 一个简单的 Spring Boot 项目 中添加了以下 Maven 依赖项。

  • org.springframework.boot:spring-boot-starter-redis:1.2.0.RELEASE
  • org.springframework.boot:spring-boot-starter-web:1.2.0.RELEASE
  • org.springframework.session:spring-session-data-redis:1.0.0.RELEASE

这是一个简单的示例应用程序

package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;
import java.util.UUID;


@EnableRedisHttpSession 
@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}

@RestController
class HelloRestController {

	@RequestMapping("/")
	String uid(HttpSession session) {
		UUID uid = (UUID) session.getAttribute("uid");
		if (uid == null) {
			uid = UUID.randomUUID();
		}
		session.setAttribute("uid", uid);
		return uid.toString();
	}
}

在运行它之前,请确保为此应用程序专门准备一个干净的 Redis 数据库。例如,你可以使用 FLUSHDB 重置当前数据库。Spring Boot Redis 启动器会自动连接到运行在 localhost 上的 Redis 数据库。如果你想将其指向特定位置,请使用 各种 spring.redis.* 属性

该示例尽可能简单:它只是确认数据正在写入 Redis 后备存储。在浏览器中与位于 localhost:8080/ 的 Web 应用程序交互后,打开你的 redis-cli 工具。第一个请求将触发一个唯一的会话,该会话将用于缓存 uid 值。同一浏览器会话的后续请求将看到相同的值。在 redis-cli 中输入 keys * 查看已持久化的内容。

部署到 CloudFoundry

迁移到此云环境可能有点棘手。如果你将其部署到 Cloud Foundry,Cloud Foundry 构建包会自动将 Spring Boot 自动配置的 RedisConnectionFactory 替换为指向绑定到应用程序的 Redis 实例的 RedisConnectionFactory。这在你在 Cloud Foundry 上运行、使用正确的构建包且应用程序中没有多个 RedisConnectionFactory 时是有效的。

我将使用 Cloud Foundry 的 manifest.yml 来描述此应用程序部署到 Cloud Foundry 时的样子。在这种情况下,它至少需要一个名为 redis-session 的后备服务,该服务支持 Redis 数据库。我已将此文件放在项目根目录,紧邻我的 Maven pom.xml。请注意,此 manifest.yml 提供了一个环境变量 SPRING_PROFILES_ACTIVE,该变量将激活 cloud Spring Profile。我们稍后会用到它。


---
applications:
- name: connectors
  memory: 512M
  instances: 1
  host: connectors-${random-word}
  domain: cfapps.io
  path: target/connectors.jar
  services:
    - redis-session
  env:
    SPRING_PROFILES_ACTIVE: cloud
    DEBUG: "true"
    debug: "true"

在推送应用程序之前,你需要创建一个 Redis 实例。我使用了以下命令在 Pivotal Web Services 上创建了一个简单的 Redis 实例(名为 redis-session,我们在 manifest.yml 中引用了它),然后推送了应用程序。

cf create-service rediscloud  25mb redis-session
cf push

我可以直接部署应用程序,在这种只有一个绑定的后备服务和一个已知类型的 bean 的示例中,一切应该能正常工作。

你可以使用 Spring Cloud PaaS 连接器来快速完成显式配置和消费云管理的 Redis 后备服务。在这种新的安排中,我们将使用 Spring Profile 来明确 Cloud Foundry 上的运行配置。像这样添加 Spring Cloud PaaS 连接器

  • org.springframework.cloud:spring-boot-starter-cloud-connectors:1.2.0.RELEASE

这将使得 Spring Boot 能够自动注入它所知的每种绑定后备服务类型的实例。如果存在一个服务 ID 为 redis-session 的 Redis 数据库,那么可以使用常规的 Spring Qualifier 来注入它,如下所示

   // ..
   @Autowired
   @Qualifier("redis-session")
   RedisConnectionFactory rcf;

这种方法是最简单的,如果你只添加 Spring Cloud 启动器依赖项,就会得到这种结果。如果你想显式配置服务,可以通过将以下属性添加到你的 Spring Boot src/main/resources/application.properties 来禁用 Spring Boot 启动器

spring.cloud.enabled=false

然后,显式使用 Spring Cloud PaaS 连接器。bean 定义cloud Spring Profile 激活时才有效。否则,Spring Boot 自动配置会生效(这正是你在本地运行时想要的效果)。

package demo;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.Cloud;
import org.springframework.cloud.CloudFactory;
import org.springframework.cloud.service.common.RedisServiceInfo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;
import java.lang.reflect.Field;
import java.util.UUID;

@EnableRedisHttpSession
@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

	@Bean
	@Profile("cloud")
	RedisConnectionFactory redisConnectionFactory() {
		CloudFactory cloudFactory = new CloudFactory();
		Cloud cloud = cloudFactory.getCloud();
		RedisServiceInfo redisServiceInfo = (RedisServiceInfo) cloud.getServiceInfo("redis-session");
		return cloud.getServiceConnector(redisServiceInfo.getId(),
                     RedisConnectionFactory.class, null);
	}
}

@RestController
class HelloRestController {

	@RequestMapping("/")
	String hello(HttpSession session) {
		UUID uid = (UUID) session.getAttribute("uid");
		if (uid == null) {
			uid = UUID.randomUUID();
		}
		session.setAttribute("uid", uid);
		return uid.toString();
	}
}

等等,还有更多内容...

这篇文章的重点是展示你如何轻松地在本地环境或云端为你的 Spring 应用程序获得可扩展的 HTTP 会话。我建议你再次将 JSF 页面图塞满你的 HTTP 会话!如果你需要一个用于存储轻量级业务状态(例如安全令牌)的、可过期、可扩展、临时性的存储,那么 Spring Session 会有所帮助。由于 Spring Session 位于你的应用程序和 HTTP 会话之间,它可以提供一些超出 Servlet HttpSession 之外的其他有用抽象。Rob Winch,Spring Security 和 Spring Session 的负责人,在文档和其他博客文章中就这些其他用例做了出色的讨论,所以我在这里只是简单回顾一下

我很幸运上周做了一个关于 Spring Session 的网络研讨会。Rob 告诉我了一些未来版本中可能会有的东西

  • 会话并发控制(“让我从我的其他账户中退出”)
  • Spring Batch 和 Spring Integration claim-check 支持
  • 支持账户管理 - 优化的持久化(超越 Java 序列化),
  • 更智能的、可注入的 bean(与作为已知请求属性公开但在其他情况下可作为 Spring MVC 参数等提供的 bean 相对)

谢谢 Rob 提供的所有精彩信息。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

抢先一步

VMware 提供培训和认证,助你快速进步。

了解更多

获取支持

Tanzu Spring 提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一个简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看全部