为什么需要 Session 共享?
在传统的单体应用中,所有用户请求都由单个应用实例处理,Session 数据可以方便地存储在该实例的内存中(如 HttpSession)。

但在分布式系统架构下,情况变得复杂:
- 负载均衡: 通常会有多个应用服务器(如 Tomcat、Jetty)实例同时运行,通过一个负载均衡器(如 Nginx、F5)将用户请求分发到不同的服务器上。
- 问题所在: 如果用户的第一次请求被分发到 Server A,Session 数据被创建并存储在 Server A 的内存中,当用户的下一次请求被负载均衡器分发到 Server B 时,Server B 的内存中没有该用户的 Session 信息,导致用户需要重新登录,或者出现“未登录”等异常状态。
Session 共享的核心目标就是:无论用户的请求被路由到集群中的哪个服务器,该服务器都能获取到该用户完整的 Session 数据。
主流的 Session 共享解决方案
业界有几种成熟的方案来实现 Session 共享,每种方案都有其优缺点和适用场景。
粘性会话
这是最简单的一种实现方式。

- 原理: 负载均衡器在分发请求时,会“用户第一次访问时被分配到的服务器,之后该用户的所有请求,只要该服务器可用,都会被分发到同一个服务器上。
- 优点:
- 实现简单: 无需修改应用代码,只需在负载均衡器上进行配置。
- 性能高: Session 数据直接从内存读取,没有网络开销。
- 缺点:
- 负载不均: 可能导致某些服务器负载过高,而另一些服务器空闲。
- 单点故障: 如果某个服务器宕机,该服务器上所有用户的 Session 数据都会丢失,导致这些用户需要重新登录。
- 扩展性差: 当需要增加服务器时,新服务器的负载无法从旧服务器平滑转移。
- 适用场景: 对可用性要求不高、流量不大、或者可以容忍少量用户因服务器宕机而重新登录的场景。
集中式存储 Session
这是目前最主流、最推荐的方案,其核心思想是:将 Session 数据与应用服务器分离,存储在一个所有服务器都能访问的集中式存储中。
当需要读写 Session 时,应用服务器会从这个集中式存储中获取或更新数据。
常见的集中式存储介质:
-
Redis
- 原理: 将 Session 序列化后存储在 Redis 中,Redis 是一个高性能的内存数据库,读写速度极快,并且支持数据持久化,解决了数据丢失问题。
- 优点:
- 高性能: 内存数据库,读写速度非常快,对应用性能影响小。
- 高可用: Redis 本身支持主从复制、哨兵模式和集群模式,可以轻松实现高可用。
- 数据类型丰富: 除了简单的 Key-Value,还支持 List, Set, Sorted Set 等多种数据结构。
- 支持过期时间: 可以非常方便地设置 Session 的超时时间。
- 缺点:
引入外部依赖,需要维护 Redis 集群。
- 实现方式 (以 Spring Boot + Tomcat 为例):
- 添加依赖:
spring-boot-starter-data-redis,spring-session-data-redis。 - 配置
application.properties:# 启用 Spring Session spring.session.store-type=redis # Redis 连接信息 spring.redis.host=your-redis-host spring.redis.port=6379 # Session 过期时间 (单位: 秒), 默认是 30 分钟 server.servlet.session.timeout=1800
- 配置 Web 服务器(如 Tomcat)使用
HttpSessionListener来监听 Session 的创建和销毁,并自动将其同步到 Redis,Spring Session 会自动完成这一切。
- 添加依赖:
-
Memcached
- 原理: 与 Redis 类似,也是一个高性能的内存键值存储系统。
- 优点:
性能极高,简单易用。
- 缺点:
- 不支持数据持久化: Memcached 是纯内存的,服务器重启后数据全部丢失,这使得它在 Session 共享场景下不如 Redis 可靠。
- 功能相对 Redis 较为单一。
- 适用场景: 对数据持久化要求不高,追求极致性能的场景。
-
数据库
- 原理: 将 Session 数据序列化后存储在关系型数据库(如 MySQL)或 NoSQL 数据库(如 MongoDB)中。
- 优点:
数据可靠性高,有成熟的备份和恢复机制。
- 缺点:
- 性能瓶颈: 每次读写 Session 都需要进行数据库 I/O 操作,在高并发下会成为性能瓶颈。
- 增加了数据库服务器的负载。
- 适用场景: Session 数据量不大,并发量不高,且对数据持久化有极高要求的场景。
无状态应用 + JWT
这是一种更现代、更彻底的解决方案,其思想是不依赖 Session。
- 原理:
- 用户登录: 用户登录成功后,服务器根据用户信息生成一个加密的、自包含的 Token(通常是 JWT - JSON Web Token)。
- 返回 Token: 服务器将这个 Token 返回给客户端。
- 客户端存储: 客户端(通常是浏览器或 App)将 Token 存储起来(如 LocalStorage, Cookie, 内存)。
- 后续请求: 客户端在每次后续请求的 Header 中携带这个 Token(
Authorization: Bearer <token>)。 - 服务器验证: 服务器收到请求后,验证 Token 的合法性(签名、是否过期等),如果合法,就认为用户已登录,并从 Token 中解析出用户信息,完成业务处理。
- 优点:
- 无状态: 服务器端完全不需要存储 Session,这使得应用服务器可以无限水平扩展,因为任何服务器都可以处理任何请求,无需考虑 Session 数据的分布。
- 高可用和可扩展性: 架构非常灵活,易于实现负载均衡和容灾。
- 跨域友好: Token 可以轻松地在不同域名、不同服务之间传递。
- 性能好: 减少了服务器端的存储和查询开销。
- 缺点:
- 退出登录复杂: 由于 Token 存储在客户端,服务器无法使其立即失效,除非使用“黑名单”机制(将失效的 Token 存储在 Redis 中),否则只能等待 Token 自然过期。
- 安全性要求高: Token 一旦泄露,在有效期内攻击者都可以使用,需要 HTTPS 来防止 Token 在传输过程中被窃取。
- 数据存储受限: Token 的大小不宜过大,不适合存储大量用户信息。
- 适用场景: 微服务架构、前后端分离应用、移动端 App、需要高并发和高扩展性的现代 Web 应用。
方案对比与选型建议
| 特性 | 粘性会话 | 集中式存储 (Redis) | 无状态 (JWT) |
|---|---|---|---|
| 实现复杂度 | 低 | 中 | 中 |
| 性能 | 高 (内存) | 高 (内存) | 高 (无服务器端存储) |
| 高可用 | 差 (单点故障) | 高 (Redis 集群) | 高 (无状态) |
| 可扩展性 | 差 | 高 | 极高 |
| 数据安全 | 差 (服务器宕机丢失) | 高 (支持持久化) | 中 (依赖 HTTPS 和 Token 算法) |
| 退出登录 | 立即生效 | 立即生效 | 复杂 (需黑名单或等待过期) |
| 适用场景 | 简单应用、测试环境 | 绝大多数 Web 应用 | 微服务、前后端分离、移动端 |
选型建议:
- 首选方案: 对于绝大多数传统的 Java Web 应用,使用 Redis 作为 Session 存储的集中式方案是最佳选择,它在性能、可靠性和易用性之间取得了很好的平衡。
- 现代架构首选: 对于正在构建或迁移到微服务、前后端分离架构的系统,JWT 是更优的选择,它更符合云原生和微服务的理念,能带来架构上的巨大优势。
- 临时/简单方案: 如果只是临时测试或小型项目,且能接受单点故障的风险,可以考虑粘性会话。
代码示例 (Spring Boot + Redis Session 共享)
这是一个非常简单的 Spring Boot 项目配置,即可实现 Session 共享。
添加 Maven 依赖
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Session Data Redis -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- Spring Boot Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
配置 application.properties
# 服务器端口 (可以启动多个实例来模拟集群) server.port=8080 # Redis 连接信息 spring.redis.host=localhost spring.redis.port=6379 # 启用 Spring Session 并指定使用 Redis 作为存储 spring.session.store-type=redis # Session 超时时间 (单位: 秒), 默认 1800 (30分钟) server.servlet.session.timeout=1800 # (可选) 让 Session Cookie 的路径为根路径,方便跨应用访问 server.servlet.session.cookie.path=/
创建一个简单的 Controller 来测试 Session
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
@RestController
public class SessionController {
@GetMapping("/set")
public String setSession(HttpSession session) {
// 在 Session 中设置一个属性
session.setAttribute("user", "John Doe");
return "Session attribute 'user' has been set.";
}
@GetMapping("/get")
public String getSession(HttpSession session) {
// 从 Session 中获取属性
String user = (String) session.getAttribute("user");
if (user != null) {
return "Got user from session: " + user;
} else {
return "No user found in session.";
}
}
}
启动和验证
- 确保 Redis 服务正在运行。
- 编译并运行这个 Spring Boot 应用。
- 为了模拟集群,你可以再运行一个相同的应用,并修改
server.port(8081)。 - 使用负载均衡器(如 Nginx)或直接通过不同端口访问:
- 访问
http://localhost:8080/set,你会看到 "Session attribute 'user' has been set."。 - 然后访问
http://localhost:8081/get,你会看到 "Got user from session: John Doe."。 - 这证明了即使请求分发到了不同的服务器,Session 数据也能被成功共享。
- 访问
Spring Session 的工作原理:
当你添加了 spring-session-data-redis 依赖后,Spring Boot 会自动配置一个 SessionRepositoryFilter (一个过滤器),这个过滤器会拦截所有对 HttpSession 的请求,将原本 Tomcat 内置的、内存中的 Session 替换为 RedisOperationsSessionRepository,当你调用 session.setAttribute() 时,数据实际上是被序列化后写入了 Redis;当你调用 session.getAttribute() 时,数据是从 Redis 中读取并反序列化的,这一切对开发者都是透明的。
