分布式Session
我用「餐厅点餐+代码实战」帮你彻底搞懂分布式Session,看完不仅能应对面试,还能直接应用到实际开发。先记住这个核心矛盾:多服务员如何记住同一顾客的喜好?
一、从生活场景理解Session的本质
传统单机场景(小餐馆)
- 服务员:Tom(唯一服务员)
- 工作流程:
- 顾客首次点餐 → Tom给纸质会员卡(SessionID)
- Tom把顾客口味记录在自己的笔记本(服务器内存)
- 顾客下次出示会员卡 → Tom查笔记本提供服务
分布式场景(连锁餐厅)
- 服务员:Tom、Jerry、Lucy(多个服务器节点)
- 致命问题:
- 顾客第一次找Tom存了爱吃辣 → 第二次请求被分配到Jerry → Jerry一脸懵逼
二、分布式Session五大解决方案
方案1:黏性会话(Sticky Session)
- 原理:让同一用户的请求始终路由到同一服务器
- 实现:Nginx配置ip_hash
upstream backend {ip_hash; # 像给顾客发固定服务员工牌server 192.168.1.101:8080;server 192.168.1.102:8080;
}
- 优点:零改造成本
- 缺点:
- 服务器宕机 → Session丢失(相当于服务员请假,笔记本被带走)
- 扩容缩容困难(新服务员没有历史记录)
方案2:Session复制(同步广播)
- 原理:所有服务器实时同步Session数据
- 实现:Tomcat配置集群
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
- 优点:任意服务器都可响应
- 缺点:
- 网络带宽消耗大(相当于每天让所有服务员互相抄笔记)
- 不适合大规模集群(超过10个节点性能暴跌)
方案3:集中存储(重点掌握)
- 原理:把Session存到独立存储服务
- 架构:
用户 → 负载均衡 → 任意服务器 → Redis/Memcached
- 代码示例(Spring Session + Redis):
- 添加依赖:
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId> </dependency>
- 配置Redis连接:
@EnableRedisHttpSession public class Config {@Beanpublic LettuceConnectionFactory connectionFactory() {return new LettuceConnectionFactory("redis-host", 6379);} }
- 使用Session与单机完全一致:
@GetMapping("/login") public String login(HttpSession session) {session.setAttribute("user", "码农阿杜"); // 自动存到Redisreturn "登录成功"; }
- 优点:
- 服务器无状态,方便扩容
- 数据持久化,服务器重启不丢失
- 缺点:
- 增加网络延迟(多一次存储访问)
- 需要维护中间件
方案4:客户端存储(JWT方案)
- 原理:把Session数据加密后直接存Cookie
- 代码示例:
// 生成Token String token = Jwts.builder().setSubject("user123").claim("role", "admin").signWith(SignatureAlgorithm.HS256, "secretKey").compact();// 验证Token Claims claims = Jwts.parser().setSigningKey("secretKey").parseClaimsJws(token).getBody();
- 优点:彻底解决服务端存储问题
- 缺点:
- Token无法主动失效(相当于会员卡永久有效)
- 数据大小受Cookie限制
方案5:Session共享协议(Token+数据库)
- 实现流程:
- 登录成功生成token(UUID)
- 把token和用户数据存入数据库
- 每次请求携带token查询数据库
- 代码示例:
// 生成Token String token = UUID.randomUUID().toString(); redisTemplate.opsForValue().set(token, userInfo, 30, TimeUnit.MINUTES);// 拦截器验证 String token = request.getHeader("X-Token"); User user = redisTemplate.opsForValue().get(token); if(user == null) throw new AuthException();
- 优点:灵活控制存储方式
- 缺点:需要手动管理生命周期
三、方案选型决策树
是否需要服务器完全无状态?
├─ 是 → 客户端存储(JWT)
└─ 否 → 是否需要高并发?├─ 是 → 集中存储(Redis)└─ 否 → Session复制(小集群)/黏性会话(稳定集群)
四、面试高频问题
Q1:如何防止Session劫持?
- 防御措施:
- 使用HTTPS防止网络嗅探
- Cookie设置HttpOnly和Secure
- 定期更换SessionID(如每10分钟)
Q2:分布式Session过期时间如何设置?
- 黄金法则:
- 基础过期时间:30分钟
- 滑动过期:每次访问刷新有效期
// Redis示例 redisTemplate.expire(sessionId, 30, TimeUnit.MINUTES);
Q3:Spring Session的实现原理?
- 核心机制:
- 通过Filter替换原生HttpSession
- SessionRepositoryFilter包装请求/响应
- 实际存储委托给Redis等实现
五、生产环境最佳实践
- Session数据最小化:只存必要信息(如userId)
- 读写分离:Redis主从架构提升读取性能
- 降级方案:在Redis故障时自动切换本地缓存
- 监控预警:监控Session存储容量和延迟
一句话总结
分布式Session的本质是:把服务员们的笔记本换成中央档案室! 掌握这个本质,所有解决方案都是围绕存储位置和同步方式的设计权衡。
补充
方案3和方案5确实都涉及外部存储,但它们的核心差异在于数据管理层次和实现方式。我用餐厅工作流程对比帮你彻底分清这对「双胞胎」:
本质区别对比表
方案3:集中存储(Spring Session) | 方案5:Session共享协议(Token+DB) | |
---|---|---|
管理层次 | Web容器层自动管理(对开发者透明) | 应用层手动管理(需要显式编码) |
存储内容 | 完整Session对象(序列化存储) | 自定义业务数据(如用户ID、权限等) |
标识传递 | 自动通过Cookie传递JSESSIONID | 手动通过Header/Param传递自定义Token |
数据读写 | 框架自动完成(如Spring Session Filter拦截读写) | 需要手动编写存取代码 |
典型应用 | 传统Web应用迁移到分布式环境 | 前后端分离架构/APP接口 |
餐厅版对比解释
假设餐厅要记录顾客的「忌口清单」:
方案3:中央档案室(Spring Session)
- 服务员直接说:“忌口清单存总部”
- 每次顾客出示会员卡 → 服务员自动联系总部查清单
- 优势:服务员工作方式不变,只是数据位置换了
方案5:自定义登记表(Token+DB)
- 服务员需要:
- 设计新的登记表格(定义Token格式)
- 手动打电话给总部:“把顾客A的清单给我”
- 更新后主动回传总部:“这是顾客A的新清单”
- 优势:完全掌控数据格式和流程
代码级区别演示
方案3典型代码(无感知):
// 和单机Session用法完全一致
HttpSession session = request.getSession();
session.setAttribute("user", user); // 自动存入Redis
方案5典型代码(全手动):
// 登录时生成并存储
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(token, user.getId(), 30, TimeUnit.MINUTES);// 拦截器中验证
String token = request.getHeader("X-Token");
if(!redisTemplate.hasKey(token)) {throw new UnauthorizedException();
}
Long userId = redisTemplate.opsForValue().get(token);
如何选择?
-
选方案3如果:
- 已有传统Web应用需要改造
- 想保持原有Session API写法
- 不介意依赖Spring生态
-
选方案5如果:
- 全新设计的前后端分离系统
- 需要精细控制Session数据结构
- 追求轻量化/去框架依赖
一句话总结区别
方案3是让框架帮你搬行李的旅行社,方案5是自己打包的自助游
两者最终都到达目的地(完成分布式Session),但过程体验和自由度截然不同。