0%

使用springSession完成分布式session

Spring Session 实现分布式 Session 详解:基于 Redis 的配置与原理

在分布式系统中,传统的单机 Session(存储在 Web 容器内存中)会因 “多节点间 Session 不共享” 导致问题(如用户登录后切换节点需重新登录)。Spring Session 提供了优雅的解决方案:通过 HttpServletRequest 包装(Wrapper) 重写 Session 操作逻辑,将 Session 存储到分布式存储(如 Redis、MongoDB)中,实现多节点 Session 共享。从 “核心原理→依赖配置→实战步骤→底层逻辑” 四个维度,彻底讲透 Spring Session 的实现与使用。

分布式 Session 核心痛点与 Spring Session 解决方案

1. 传统单机 Session 的问题

在分布式部署(如多 Tomcat 节点负载均衡)场景下,传统 Session 存在以下问题:

  • Session 不共享:用户请求被负载均衡分发到不同节点,各节点内存中的 Session 独立,导致用户登录状态丢失;
  • Session 持久化差:Session 存储在节点内存中,节点重启后 Session 丢失;
  • 扩展性差:无法支持大规模集群,节点数量增加会导致 Session 同步成本升高。

2. Spring Session 的核心思路

Spring Session 不依赖 Web 容器的 Session 实现,而是通过以下机制实现分布式 Session:

  1. 包装 HttpServletRequest:通过 HttpServletRequestWrapper 重写 getSession() 方法,将 Session 操作委托给自定义的 SessionRepository
  2. 分布式存储SessionRepository 将 Session 数据存储到 Redis、MongoDB 等分布式存储中,而非节点内存;
  3. 过滤器拦截请求:通过 DelegatingFilterProxy 拦截所有请求,将原生 HttpServletRequest 替换为包装后的对象,确保所有 Session 操作都走分布式逻辑。

环境准备:依赖与 Redis 配置

Spring Session 支持多种存储介质,本文以 Redis(最常用)为例,需先配置依赖和 Redis 连接。

1. 依赖配置(Maven)

以下是更稳定的版本组合(适配 Spring 4.x/5.x):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- Spring Session 核心依赖(Redis 存储) -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.7.3</version> <!-- 推荐使用 2.x 版本,兼容 Spring Boot 2.x/3.x -->
</dependency>

<!-- Redis 客户端(Lettuce,Spring Session 默认推荐) -->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.2.6.RELEASE</version> <!-- 与 Spring Session 2.7.x 兼容 -->
</dependency>

<!-- Spring Data Redis(简化 Redis 操作,Spring Session 依赖) -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.7.3</version>
</dependency>

<!-- Jackson(Spring Session 序列化 Session 数据需用到) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.5</version>
</dependency>
依赖说明:
  • spring-session-data-redis:Spring Session 针对 Redis 的实现包;
  • lettuce-core:Redis 客户端(替代老的 jedis,支持异步、线程安全);
  • spring-data-redis:Spring 封装的 Redis 操作工具,简化连接与序列化;
  • jackson-databind:用于将 Session 中的 Java 对象序列化为 JSON 存储到 Redis。

2. Redis 服务准备

确保 Redis 服务已启动,且满足以下条件:

  • Redis 版本 ≥ 2.8.0(支持过期键功能,用于 Session 过期清理);
  • 分布式环境下,所有应用节点需连接同一 Redis 集群 / 实例(确保 Session 共享)。

Spring Session 配置(XML 方式)

核心是 “配置 Redis 连接→配置 Spring Session→配置过滤器拦截请求”。

1. 配置 Redis 连接工厂

Spring Session 通过 RedisConnectionFactory 与 Redis 交互,这里使用 LettuceConnectionFactory(默认推荐):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- 1. Redis 连接工厂(Lettuce) -->
<bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
<!-- Redis 服务器地址 -->
<property name="hostName" value="localhost"/>
<!-- Redis 端口(默认 6379) -->
<property name="port" value="6379"/>
<!-- Redis 密码(若未设置密码,可省略) -->
<property name="password" value=""/>
<!-- Redis 数据库索引(默认 0,可根据需求指定) -->
<property name="database" value="0"/>

<!-- 可选:Lettuce 客户端配置(连接池、超时等) -->
<property name="lettuceClientConfiguration">
<bean class="org.springframework.data.redis.connection.lettuce.LettuceClientConfigurationBuilderCustomizer">
<lambda>
<![CDATA[
builder -> builder
.commandTimeout(java.time.Duration.ofSeconds(5)) // 命令超时时间
.poolConfig(new org.apache.commons.pool2.impl.GenericObjectPoolConfig() {{
setMaxTotal(8); // 连接池最大连接数
setMaxIdle(8); // 连接池最大空闲连接数
setMinIdle(2); // 连接池最小空闲连接数
}})
]]>
</lambda>
</bean>
</property>
</bean>

2. 配置 Spring Session 核心组件

通过 RedisHttpSessionConfiguration 自动配置 Spring Session 所需组件(如 SessionRepository、Session 序列化器):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- 2. Spring Session 核心配置(Redis 存储) -->
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<!-- Session 过期时间(默认 30 分钟,单位:秒) -->
<property name="maxInactiveIntervalInSeconds" value="1800"/>

<!-- Session 数据序列化方式(默认 JdkSerializationRedisSerializer,推荐改为 Jackson2JsonRedisSerializer,可读性更高) -->
<property name="defaultRedisSerializer">
<bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer">
<!-- 配置 Jackson,支持 Java 8 日期类型(如 LocalDateTime) -->
<constructor-arg>
<bean class="com.fasterxml.jackson.databind.ObjectMapper">
<property name="dateFormat">
<bean class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-MM-dd HH:mm:ss"/>
</bean>
</property>
<property name="modules">
<list>
<bean class="com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"/>
</list>
</property>
</bean>
</constructor-arg>
</bean>
</property>

<!-- Redis 键前缀(默认 "spring:session:",避免与其他 Redis 数据冲突) -->
<property name="redisNamespace" value="myapp:session"/>
</bean>
关键配置说明:
  • maxInactiveIntervalInSeconds:Session 过期时间(如 1800 秒 = 30 分钟),过期后 Redis 会自动清理 Session 数据;
  • defaultRedisSerializer:Session 数据序列化器,推荐使用 GenericJackson2JsonRedisSerializer(JSON 格式,可读性高),替代默认的 JdkSerializationRedisSerializer(二进制格式,可读性差);
  • redisNamespace:Redis 键前缀(如 myapp:session),避免不同应用的 Session 数据在 Redis 中冲突。

3. 配置过滤器:拦截请求并包装 HttpServletRequest

这是 Spring Session 生效的核心步骤!通过 DelegatingFilterProxy 拦截所有请求,将原生 HttpServletRequest 替换为 SessionRepositoryRequestWrapper(重写了 getSession() 方法)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 3. Spring Session 过滤器:必须在所有过滤器之前(尤其是 Spring MVC 的 DispatcherServlet 之前) -->
<filter>
<!-- 过滤器名称必须为 "springSessionRepositoryFilter"(Spring Session 固定约定,不可修改) -->
<filter-name>springSessionRepositoryFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 可选:设置为 true 表示过滤器初始化时立即查找 Spring 容器中的 bean(默认 false,延迟查找) -->
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>

<!-- 过滤器映射:拦截所有请求(包括 REQUEST 和 ERROR Dispatcher) -->
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher> <!-- 拦截正常请求 -->
<dispatcher>ERROR</dispatcher> <!-- 拦截错误页面请求 -->
</filter-mapping>
为什么必须用 DelegatingFilterProxy

springSessionRepositoryFilter 没有默认构造器”,这是关键原因:

  • springSessionRepositoryFilter 是 Spring 管理的 Bean(由 RedisHttpSessionConfiguration 自动创建),依赖 SessionRepository 等组件,无法通过 new 直接实例化;
  • DelegatingFilterProxy 是一个 “代理过滤器”,它会从 Spring 容器中查找名为 springSessionRepositoryFilter 的 Bean,并将过滤逻辑委托给该 Bean;
  • 过滤器名称必须为 springSessionRepositoryFilter:这是 Spring Session 的固定约定,RedisHttpSessionConfiguration 会创建同名的过滤器 Bean。

实战使用:像操作普通 Session 一样使用分布式 Session

配置完成后,开发者无需修改任何业务代码,像操作传统 Session 一样操作分布式 Session,Spring Session 会自动将 Session 数据存储到 Redis 中。

1. Controller 中操作 Session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession;

@Controller
public class SessionController {

// 1. 存储 Session 数据(如用户登录后存储用户信息)
@GetMapping("/login")
@ResponseBody
public String login(HttpSession session, String username) {
// 存储用户信息到 Session(Spring Session 自动同步到 Redis)
session.setAttribute("loginUser", username);
// 获取 Session ID(由 Spring Session 生成,而非 Web 容器)
String sessionId = session.getId();
return "登录成功!Session ID:" + sessionId + ",用户名:" + username;
}

// 2. 获取 Session 数据(如判断用户是否登录)
@GetMapping("/user/info")
@ResponseBody
public String getUserInfo(HttpSession session) {
// 从 Session 中获取用户信息(Spring Session 自动从 Redis 读取)
String loginUser = (String) session.getAttribute("loginUser");
if (loginUser == null) {
return "未登录";
}
return "当前登录用户:" + loginUser;
}

// 3. 销毁 Session(如用户退出登录)
@GetMapping("/logout")
@ResponseBody
public String logout(HttpSession session) {
// 销毁 Session(Spring Session 自动删除 Redis 中的 Session 数据)
session.invalidate();
return "退出登录成功";
}
}

2. 验证 Redis 中的 Session 数据

启动应用并访问 http://localhost:8080/login?username=张三,登录成功后,通过 Redis 客户端查看 Session 数据:

1
2
3
4
5
6
7
8
9
# 1. 查看所有 Session 相关的键(前缀为 "myapp:session:"
127.0.0.1:6379> keys myapp:session:*
1) "myapp:session:sessions:expires:3fbe4ab6-b9ff-4065-8c07-b0ed7f02a4db"
2) "myapp:session:expirations:1623743940000"
3) "myapp:session:sessions:3fbe4ab6-b9ff-4065-8c07-b0ed7f02a4db"

# 2. 查看 Session 数据(JSON 格式,可读性高)
127.0.0.1:6379> get myapp:session:sessions:3fbe4ab6-b9ff-4065-8c07-b0ed7f02a4db
"{\"createdTime\":1623743880000,\"lastAccessedTime\":1623743880000,\"maxInactiveInterval\":1800,\"attributes\":{\"loginUser\":\"张三\"},\"isNew\":false,\"isInvalidated\":false}"
键含义说明:
  • myapp:session:sessions:{sessionId}:存储 Session 的核心数据(创建时间、属性、过期时间等);
  • myapp:session:sessions:expires:{sessionId}:用于 Redis 过期监听,确保 Session 过期后及时清理;
  • myapp:session:expirations:{timestamp}:按过期时间分组的 Session ID 集合,用于批量清理过期 Session。

底层原理:Spring Session 如何实现 Session 共享?

核心是 “过滤器拦截→请求包装→Session 委托” 三步流程,以下是简化的原理示意图:

用户请求
DelegatingFilterProxy 拦截
从 Spring 容器获取 springSessionRepositoryFilter
创建 SessionRepositoryRequestWrapper 包装原生 HttpServletRequest
重写 getSession() 方法,委托给 RedisOperationsSessionRepository
RedisOperationsSessionRepository 操作 Redis 存储/读取 Session 数据
将包装后的 Request 传递给后续业务(如 Controller)

关键组件解析

  1. SessionRepositoryRequestWrapper
    • 继承 HttpServletRequestWrapper,重写 getSession()getSession(boolean create) 方法;
    • 当调用 getSession() 时,不访问 Web 容器的内存 Session,而是通过 SessionRepository 从 Redis 读取或创建 Session。
  2. RedisOperationsSessionRepository
    • SessionRepository 的 Redis 实现类,负责 Session 的 CRUD 操作;
    • 序列化 Session 数据并存储到 Redis,同时设置过期时间;
    • 监听 Redis 过期事件,当 Session 过期时清理相关数据。
  3. DelegatingFilterProxy
    • 作为 “桥梁”,将 Web 容器的过滤器生命周期与 Spring 容器的 Bean 管理结合;
    • 确保 springSessionRepositoryFilter 能使用 Spring 容器中的 RedisConnectionFactorySessionRepository 等组件。

分布式环境验证

为确保 Session 共享,可部署两个应用节点(如端口 8080 和 8081),通过负载均衡(如 Nginx)测试:

1. Nginx 配置(负载均衡)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
http {
upstream myapp {
server localhost:8080; # 节点 1
server localhost:8081; # 节点 2
}

server {
listen 80;
server_name localhost;

location / {
proxy_pass http://myapp;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}

2. 测试步骤

  1. 访问 http://localhost/login?username=张三(Nginx 分发到 8080 节点),登录成功;
  2. 刷新页面或访问 http://localhost/user/info(Nginx 可能分发到 8081 节点);
  3. 结果:仍能获取到 loginUser=张三,说明 Session 在两个节点间共享(数据存储在 Redis 中)。

常见问题与解决方案

1. Session 数据未写入 Redis

  • 原因:
    1. 过滤器 springSessionRepositoryFilter 未配置或顺序错误(需在所有过滤器之前);
    2. Redis 连接失败(如地址、端口错误,或 Redis 服务未启动);
    3. 序列化器配置错误(如 Jackson 依赖缺失,导致序列化失败);
  • 解决方案:
    1. 检查 filter-mapping 顺序,确保 springSessionRepositoryFilter 是第一个过滤器;
    2. 查看应用日志,确认 Redis 连接状态(如 RedisConnectionFailedException);
    3. 确保 jackson-databind 依赖已添加,且序列化器配置正确。

2. 多节点 Session 不共享

  • 原因:
    1. 不同节点连接的 Redis 实例不同(需连接同一 Redis);
    2. redisNamespace 配置不同(导致键前缀不同,无法共享数据);
  • 解决方案:
    1. 所有节点的 redisConnectionFactory 配置相同的 Redis 地址;
    2. 确保所有节点的 redisNamespace 一致(如统一为 myapp:session)。

3. Session 过期时间不生效

  • 原因:
    1. maxInactiveIntervalInSeconds 配置错误(单位是秒,而非分钟);
    2. Redis 未启用过期键功能(需 Redis 版本 ≥ 2.8.0);
  • 解决方案:
    1. 确认 maxInactiveIntervalInSeconds 配置(如 30 分钟 = 1800 秒);
    2. 升级 Redis 到 2.8.0 以上版本,或通过 redis-cli config get enable-expire 确认过期功能已启用。

总结与最佳实践

1. 核心优势

  • 无侵入:无需修改业务代码,像操作普通 Session 一样使用;
  • 多存储支持:除 Redis 外,还支持 MongoDB、JDBC 等存储;
  • 高可用:依赖分布式存储(如 Redis 集群),避免单点故障;
  • 扩展性强:支持 Session 事件监听、自定义序列化等高级功能。

2. 最佳实践

  1. Redis 部署
    • 生产环境使用 Redis 集群(如 Redis Cluster),确保高可用;
    • 配置 Redis 持久化(如 RDB + AOF),避免 Session 数据丢失。
  2. 序列化选择
    • 推荐使用 GenericJackson2JsonRedisSerializer(JSON 格式,可读性高,支持复杂对象);
    • 避免使用 JdkSerializationRedisSerializer(二进制格式,可读性差,且依赖类的序列化 ID)。
  3. Session 优化
    • 减少 Session 中存储的数据量(仅存储必要信息,如用户 ID、角色);
    • 合理设置 maxInactiveIntervalInSeconds(如 30 分钟,避免 Session 长期占用 Redis 资源)

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10