广告位 · 页头(后台-模块管理-新建模块后替换此区域)

Spring Boot 如何整合 Spring Session 实现分布式会话管理?

会话管理是几乎所有 Web 应用绕不开的话题。当应用从单机走向集群部署,原本存放在 Tomcat 内存里的 HttpSession 就成了最先暴露的短板:用户在 A 节点完成登录,下一个请求被负载均衡转发到 B 节点,而 B 节点的内存里根本没有这份会话数据,用户只能被强制踢回登录页。Spring Session 正是为解决这类问题而生的,它在 Servlet 规范层面把会话的存储与管理抽离出来,业务代码继续使用标准的 HttpSession 接口,真正的数据则落在 Redis、MongoDB 或关系型数据库中,对开发者几乎零侵入。

Spring Boot 如何整合 Spring Session 实现分布式会话管理?

一、传统 Session 方案在分布式环境下的困境

在展开整合之前,有必要先弄清楚为什么集群环境下的会话会成为问题。HTTP 协议本身是无状态的,服务器靠 Session 机制记住用户,而 Session 的默认实现是各容器自己维护的内存对象。单机部署时这没有任何问题,一旦水平扩容到两个以上实例,Session 就被割裂在各自的 JVM 内存里,彼此不可见。

业界常见的应对思路有三种。第一种是会话粘滞,即在负载均衡层配置 ip_hash 或基于 Cookie 的亲和策略,让同一个用户始终打到同一台机器。这种方式实现简单,但致命缺点是节点宕机时该节点上的所有会话瞬间丢失,而且容易造成流量分布不均。第二种是容器级别的会话复制,Tomcat 集群可以通过组播把 Session 同步到所有节点,但节点数量一多,网络开销和内存占用会急剧膨胀,基本只适合三五个节点的小集群。

第三种也就是目前的主流做法:会话外置存储。把 Session 数据统一放到 Redis 这样的集中式缓存中,所有应用节点读写同一份数据,天然解决了共享问题,还能借助 Redis 的过期机制自动清理无效会话。Spring Session 就是这一思路的标准落地实现,它把对接外部存储、替换原生会话、保持 API 兼容这些脏活全部包办了。

二、Spring Session 的核心工作原理

理解 Spring Session 的关键在于一个过滤器:SessionRepositoryFilter。Spring Boot 自动装配后,这个过滤器会被注册到过滤器链的最前端,拦截每一个进入应用的请求。它的工作方式是典型的装饰者模式:把原始的 HttpServletRequest 包装成 SessionRepositoryRequestWrapper,把 HttpServletResponse 包装成 SessionRepositoryResponseWrapper。

当业务代码调用 request.getSession() 时,拿到的不再是容器原生的 HttpSession,而是包装器返回的 HttpSessionWrapper。这个对象底层对接的是 SessionRepository 接口,具体实现可以是 RedisIndexedSessionRepository、MongoIndexedSessionRepository 等。也就是说,你在代码里写的还是熟悉的 setAttribute、getAttribute,但数据实际读写的是 Redis。

还有一个容易被忽略的细节是 Cookie 的处理。Spring Session 默认不再使用容器原生的 JSESSIONID,而是改用一个名为 SESSION 的 Cookie,其值是会话 ID 经过 Base64 编码后的结果。响应提交时,包装过的 Response 会在 Cookie 中写入这个新名称。理解这一点对排查登录失效问题非常重要,后面踩坑部分会展开说明。

三、整合 Redis 的完整实战步骤

下面以最常见的 Redis 存储为例,演示在 Spring Boot 项目中接入 Spring Session 的全过程。整个过程只需要三步:加依赖、写配置、跑验证,不需要编写任何会话相关的业务代码。

第一步是引入依赖。除了 Spring Web 场景启动器外,需要额外加入 Redis 客户端和 Spring Session 的 Redis 模块:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

第二步在配置文件中指定 Redis 连接信息和会话存储类型。注意 spring.session.store-type 在新版本中可以省略,因为引入了对应依赖后 Spring Boot 会自动推断,但显式声明更利于维护:

spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: yourpassword
  session:
    store-type: redis
    timeout: 30m
server:
  servlet:
    session:
      cookie:
        name: MYSESSIONID

第三步写一个简单的登录接口来验证效果。代码里使用的依然是标准的 HttpSession 接口,完全感知不到底层已经换成 Redis:

@RestController
public class LoginController {
    @PostMapping("/login")
    public String login(HttpSession session, @RequestParam String username) {
        // 数据实际写入了 Redis,而不是 Tomcat 内存
        session.setAttribute("loginUser", username);
        return "登录成功,sessionId=" + session.getId();
    }
    @GetMapping("/user")
    public String getUser(HttpSession session) {
        Object user = session.getAttribute("loginUser");
        return user == null ? "未登录或会话已过期" : "当前登录用户:" + user;
    }
}

启动两个不同端口的实例,比如 8080 和 8081,先访问 8080 的登录接口,再用同一个浏览器访问 8081 的查询接口,会发现登录状态依然有效。此时打开 Redis 客户端执行 keys spring:session:*,能看到以会话 ID 命名的 Hash 结构,里面保存着会话的属性值、创建时间、最后访问时间等信息,这就是跨节点共享的根本原因。

四、序列化方式与会话超时的进阶配置

默认情况下,Spring Session 使用 JDK 原生序列化,存进 Redis 的值是一串二进制字节,肉眼无法辨认,排查问题时非常不友好,而且要求所有会话属性实现 Serializable 接口。生产环境建议换成 JSON 序列化,既提升了可读性,也避免了反序列化版本不一致的隐患。

替换方式是向容器注册一个名为 springSessionDefaultRedisSerializer 的 Bean,Spring Session 启动时会按名称查找并应用它:

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        // 使用 Jackson 完成 JSON 序列化,Redis 中存储明文 JSON
        return new GenericJackson2JsonRedisSerializer();
    }
}

关于会话超时,有几个容易混淆的配置项。@EnableRedisHttpSession 注解上的 maxInactiveIntervalInSeconds 以秒为单位;配置文件里的 spring.session.timeout 支持带单位的时长写法,例如 30m 表示三十分钟。需要特别注意的是,Spring Session 会话的最长不活跃时间由 Redis 的 Key 过期机制控制,Redis 会在每次访问时刷新过期时间,这与容器内存会话的行为一致,但精度取决于 Redis 的过期扫描策略,可能存在几秒到一分钟的误差,对绝大多数业务没有影响。

五、整合过程中的常见坑与应对办法

第一个坑是登录态突然全部失效。原因通常是把旧应用直接切换到 Spring Session 后,用户浏览器里还带着旧的 JSESSIONID,而 Spring Session 读取的是新的 Cookie 名称,自然判定为未登录。解决办法是如上文配置所示,通过 server.servlet.session.cookie.name 把 Cookie 名改回 JSESSIONID,或者干脆接受一次性的全员重新登录。

第二个坑是存入会话的实体类反序列化报错。使用 JSON 序列化后,如果实体类缺少无参构造函数,或者后续修改了包名和字段名,从 Redis 取回数据时就会抛出异常。建议会话中只存放必要的精简字段,比如用户 ID 和用户名,而不是整个用户实体对象,这样既能减小 Redis 占用,也能降低耦合。

第三个坑与 @EnableRedisHttpSession 的自动装配有关。一旦你手动添加了这个注解,Spring Boot 的自动配置就会退位,此时 spring.session.timeout 等配置文件参数可能不再生效,需要以注解属性或自定义 Bean 的方式显式指定。另外,如果项目里同时存在多个 SessionRepository 实现,务必通过 store-type 或条件注解明确使用哪一个,避免启动时的 Bean 冲突。

整体来看,Spring Session 以极低的接入成本解决了分布式环境下的会话共享难题。它保留了标准 Servlet API 的使用习惯,把复杂性封装在过滤器与仓储层之间,配合 Redis 的高性能读写,即使面对大规模集群也能稳定支撑。只要在序列化方案、超时配置和 Cookie 名称这几个细节上多留心,这套方案完全可以作为企业级应用会话管理的默认选择。

广告位 · 文章底部(后台-模块管理替换此区域)