应用从单机搬到集群,最先暴露的问题往往不是性能,而是登录状态丢失。用户明明刚登录成功,刷新一下页面就被踢回登录页,或者验证码明明填对了却始终提示错误。这些现象背后的根源在于Session的存储方式:默认情况下,Tomcat、Jetty等容器把Session放在各自进程的内存里,集群里的每台服务器各存各的,负载均衡器一旦把下一次请求转发到另一台机器,服务端就认不出这个用户了。要解决这个问题,就需要把Session从应用内存中抽出来,放到一个所有节点都能访问的集中式存储里,Redis凭借高性能和天然的过期机制,成为了最主流的选择。

集群环境下Session处理的三种方案对比
在决定用Redis之前,有必要先了解一下业界常见的几种Session处理思路,理解各自的适用场景,才能明白为什么集中式存储最终胜出。
第一种是粘性会话(Sticky Session),即在负载均衡器上配置策略,让同一个用户的请求始终落到同一台服务器。Nginx上可以通过ip_hash或者基于Cookie的sticky模块实现。这种方式实现简单,应用层几乎不用改代码,但缺点很明显:一旦某台服务器宕机,挂在它上面的所有用户Session全部丢失,而且容易造成流量分布不均,违背了负载均衡的初衷。
第二种是Session复制,利用Tomcat自带的集群广播功能,把每个节点创建的Session同步到其他所有节点。它的优点是容错性好,任意节点宕机不影响用户。但随着节点数量增加,同步的代价呈指数级增长,每台机器都要保存全量的Session副本,内存和网络开销都很可观,一般不建议超过四个节点使用。
第三种就是集中式存储,把Session统一放到外部的存储介质中,应用节点无状态化,随便怎么扩缩容都不影响会话。存储介质可以选数据库、Memcached或Redis,其中Redis支持丰富的数据结构、毫秒级响应、原生的过期淘汰机制,并且本身可以搭建哨兵或集群保证高可用,是综合表现最好的方案。下面所有内容都围绕Redis方案展开。
使用Spring Session Redis接入实战
Java生态下最优雅的接入方式是Spring Session,它通过Servlet规范的Filter机制透明地替换掉默认的HttpSession实现,业务代码完全不用改动,request.getSession()照常使用,背后读写的数据已经变成Redis了。
第一步引入依赖,以Spring Boot项目为例,在pom.xml中添加:
<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.0.1
port: 6379
password: yourpassword
server:
servlet:
session:
cookie:
name: SESSIONID
domain: ipipp.com
配置完成后启动应用,Spring Session会自动注册一个SessionRepositoryFilter,它包裹在真正的业务请求之前,把标准的HttpServletRequest包装成SessionRepositoryRequestWrapper。之后每次调用getSession(),实际操作的就是Redis中key形如spring:session:sessions:xxxx的Hash结构。可以用redis-cli连上去验证,登录后执行keys spring:session:*,能看到会话数据、过期时间索引等key,说明接入成功。
有一点值得注意:Cookie中保存的只是Session ID,真实数据都在Redis里,所以只要集群所有节点连的是同一个Redis实例或同一个Redis集群,会话就天然共享了。域名不同的情况下(比如主站和子域分开部署),记得配置cookie的domain属性让Session ID可以跨子域传递。
序列化方式与过期策略优化
默认情况下Spring Session使用JDK序列化,存进Redis的值是二进制字节流,可读性差、体积大,而且要求所有实体类实现Serializable接口。生产环境强烈建议换成JSON序列化,Redis中的数据肉眼可读,排查问题方便很多。
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
// 使用Jackson进行JSON序列化,替代默认的JDK序列化
return new GenericJackson2JsonRedisSerializer();
}
}
关于Session过期,Redis这边Spring Session采用惰性删除加定时清理的双重机制。读取Session时会顺带检查过期时间,过期则删除;同时还有一个定时任务扫描spring:session:expirations:时间戳这种key来批量清理。需要注意maxInactiveIntervalInSeconds的语义是空闲超时而不是绝对超时,用户持续操作就会一直续期。如果你的业务需要强制重新登录(比如敏感操作30分钟后失效),要在代码里自己维护最后登录时间并主动invalidate。
另外要警惕大Session问题。有些开发者习惯把用户资料、权限列表、购物车等一堆对象往Session里塞,单个Session膨胀到几十KB甚至更大,高并发场景下Redis带宽会被大量会话读写吃掉。原则是Session里只放必要的小数据(用户ID、角色标识),其余信息按需从数据库或缓存中加载。可以定期执行redis-cli --bigkeys或者用MEMORY USAGE命令分析会话key的大小,发现异常及时治理。
常见踩坑点与高可用建议
接入过程中有几个高频问题需要提前规避。第一个是序列化不一致:集群中部分节点用JSON、部分节点还用JDK序列化,读到对方写入的数据就会反序列化失败,报一堆ClassCastException,升级或调整序列化器时必须全量节点同步发布。
第二个是Redis连接抖动导致会话间歇性丢失。Session读写是每次请求都发生的强依赖,Redis一旦响应慢,整个系统请求都会被拖累。建议给Redis客户端配置合理的连接池和超时时间,避免默认的无限等待:
spring:
redis:
lettuce:
pool:
max-active: 50
max-idle: 20
shutdown-timeout: 100ms
timeout: 2000ms
第三个是时钟问题,Session过期依赖服务器时间戳,如果应用服务器之间时钟偏差较大,可能出现会话提前过期或延迟过期的诡异现象,机房部署务必做好NTP时间同步。
最后谈谈高可用架构。单点Redis一旦挂掉,整个集群的所有用户会话全部失效,等于一场事故。生产环境至少要部署Redis哨兵模式实现主从自动切换,Spring Boot中配置spring.redis.sentinel.master和spring.redis.sentinel.nodes即可;规模更大时使用Redis Cluster,Spring Session同样原生支持。再配合Redis的RDB加AOF持久化,即使主从整体重启,会话数据也能最大程度保留,用户无感知地继续使用,这才是完整的会话高可用方案。
Redis Session共享集群会话管理Spring Session修改时间:2026-09-16 18:40:48