一、基础回顾:Session 与 Cookie 的关系
1.1 无状态的 HTTP
HTTP 本身无状态,服务器不记住请求来自哪个用户。为实现「有状态」会话,引入 Session 和 Cookie 这对搭档。
1.2 Cookie:客户端的小纸条
服务器通过Set-Cookie下发,浏览器保存并在后续请求自动携带。
http
HTTP/1.1 200 OK Set-Cookie: JSESSIONID=ABC123DEF456; Path=/; HttpOnly
关键属性:
| 属性 | 作用 |
|---|---|
| Domain | 生效域名 |
| Path | 生效路径 |
| Max-Age / Expires | 存活时长 |
| HttpOnly | 禁止 JS 读取,防 XSS |
| Secure | 仅 HTTPS 传输 |
| SameSite | 控制跨站携带,防 CSRF |
1.3 Session:服务器端的用户档案
java
HttpSession session = request.getSession(true); session.setAttribute("loginUser", loginUser);关键:Cookie 只存无业务含义的会话 ID,业务数据全在服务器端。客户端无法篡改用户 ID、角色等关键字段。
二、单体架构下的 Session
2.1 登录完整时序
首次访问无会话 ID → 创建 HttpSession,生成 JSESSIONID
登录成功信息写入 Session
Set-Cookie返回浏览器后续请求自动携带
服务器根据 JSESSIONID 在内存 Map 中命中
Tomcat 实现:ConcurrentHashMap管理会话,SecureRandom 生成会话 ID,默认 30 分钟超时。
2.2 内存 Session 的优点
速度快:本地内存操作,纳秒到微秒级
使用简单:容器封装生命周期
无外部依赖:架构简单
2.3 单机方案的潜台词
一切优秀表现都建立在假设之上:同一用户的请求永远落到同一台服务器。集群化后这个假设被打破。
三、集群化后 Session 为什么失效
3.1 登录漂移问题
A、B 两台服务器,轮询分发:
登录请求 → A 节点创建 Session
第二次请求 → B 节点查不到 Session
B 节点认为用户未登录 → 踢回登录页
3.2 三类问题
| 问题 | 说明 |
|---|---|
| 共享问题 | Session 只在创建它的节点,其他节点读不到 |
| 一致性问题 | 复制方案存在时间差,A 修改后 B 读到旧值 |
| 生命周期问题 | 超时、退出、下线在多节点需一致 |
3.3 分布式 Session 的终极目标
用户登录一次后,无论请求落到哪台节点,都能正确识别身份并读写同一份会话数据,且在单点故障、扩容、退出时不出错。
四、分布式 Session 的六大核心难点
4.1 数据一致性
基于复制的方案存在同步时间窗口。强一致方案通常依赖集中式存储,每次读写直接访问共享数据源。
4.2 可用性与容灾
外部存储宕机 → 整个集群登录瘫痪。需要高可用(主从 + 哨兵、集群模式)和降级策略。
4.3 性能与延迟
内存纳秒级 vs 跨网络毫秒级。每个请求都访问 Redis 会明显影响吞吐。需考虑连接池、本地二级缓存、异步续期。
4.4 序列化与版本兼容
对象字段变更可能导致反序列化失败。JDK 序列化体积大、性能差。需选择 JSON、Hessian、Kryo、Protobuf 并设计版本号机制。
4.5 会话过期与清理
Redis TTL 与 Cookie Max-Age 两端时钟不一致。需要识别失效会话并引导重新登录;主动退出时两端同时清理。
4.6 安全防护
Session ID 传输链路更长,风险面更大。需配置 HttpOnly、Secure、SameSite;会话 ID 足够随机;Redis 设置密码。
五、方案一:Session 粘滞
5.1 原理
让同一用户的所有请求固定分发到第一次处理它的节点。负载均衡器根据 Cookie 或 IP 做哈希。
Nginx 配置:
nginx
upstream backend { ip_hash; server 192.168.1.11:8080; server 192.168.1.12:8080; }或基于 Cookie:
nginx
upstream backend { server 192.168.1.11:8080; server 192.168.1.12:8080; sticky name=route path=/; }5.2 优点
实现成本低,应用代码不改,本地内存读写快,适合快速上线。
5.3 缺点与致命伤
| 问题 | 说明 |
|---|---|
| 节点故障 | 粘滞用户集体掉线 |
| 水平扩容 | 哈希规则变化导致会话重分 |
| 负载不均衡 | IP 粘滞在共享出口下导致热点 |
| 网络变化 | 切换网络后 IP 变化失效 |
| 未解决一致性 | 本质是绕开共享问题 |
5.4 适用场景
只适合轻量场景或临时过渡,不建议作为长期方案。
六、方案二:Session 复制
6.1 原理
Session 保存在本地内存,但发生变化时通过节点间通信同步给其他节点。
Tomcat 配置:
xml
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"> <Manager className="org.apache.catalina.ha.session.DeltaManager" expireSessionsOnShutdown="false" notifyListenersOnReplication="true"/> <Channel className="org.apache.catalina.tribes.group.GroupChannel"> <Membership className="org.apache.catalina.tribes.membership.McastService" address="228.0.0.4" port="45564" frequency="500" dropTime="3000"/> <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver" address="auto" port="4000" autoBind="100"/> </Channel> </Cluster>
DeltaManager:增量同步,只同步差异数据
BackupManager:备份式,只有主副本和备份副本
6.2 优点
应用无感知,本地内存读写快,不引入外部存储,中小规模集群部署简单。
6.3 缺点:网络风暴与节点爆炸
核心问题:N 个节点,每次更新同步给 N-1 个节点,消息量与 N² 成正比。
节点从 3 台增加到 10 台时:
同步消息量从 6 条增加到 90 条
网络流量急剧攀升
演变成「网络风暴」
其他问题:
每节点保存全量 Session,内存开销大
节点越多,内存浪费越严重
大集群下不可行
6.4 适用场景
只适合节点数较少(通常不超过 5 台)的中小规模集群。
七、方案三:Session 集中式存储(Redis)
7.1 原理
把 Session 从应用节点剥离,统一存储到 Redis。所有节点共享同一份数据。
7.2 核心实现
key 设计:
text
spring:session:sessions:{sessionId}序列化:推荐 JSON 或 Hessian,避免 JDK 序列化
过期时间:与 Cookie Max-Age 保持一致
代码示例(Spring Session + Redis):
java
@Configuration @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) public class SessionConfig { @Bean public LettuceConnectionFactory connectionFactory() { return new LettuceConnectionFactory(); } }7.3 优点
强一致:所有节点读同一份数据
高可用:Redis 主从 + 哨兵或集群
水平扩展:新增节点无需同步
运维简单:集中式管理
7.4 缺点
| 问题 | 说明 |
|---|---|
| 网络延迟 | 每次请求访问 Redis,毫秒级 |
| 单点风险 | Redis 故障影响全局 |
| 序列化开销 | 需要序列化/反序列化 |
| 存储成本 | 大量 Session 占用内存 |
7.5 优化手段
本地二级缓存:减少 Redis 访问
连接池:复用连接
异步续期:避免每次请求都刷新 TTL
Redis 集群:分担压力
7.6 适用场景
当前最主流的方案,适合绝大多数生产系统。
八、方案四:JWT(无状态令牌)
8.1 原理
把用户信息编码到 Token 中,服务端不存储会话数据。客户端每次请求携带 Token,服务端验签即可识别用户。
JWT 结构:
text
Header.Payload.Signature
示例:
json
{ "alg": "HS256", "typ": "JWT" } . { "sub": "1234567890", "name": "John Doe", "iat": 1516239022, "exp": 1516242622 } . 签名8.2 优点
无状态:服务端不存储,天然支持分布式
跨域友好:可放在 Header 中
自包含:Token 中直接包含用户信息
8.3 缺点
| 问题 | 说明 |
|---|---|
| 无法撤销 | 签发后无法主动失效 |
| 体积大 | 比 Session ID 大很多 |
| 安全性 | 密钥泄露风险,需 HTTPS |
| 刷新复杂 | 需要设计 refresh token 机制 |
8.4 适用场景
适合无状态 API、移动端、第三方开放平台。不适合需要强制下线的场景。
九、方案五:Spring Session 深度解析
9.1 Spring Session 是什么
Spring Session 是 Spring 官方提供的会话管理抽象,支持 Redis、JDBC、Hazelcast 等多种存储。
9.2 核心组件
| 组件 | 职责 |
|---|---|
| SessionRepository | 会话存储抽象 |
| SessionRepositoryFilter | 拦截请求,替换 HttpServletRequest |
| RedisOperationsSessionRepository | Redis 实现 |
| CookieHttpSessionIdResolver | Cookie 解析 |
9.3 工作流程
请求进入 →
SessionRepositoryFilter拦截包装
HttpServletRequest→ 替换getSession()方法从 Cookie 读取 sessionId
从 Redis 加载 Session
业务代码操作 Session → 写回 Redis
9.4 Redis 数据结构
text
spring:session:sessions:{sessionId} → Hash,存储会话属性 spring:session:sessions:expires:{sessionId} → String,TTL 标记 spring:session:expirations:{expireTime} → Set,过期时间索引9.5 配置示例
java
@Configuration @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) public class SessionConfig { @Bean public LettuceConnectionFactory connectionFactory() { return new LettuceConnectionFactory(); } @Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer = new DefaultCookieSerializer(); serializer.setCookieName("SESSION"); serializer.setCookiePath("/"); serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$"); serializer.setUseHttpOnlyCookie(true); serializer.setUseSecureCookie(true); serializer.setSameSite("Lax"); return serializer; } }十、五种方案横向对比
| 维度 | 粘滞 | 复制 | Redis | JWT | Spring Session |
|---|---|---|---|---|---|
| 一致性 | 弱 | 弱 | 强 | 强 | 强 |
| 可用性 | 中 | 中 | 高 | 高 | 高 |
| 扩展性 | 差 | 差 | 好 | 好 | 好 |
| 性能 | 高 | 高 | 中 | 高 | 中 |
| 实现复杂度 | 低 | 中 | 中 | 中 | 低 |
| 单点风险 | 节点 | 节点 | Redis | 无 | Redis |
| 主动下线 | 难 | 难 | 易 | 难 | 易 |
| 适用规模 | 小 | 中小 | 大 | 大 | 大 |
选型建议
| 场景 | 推荐 |
|---|---|
| 小规模、快速上线 | 粘滞 |
| 中小集群、无外部依赖 | 复制 |
| 大规模、主流方案 | Redis + Spring Session |
| 无状态 API、移动端 | JWT |
| 需要强制下线 | Redis + Spring Session |
十一、面试高频追问与回答
11.1 Session 粘滞为什么不可靠?
节点故障、水平扩容、负载不均、网络变化都会导致粘滞失效,且本质上是绕开共享问题而非解决它。
11.2 Session 复制为什么会导致网络风暴?
N 个节点,每次更新同步给 N-1 个节点,消息量与 N² 成正比。节点增多时带宽被同步消耗。
11.3 Redis 存 Session 的 key 怎么设计?
使用命名空间前缀:
spring:session:sessions:{sessionId}存储会话属性用 Hash
过期用独立 key + TTL
避免大 key,控制单个 Session 大小
11.4 Cookie 和 Redis 两端过期时间不一致怎么办?
Redis TTL 略长于 Cookie Max-Age
请求到达时先校验 Redis,不存在则引导重新登录
不要因为 Cookie 存在就认为 Session 有效
11.5 并发读写 Session 会覆盖吗?
会。多个请求同时修改同一 Session,可能后写覆盖先写。解决方案:
加分布式锁
使用乐观锁(版本号)
业务上避免并发修改同一字段
11.6 如何防止 Session 被窃取?
Cookie 配置 HttpOnly、Secure、SameSite
Session ID 足够随机
登录后重新生成 Session ID(防会话固定攻击)
HTTPS 传输
Redis 设置密码 + 网络隔离
11.7 JWT 和 Session 怎么选?
| 维度 | JWT | Session |
|---|---|---|
| 状态 | 无状态 | 有状态 |
| 撤销 | 难 | 易 |
| 存储 | 客户端 | 服务端 |
| 适用 | 无状态 API | 需要强制下线 |
十二、总结
12.1 核心逻辑
HTTP 无状态 → Cookie + Session 实现有状态
集群化 → Session 共享问题暴露
五种方案各有取舍:粘滞简单但不可靠,复制有网络风暴,Redis 主流但需高可用,JWT 无状态但难撤销
Spring Session 是 Redis 方案的成熟封装
12.2 面试答题思路
先讲背景:HTTP 无状态 → Session/Cookie 分工
讲问题:集群化后共享、一致性、生命周期三类问题
给方案:粘滞、复制、Redis、JWT、Spring Session,说明取舍
讲难点:一致性、可用性、性能、序列化、过期、安全
给推荐:结合场景选择,主流是 Redis + Spring Session
补充细节:key 设计、序列化、过期协调、安全配置