bigywater.tech
Spring · 143

Spring Session:把 Session 放进 Redis

Spring Session 把会话存成了什么?Redis 里的值能直接读吗?超时到底在哪配?属性不可序列化会怎样?每个请求给 Redis 增加多少压力?这篇在 Spring Session 4.0.5 和真实的 Redis 上用两个实例实测。

静默失效
老属性名 spring.session.redis.repository-type=indexed 在 Boot 4 里不生效,没有报错
6 条
带会话 cookie 的请求,不管碰不碰会话,对 Redis 固定发的命令数
60 秒
同时配 server.servlet.session.timeout=100s 和 spring.session.timeout=60s 时会话 TTL

存储结构

实测 · 本机执行输出
  [默认] Redis 里的 key:['spring:session:sessions:<id>']
    会话 hash 的字段:['creationTime', 'lastAccessedTime', 'maxInactiveInterval', 'sessionAttr:k'];各 key 的 TTL(秒):[('spring:session:sessions:<id>', 1800)]
    属性 k 的原始字节前 4 个 = [172, 237, 0, 5](Java 序列化头是 172, 237 即 0xACED);Set-Cookie:名字 SESSION,属性 Path=/; HttpOnly; SameSite=Lax;cookie 值 base64 解码后 = 343a5ad9… 与会话 id 前 8 位 343a5ad9 相同
  [旧属性名 spring.session.redis.repository-type=indexed] Redis 里的 key:['spring:session:sessions:<id>']
    会话 hash 的字段:['creationTime', 'lastAccessedTime', 'maxInactiveInterval', 'sessionAttr:k'];各 key 的 TTL(秒):[('spring:session:sessions:<id>', 1800)]
    属性 k 的原始字节前 4 个 = [172, 237, 0, 5](Java 序列化头是 172, 237 即 0xACED);Set-Cookie:名字 SESSION,属性 Path=/; HttpOnly; SameSite=Lax;cookie 值 base64 解码后 = 19b15dd9… 与会话 id 前 8 位 19b15dd9 相同
  [spring.session.data.redis.repository-type=indexed] Redis 里的 key:['spring:session:expirations:<ts>', 'spring:session:sessions:<id>', 'spring:session:sessions:expires:<id>']
    会话 hash 的字段:['creationTime', 'lastAccessedTime', 'maxInactiveInterval', 'sessionAttr:k'];各 key 的 TTL(秒):[('spring:session:expirations:<ts>', -1), ('spring:session:sessions:<id>', 2100), ('spring:session:sessions:expires:<id>', 1800)]
    属性 k 的原始字节前 4 个 = [172, 237, 0, 5](Java 序列化头是 172, 237 即 0xACED);Set-Cookie:名字 SESSION,属性 Path=/; HttpOnly; SameSite=Lax;cookie 值 base64 解码后 = 00a780a4… 与会话 id 前 8 位 00a780a4 相同

默认一个 hash,indexed 仓库三类 key;值是 Java 序列化。

共享

实测 · 本机执行输出
  在实例 A 上 /set 之后,用同一个 cookie 请求实例 B 的 /get:k=from-A id=65598519
  对照:没有 Spring Session,同样请求实例 B:没有会话(cookie 名 JSESSIONID)

多个实例共享同一个会话。

序列化

实测 · 本机执行输出
  存一个可序列化对象:ok;存一个没有实现 Serializable 的对象:HTTP 500(请求结束写 Redis 时失败)
  之后 GET /get:k=x id=a23ee6d3

不可序列化的属性在请求结束时才失败。

超时

实测 · 本机执行输出
  [默认] 会话 hash 的 TTL = 1800 秒
  [server.servlet.session.timeout=100s] 会话 hash 的 TTL = 100 秒
  [spring.session.timeout=60s] 会话 hash 的 TTL = 60 秒
  [两个都配(server 100s,spring.session 60s)] 会话 hash 的 TTL = 60 秒

spring.session.timeout 优先。

过期

实测 · 本机执行输出
  立刻 GET /get:k=x id=db51536f
  4.5 秒后 Redis 里还有会话 hash:False;用同一个 cookie GET /get:没有会话

默认仓库靠 Redis TTL。

请求开销

实测 · 本机执行输出
  带 cookie 但不碰会话的 /noop:{'hmset': 1, 'pexpireat': 1, 'exists': 2, 'hgetall': 2}(共 6 条)
  读会话属性 /read:{'hmset': 1, 'pexpireat': 1, 'exists': 2, 'hgetall': 2}(共 6 条)
  写会话属性 /set:{'hmset': 1, 'pexpireat': 1, 'exists': 2, 'hgetall': 2}(共 6 条)

每个带 cookie 的请求 6 条命令并续期。

分步演示

Spring Session
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。Redis 是自己起的独立实例(端口 7396);两个应用实例是同一个 jar 的两个进程;请求用 Python 手工带 Cookie;命令数由 INFO commandstats 前后差得到
    • 没有验证:indexed 仓库的键空间通知与清理任务;JSON 序列化(springSessionDefaultRedisSerializer);flush-mode / save-mode 对写入时机的影响;Redis Cluster / Sentinel 下的会话;会话固定攻击与登录后更换会话 id(Spring Security 的 sessionFixation);WebSession(WebFlux)版本;并发请求同时修改同一个会话的覆盖问题;为什么每个请求 exists 与 hgetall 各发两次
    京ICP备2026067502号-1京公网安备11010502063487号