系统一旦从单台服务器扩展到多台,最先暴露出来的问题往往不是性能,而是登录状态莫名其妙丢失。用户上一秒还在操作页面,下一秒就被踢回登录页,或者购物车里的商品时有时无,这些现象十有八九和Session在多实例之间的存储方式有关。要彻底解决这个问题,就需要先理解负载均衡环境下会话为什么会失效,再选择合适的方案处理。

为什么会话保持会失效
HTTP本身是无状态协议,服务器靠Session机制记住用户身份。传统单机部署时,Session默认保存在服务器的内存里,客户端只持有JSESSIONID或PHPSESSID这类标识,通过Cookie随请求带上。
问题出在请求进入集群之后。负载均衡器(如Nginx、LVS、云上的SLB)会按照轮询、最小连接数等策略分发请求,同一个用户的第1次请求落在服务器A,Session就创建在A上;第2次请求可能被分到服务器B,而B的内存里根本没有这个Session,自然判定用户未登录。更麻烦的是,如果负载均衡器后端配置了健康检查,某台机器重启或宕机,其内存中所有Session全部丢失,即使请求一直落在同一台机器也无法恢复。
这种失效的本质是:会话状态是有状态数据,而负载均衡追求的是无状态分发。两者天然冲突,解决思路无非两条路:要么想办法让同一用户的请求始终落到同一台机器,要么把Session抽出来集中存储,让所有机器都能访问。
常见会话保持方案的对比与缺陷
IP Hash策略
Nginx中配置ip_hash,根据客户端IP做哈希后固定转发到某台后端机器。配置极简,一行就能生效:
upstream backend {
ip_hash;
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}但它有明显短板。客户端IP在经过代理、运营商NAT出口后可能变化,哈希结果随之改变,会话照样丢失;其次,同一出口IP背后可能是大量用户,哈希分布不均会导致某台服务器压力远大于其他机器;后端增减节点时,哈希环变化会让大量用户重新分配,会话集体失效。生产环境一般只把它当作临时过渡手段。
粘性会话(Sticky Session)
负载均衡器在首次响应时给客户端种一个标识Cookie,后续请求带上这个Cookie,均衡器据此路由到固定的后端。Nginx的sticky模块、云厂商SLB的会话保持功能都属于这类。它的好处是应用代码零改动,Session仍在本地内存,访问速度最快。
缺陷同样致命:某台机器宕机,其上所有用户的Session直接蒸发,用户体验断崖式下跌;后端发布重启时同样面临会话丢失;而且集群规模变化时粘性映射需要重新维护。简单场景可以用,但对可用性要求高的系统不建议依赖它。
Session复制
Tomcat等容器支持集群内Session广播复制,每台机器都持有全量Session副本。机器少时可用,一旦实例数量上来,网络广播开销呈指数级增长,内存占用也翻倍浪费,基本只存在于教科书里,实际生产很少采用。
Redis共享Session方案的实现
真正的根治方案是集中存储:把Session从应用内存中剥离,统一放到Redis里,所有实例读写同一份数据。应用服务器变成完全无状态,任何一台宕机,另一台接管请求后照常读到Session,用户毫无感知。这也是目前主流互联网公司通用的做法。
Spring Boot集成Spring Session
Spring Session提供了对Servlet Session的透明替换,业务代码里照常使用HttpSession,底层自动读写Redis,改动量几乎为零。引入依赖:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>配置文件中指定存储类型和Redis地址:
spring:
session:
store-type: redis
timeout: 30m
redis:
host: 192.168.1.100
port: 6379
password: yourpassword
server:
servlet:
session:
cookie:
name: SESSIONID
domain: example.ipipp.com启动类上加@EnableRedisHttpSession(Spring Boot自动配置开启后也可以省略),此后所有session.setAttribute()都会以Hash结构写入Redis,Key形如spring:session:sessions:xxxx,并通过spring:session:expirations:xxxx管理过期。
序列化方式的选择
Spring Session默认使用JDK序列化,要求存入Session的对象实现Serializable接口,且存储体积大、跨语言不可读。建议替换为JSON序列化,便于排查问题、减少Redis内存占用:
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
// 使用Jackson序列化,替代默认的JDK序列化
return new GenericJackson2JsonRedisSerializer();
}需要注意,JSON反序列化带泛型的复杂对象时可能出现类型丢失,存入的对象结构要尽量简单,复杂对象建议转换成DTO后再放入Session。
需要注意的踩坑点
第一,Session中不要存大对象。每请求都涉及Redis读写,塞进几十KB的集合会显著拉长响应时间,Session只放用户ID、角色这类轻量标识,业务数据放数据库或缓存。
第二,Cookie的domain和path要配置正确。跨子域名场景下,domain要设为父域名,否则各子域的Session无法共享,这是实际部署中最高频的坑。
第三,Redis本身要保证高可用。集中存储意味着Redis成为单点,建议采用哨兵或Cluster模式部署,并做好持久化与监控,避免Redis故障导致全站登录态丢失。
第四,如果之前使用粘性会话过渡,切换到Redis方案后记得关闭均衡器上的会话保持开关,同时灰度发布观察Session读写延迟,确认无异常后再全量上线。
方案选型建议
综合来看,小规模内部系统、发布频率极低时,粘性会话够用且零改造成本;面向公众的互联网应用、容器化弹性伸缩环境(Kubernetes下Pod随时漂移),Redis共享Session几乎是唯一合理选择。它换来的是真正的无状态应用层,扩容缩容、滚动发布、故障转移都不再受Session牵制,这也是微服务架构对会话管理的基本要求。如果后续还有进一步解耦的打算,可以顺势演进到JWT令牌方案,把服务端存储彻底去掉,但两者并非互斥,按业务场景取舍即可。