杰瑞科技汇

Java中如何实现session共享?

为什么需要 Session 共享?

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

Java中如何实现session共享?-图1
(图片来源网络,侵删)

但在分布式系统架构下,情况变得复杂:

  • 负载均衡: 通常会有多个应用服务器(如 Tomcat、Jetty)实例同时运行,通过一个负载均衡器(如 Nginx、F5)将用户请求分发到不同的服务器上。
  • 问题所在: 如果用户的第一次请求被分发到 Server A,Session 数据被创建并存储在 Server A 的内存中,当用户的下一次请求被负载均衡器分发到 Server B 时,Server B 的内存中没有该用户的 Session 信息,导致用户需要重新登录,或者出现“未登录”等异常状态。

Session 共享的核心目标就是:无论用户的请求被路由到集群中的哪个服务器,该服务器都能获取到该用户完整的 Session 数据。


主流的 Session 共享解决方案

业界有几种成熟的方案来实现 Session 共享,每种方案都有其优缺点和适用场景。

粘性会话

这是最简单的一种实现方式。

Java中如何实现session共享?-图2
(图片来源网络,侵删)
  • 原理: 负载均衡器在分发请求时,会“用户第一次访问时被分配到的服务器,之后该用户的所有请求,只要该服务器可用,都会被分发到同一个服务器上。
  • 优点:
    • 实现简单: 无需修改应用代码,只需在负载均衡器上进行配置。
    • 性能高: Session 数据直接从内存读取,没有网络开销。
  • 缺点:
    • 负载不均: 可能导致某些服务器负载过高,而另一些服务器空闲。
    • 单点故障: 如果某个服务器宕机,该服务器上所有用户的 Session 数据都会丢失,导致这些用户需要重新登录。
    • 扩展性差: 当需要增加服务器时,新服务器的负载无法从旧服务器平滑转移。
  • 适用场景: 对可用性要求不高、流量不大、或者可以容忍少量用户因服务器宕机而重新登录的场景。

集中式存储 Session

这是目前最主流、最推荐的方案,其核心思想是:将 Session 数据与应用服务器分离,存储在一个所有服务器都能访问的集中式存储中。

当需要读写 Session 时,应用服务器会从这个集中式存储中获取或更新数据。

常见的集中式存储介质:

  1. 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 会自动完成这一切。
  2. Memcached

    • 原理: 与 Redis 类似,也是一个高性能的内存键值存储系统。
    • 优点:

      性能极高,简单易用。

    • 缺点:
      • 不支持数据持久化: Memcached 是纯内存的,服务器重启后数据全部丢失,这使得它在 Session 共享场景下不如 Redis 可靠。
      • 功能相对 Redis 较为单一。
    • 适用场景: 对数据持久化要求不高,追求极致性能的场景。
  3. 数据库

    • 原理: 将 Session 数据序列化后存储在关系型数据库(如 MySQL)或 NoSQL 数据库(如 MongoDB)中。
    • 优点:

      数据可靠性高,有成熟的备份和恢复机制。

    • 缺点:
      • 性能瓶颈: 每次读写 Session 都需要进行数据库 I/O 操作,在高并发下会成为性能瓶颈。
      • 增加了数据库服务器的负载。
    • 适用场景: Session 数据量不大,并发量不高,且对数据持久化有极高要求的场景。

无状态应用 + JWT

这是一种更现代、更彻底的解决方案,其思想是不依赖 Session

  • 原理:
    1. 用户登录: 用户登录成功后,服务器根据用户信息生成一个加密的、自包含的 Token(通常是 JWT - JSON Web Token)。
    2. 返回 Token: 服务器将这个 Token 返回给客户端。
    3. 客户端存储: 客户端(通常是浏览器或 App)将 Token 存储起来(如 LocalStorage, Cookie, 内存)。
    4. 后续请求: 客户端在每次后续请求的 Header 中携带这个 Token(Authorization: Bearer <token>)。
    5. 服务器验证: 服务器收到请求后,验证 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.";
        }
    }
}

启动和验证

  1. 确保 Redis 服务正在运行。
  2. 编译并运行这个 Spring Boot 应用。
  3. 为了模拟集群,你可以再运行一个相同的应用,并修改 server.port (8081)。
  4. 使用负载均衡器(如 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 中读取并反序列化的,这一切对开发者都是透明的。

分享:
扫描分享到社交APP
上一篇
下一篇