很多人做分布式登录时,第一次遇到“Session不一致”都会懵:明明单机跑得好好的,一上负载均衡就掉登录态,一会儿要重新登录,一会儿又正常的。我早期做电商项目时也在这个坑里折腾了整整两天,翻了很多资料才把Spring Session + Redis这套方案吃透。这篇博文就把Session核心知识、分布式共享Session的原理,以及Redis落地方案一次性讲清楚,代码可以直接抄,注释也给你写好了。
如果你正在做微服务改造、集群部署,或者面试前想系统梳理Session和分布式登录这块,这篇文章应该能帮你省下不少时间。我会从底层原理讲到代码实现,再到实际部署中的坑,尽量做到“保姆级”。
1. Session到底是个什么东西,为什么分布式下会出问题
1.1 Session的本质:服务器端的“临时档案柜”
Session,翻译过来是“会话”,但我的理解它更像服务器端的一个临时档案柜。HTTP协议本身是无状态的,也就是说服务器默认不记得你上一次请求是谁、干了什么。但业务场景需要记住用户状态,比如你已经登录了、购物车里放了东西,这时就需要一种机制让“无状态”的HTTP变得“有状态”。
Session的做法是:用户第一次访问时,服务器生成一个唯一的会话ID,同时开辟一块存储空间,把用户状态数据存在里面。之后每次请求带上这个ID,服务器通过ID就能找到对应的存储空间。
这里有个关键点要区分清楚:用户端保存的其实只是Session ID,真正的数据永远在服务器端。就好比你去健身房办了一张卡,卡片上只刻了一串编号,真正记录你身高体重、锻炼计划的是健身房前台那台电脑,不是这张卡本身。
我在实际排查问题时发现,很多新手会把Session和Cookie混为一谈,但实际上它们的分工完全不同。Cookie是保存在浏览器端的载体,Session是存在服务器端的数据。两者通过Session ID关联起来,形成完整的会话链路。
1.2 Cookie和Session的配合流程
一个典型的Web登录流程是这样的:
- 用户提交用户名和密码,服务器验证通过。
- 服务器创建Session,把用户信息存入Session对象,同时生成一个Session ID。
- 服务器通过响应头
Set-Cookie把Session ID发给浏览器。 - 浏览器收到后,把这个ID保存在Cookie中。
- 后续每次请求,浏览器自动在请求头
Cookie里带上这个Session ID。 - 服务器根据Session ID找到对应Session,就知道当前请求是谁了。
这个过程在日常开发中太常见了,以至于很多人根本没想过:如果服务器从一台变成了三台,问题就来了。
举个例子,请求第一次打到了服务器A,Session数据存在了A上。下一次请求被负载均衡转发到了服务器B,B上根本没有这个Session,于是服务器判定“用户未登录”,把你踢回登录页。用户就会感觉登录状态“时灵时不灵”,刷新一次可能又好了,这实际上是因为这次请求又被转发回了A。
1.3 单机环境下的Session生命周期
在单机环境下,Session的生命周期还是比较清晰的:
- 创建:首次访问时由服务器创建。
- 存活:默认超时时间内(Tomcat默认30分钟)有效。
- 销毁:超过超时时间没有再次访问,或者主动调用
session.invalidate()销毁。 - 关闭浏览器不销毁:注意,关掉浏览器并不会让服务器端的Session立刻消失,只是浏览器端的Cookie丢了。下次打开浏览器,由于带不上原来的Session ID,服务器会认为是一个新会话。
还有一个容易忽略的细节:Session的超时时间是从“最后一次访问”开始计算的,不是从创建时间开始。也就是说,用户一直在操作,Session就一直续期,不会因为超过了30分钟就被踢下线。这个机制在实现“记住登录状态”时很有用,但在分布式环境下也带来一个问题——Session续期这个操作,必须每次都打到保存Session的那台服务器上才能生效。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分布式场景下,Session共享的几种主流方案
2.1 方案一:Nginx IP Hash粘滞会话
既然问题出在请求被转发到了没有Session的服务器上,那最简单粗暴的思路就是:让同一个用户的请求始终打到同一台服务器上。
Nginx的ip_hash策略就是干这个的,通过哈希算法把同一IP的请求固定到某台后端服务器。但实际用下来会发现几个问题:
- 用户IP可能变化,比如手机从WiFi切到4G/5G,IP变了,哈希结果就变了,Session还是丢。
- 后端服务器重启或扩缩容时,哈希映射关系变化,原本固定的用户会跳到其他机器上。
- 失去了负载均衡的灵活性,某台机器可能因为集中了大量活跃用户而负载过高。
所以这个方案只能算“缓解”,没有从根上解决问题,只适合对会话一致性要求不高的场景。
2.2 方案二:Session复制(集群广播)
这个方案在早期Java Web集群中比较常见。应用服务器(比如Tomcat)配置了集群模式后,一台服务器上Session发生变化,会同步复制到集群中的其他服务器。
好处是代码无侵入,每台服务器都有全量Session数据。坏处也很明显:
- 同步复制有网络开销,服务器越多,广播量越大。
- 数据冗余严重,每台机器都保存所有人的Session。
- 节点之间数据同步存在延迟,极端情况下可能出现刚创建的Session还没同步完,请求就转到了另一台机器。
这个方案在节点数少、并发量低的场景下还能凑合,节点一多就成了性能瓶颈。
2.3 方案三:Redis统一存储,也就是这篇文章的主角
既然粘滞会话不够灵活、Session复制有性能问题,那最优雅的思路就是把Session数据统一存到一个所有服务器都能访问的地方。这个“地方”就是Redis。
所有应用服务器不再本地保存Session数据,而是把Session信息写入Redis。用户请求到达任意一台服务器,服务器都从Redis里查Session,查到就放行,查不到就说明未登录。这样Session就变成了“全局共享”的,服务器本身是无状态的,扩容缩容对用户无感。
选择Redis作为Session存储容器,最主要的原因是它的读写速度极快,毕竟是纯内存操作,单机QPS轻轻松松过万,适合高并发场景。另外Redis的数据结构丰富,字符串、哈希都可以很自然地映射Session的结构。还有一点很重要,Redis的Key可以设置过期时间,正好对应Session的超时机制,不用自己另外开发一套清理逻辑。
2.4 为什么我最终选择了Spring Session + Redis
如果你用的是Spring Boot,可以直接用Spring Session组件,它把“Session数据存到哪里”这个底层逻辑完全抽象掉了。你只需要加一个依赖、改几行配置,原本的HttpSession接口用法完全不变,但底层存储已经悄悄变成了Redis。
这套方案的优势在于:
- 代码无侵入,原来怎么用
HttpSession,现在还是怎么用。 - 成熟的官方生态,Spring官方维护,踩坑率低。
- 天然支持分布式环境,Session数据在Redis里,所有节点共享。
- 支持配置Session超时时间、Cookie策略等细节。
我在生产环境用这套方案扛过日均百万级请求,稳定性没有问题,这也是我把它推荐给你的原因。
3. Redis分布式共享Session的原理与整体设计
3.1 核心机制:Spring Session怎么把HttpSession搬进Redis
如果你第一次接触Spring Session,你可能会好奇:我代码里明明写的session.setAttribute("user", user),这数据怎么就跑到Redis里去了?
秘密在于Spring Session的过滤器(SessionRepositoryFilter)。在请求进入Servlet容器后,这个过滤器先拦截下来,然后把原始的HttpServletRequest包装成一个自定义的请求对象。这个包装后的请求对象,调用getSession()时,不再从内存中创建Session,而是从一个SessionRepository(仓库)里取数据。
这个SessionRepository在Redis方案中就是RedisIndexedSessionRepository。它的工作流程大致是:
- 请求带上来一个名为
SESSION的Cookie(默认名字)。 - Spring Session从Redis里查询这个Session ID对应的数据。
- 找到了,反序列化成Session对象;没找到,就创建一个新的。
- 请求处理完后,如果Session有变化,再把数据写回Redis。
每次写回时,Redis会更新这个Session Key的过期时间,实现“超时续期”的效果。
3.2 Redis中的数据结构与Key设计
Spring Session在Redis里存储时,用的Key设计比较讲究。默认情况下,它会把Session存储为三个部分:
第一个部分是Session本身的哈希结构,Key格式类似spring:session:sessions:<sessionId>,里面存储了你通过setAttribute放入的所有数据,还有creationTime、lastAccessedTime、maxInactiveInterval等元信息。
第二个部分是Session过期时间的索引,Key格式为spring:session:expirations:<时间戳>。这个时间戳是当前时间加上过期时间后,取整到下一分钟的毫秒值。这个索引的作用是让Redis后台能精准扫描到哪些Session即将过期,从而触发清理。
第三个部分是Session ID到Session数据的映射,用于按名称查找,Key格式类似spring:session:sessions:expires:<sessionId>。
我看过不少Redis实例的keys *输出,发现很多同学因为不明白这些Key的用途,以为是什么垃圾数据,直接删掉了,结果导致Session失效。这里提醒一下:这些Key都是Spring Session正常运行需要的,不要手动清理。
3.3 Session数据序列化方式的选择
Session数据写入Redis前,必须经过序列化。Spring Session默认使用JDK序列化,这就导致一个现象:你在Redis Desktop Manager里看到的Session值是一堆\xAC\xED\x00\x05t...之类的乱码。
JDK序列化的好处是兼容性好,坏处是可读性差、存储体积大,而且跨语言不友好。如果你的服务以后可能需要被非Java应用访问,推荐改成JSON序列化。
改序列化方式的方法是通过自定义RedisSerializer的Bean实现。我一般会配置一个GenericJackson2JsonRedisSerializer,这样存入Redis的数据就是可读的JSON。
不过需要特别注意的是,JSON反序列化时存在类型信息丢失的问题,尤其是List、Map这些集合类型,以及多态对象。为了让JSON能正确还原类型,需要确保存入Session的对象类型在反序列化时能被识别。我的做法是:存入Session的DTO尽量做成简单的POJO,避免嵌套太深。
3.4 项目整体架构图
我画一下这套方案在典型微服务环境下的部署形态,方便你对整体有个直观的感受:
用户经过Nginx负载均衡,请求分发到多个应用节点。每个应用节点都不存储Session数据,统一从Redis读取和写入Session。Redis可以是一主多从架构,保证高可用。整个链路中,应用节点完全无状态,任意节点宕机,请求转发到其他节点,用户登录态不受影响。
4. 保姆级实操:Spring Boot + Redis实现共享Session登录
4.1 环境准备与依赖引入
要实现这套方案,你本地的环境需要满足:
- JDK 8及以上
- Maven 3.5及以上
- Redis 5.0及以上
- Spring Boot 2.x或3.x
我用的版本是Spring Boot 2.7.x + Spring Cloud(微服务场景)+ Redis 6.x,这套组合在目前生产环境中覆盖面比较广。
在Maven项目的pom.xml里引入依赖,核心就两个:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
spring-session-data-redis是Spring Session针对Redis的适配器,有了它,Spring Boot会自动配置好SessionRepositoryFilter,并把Session存储切换到Redis。
有一点要提醒,如果你用的是Spring Boot 3.x,包名从javax.servlet变成了jakarta.servlet,但Spring Session的使用方式基本不变。老项目的代码迁移成本也比较低,主要是替换import就行。
4.2 配置文件详解
在application.yml里配置Redis和Session相关参数:
yaml复制spring:
application:
name: user-service
redis:
host: 192.168.1.100
port: 6379
password: 123456
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 2
session:
store-type: redis
timeout: 1800s
redis:
namespace: myapp:session
flush-mode: on_save
几个参数说明一下:
spring.session.timeout设置Session超时时间,单位是秒,这里设置了30分钟。spring.session.redis.namespace是Redis中Key的前缀,多项目共用Redis时可以隔离。flush-mode有两个选项:on_save表示Session有任何修改就立即写入Redis;on_save是默认值,immediate表示每次请求结束立刻保存。实际用默认的on_save就够了。
Redis连接池这里要注意,max-active不要设置太大,否则高并发时会把Redis连接耗尽。我一般根据压测结果调整,16~32之间比较合适。
4.3 登录接口的完整代码实现
下面写一个最典型的登录接口,完整演示从校验到写Session的流程:
java复制@RestController
@RequestMapping("/api/user")
public class UserController {
@Resource
private UserService userService;
@PostMapping("/login")
public Result<String> login(@RequestBody LoginRequest request,
HttpSession session,
HttpServletResponse response) {
// 1. 校验用户名密码
User user = userService.checkLogin(request.getUsername(), request.getPassword());
if (user == null) {
return Result.error("用户名或密码错误");
}
// 2. 将用户信息存入Session
// 注意:存入的是脱敏后的用户对象,不要把密码等敏感信息存进去
session.setAttribute("LOGIN_USER", user.toSafeVO());
// 3. 设置Session最大不活动时间(可选,默认走配置)
session.setMaxInactiveInterval(30 * 60);
return Result.success("登录成功");
}
@GetMapping("/info")
public Result<UserVO> info(HttpSession session) {
UserVO user = (UserVO) session.getAttribute("LOGIN_USER");
if (user == null) {
return Result.error(401, "未登录");
}
return Result.success(user);
}
@PostMapping("/logout")
public Result<String> logout(HttpSession session) {
session.invalidate();
return Result.success("退出成功");
}
}
这段代码里我觉得最需要强调的是这一步:userService.checkLogin()。在我的经验里,这里涉及一个很实际的问题——登录校验时,密码比对一定要用加密后的摘要进行比对,绝不能比对明文。密码在传输层用HTTPS加密,在存储层用BCrypt加盐哈希,登录时把用户输入的密码也用同样算法生成摘要,再和库里存的比对。
再补充一个细节:session.setAttribute("LOGIN_USER", user)这一步,Spring Session在请求结束时会把整个Session对象的快照序列化到Redis里。如果你的User对象没有实现Serializable接口,或者内部有不可序列化的字段,这里就会抛异常。这也是新手很容易踩的坑,建议所有存入Session的DTO都实现Serializable接口,并显式声明serialVersionUID。
4.4 登出逻辑
登出的核心操作是让Session失效。session.invalidate()会做两件事:把Redis里的Session数据删除,同时通过响应头告诉浏览器删除SESSION这个Cookie。
这里有个细节容易被忽略:如果你只调用了session.invalidate(),但是浏览器端Cookie没有删除,那么下次请求浏览器还是会带上这个已经失效的Session ID。Spring Session在Redis里查不到数据,就会认为这是一个新会话,重新创建一个Session。结果就是用户以为自己还“挂着”,实际上每次请求都是新会话。
为了避免这个问题,我一般在登出接口里同时主动清除Cookie:
java复制@PostMapping("/logout")
public Result<String> logout(HttpSession session, HttpServletResponse response) {
session.invalidate();
Cookie cookie = new Cookie("SESSION", null);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
return Result.success("退出成功");
}
4.5 登录拦截器实现
有了登录接口,还需要保护那些只有登录后才能访问的资源。我用HandlerInterceptor实现一个登录拦截器,代码结构比较清晰:
java复制@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 放行预检请求
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
// 检查Session中是否有登录用户
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("LOGIN_USER") != null) {
// 可以在这里做用户信息刷新,比如从数据库重新查一次
return true;
}
// 未登录,返回401状态码
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\"}");
return false;
}
}
注意这里我用了request.getSession(false),传false的意思是:如果当前没有Session,不要创建新的。这样避免了未登录请求也白生成一个Session对象,浪费Redis存储。很多同学默认调用getSession()不传参,或者传true,结果就是每个未登录请求都往Redis写一条数据,还很难排查。
然后注册这个拦截器,同时配置放行路径:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
// 拦截所有请求
.addPathPatterns("/**")
// 放行登录、注册、验证码等接口
.excludePathPatterns("/api/user/login",
"/api/user/register",
"/api/captcha",
"/error");
}
}
这里要特别提醒一点,/error路径一定要放行。否则当拦访问一个不存在的接口时,Spring Boot会转发到/error,而/error又被拦截器拦截,可能产生重复重定向或返回奇怪的错误结构。这个坑我在生产环境踩过,排查了很久才发现是拦截器的锅。
4.6 多端登录的处理:同一账号多点登录
如果你需要做到“同一账号可以多端登录,但每个端独立Session”,这套方案天然就支持。因为每次登录都会生成全新的Session ID,存到Redis里是互相独立的Key,互不影响。
如果你需要做到“同一账号只能在一处登录,后登录的踢掉先登录的”,那就需要额外设计。我的做法是:
用户登录成功后,把Session ID记录到Redis的另一个Key中,比如login:token:<userId>。每次请求进来,拦截器对比当前请求的Session ID是否等于login:token:<userId>的值。如果不等,说明这个账号已经在别处登录了,当前Session强制失效。
这个方案涉及两个Key的管理,代码量不大,但逻辑比较绕。具体落地时要注意:登出时要同时删掉login:token:<userId>,不能只删Session。否则会出现登录已退出,但Redis里的token还占着位置,导致别人登录不了的情况。
5. 生产环境配置与优化细节
5.1 Redis高可用配置
Session一旦存到Redis里,Redis就变成了整个登录体系中的单点。如果Redis挂了,所有用户都登录不了,这是灾难级的故障。所以生产环境必须做高可用。
我推荐的主从哨兵架构是:三台Redis节点,一主两从,配三个Sentinel实例做故障转移。Sentinel会监控主节点状态,主节点宕机后,从节点自动选举升级为新的主节点。
Spring Boot配置哨兵模式:
yaml复制spring:
redis:
sentinel:
master: mymaster
nodes:
- 192.168.1.101:26379
- 192.168.1.102:26379
- 192.168.1.103:26379
如果公司运维能力比较强,也可以用Redis Cluster集群模式。Cluster模式天然支持分片和高可用,但要注意的是,Session数据如果集中在某个分片,其他分片压力会很小,会有资源浪费。
我个人倾向于小规模场景用哨兵,大规模高并发场景用Cluster。核心指标是Session数据量和读写频次。
5.2 Session大小控制
Session里存的数据越多,每次请求序列化/反序列化的开销就越大。我看过有人把用户的完整菜单权限列表、购物车明细、甚至图片Base64都塞到Session里,一个Session几MB,每次请求Redis网络传输都很慢。
经验值是:单条Session的数据量控制在10KB以内。如果超过这个量,建议重新审视哪些数据真的需要放Session。有些可以放Redis的独立Key里按需读取,有些可以直接由前端缓存,有些可以用JWT做无状态化。
存储结构上,如果Session里的数据是扁平的键值对,直接用哈希类型效率最高。Spring Session内部用的就是哈希,不用担心这一步。
5.3 会话超时时间的合理设置
超时时间设置太短,用户体验差,稍微停顿一下就要重新登录;太长,安全风险增大,别人捡到你的Cookie就能一直访问。
分场景来设置:
- 后台管理端:30分钟比较合适。
- 用户前台:2小时比较常见。
- 移动端:用Token机制可能更合适,Session方案更适合浏览器环境。
- 金融类、支付类:建议不超过15分钟,并且混合验证码二次校验。
如果用户处于活跃状态,每次操作都会刷新过期时间,所以设置短一点问题也不大。
5.4 会话固定攻击防护
“会话固定攻击”是一种比较偏门的Web安全威胁,思路是:攻击者先自己获取一个合法的Session ID,然后诱导受害者使用这个Session ID登录。如果系统不更换Session ID,那么登录成功后,攻击者依然可以用这个ID访问受害者的会话。
防护方法很简单:登录成功后,立刻把当前Session作废,重新生成一个新的Session ID。Spring Security框架默认会做这件事,但是如果你的项目没有用Spring Security,而是自己写的登录逻辑,就要手动处理。
代码示例:
java复制// 登录成功后,重建Session避免会话固定攻击
HttpSession oldSession = request.getSession(false);
if (oldSession != null) {
oldSession.invalidate();
}
HttpSession newSession = request.getSession(true);
newSession.setAttribute("LOGIN_USER", user.toSafeVO());
5.5 分布式锁不是Session共享的必要条件
搜索热词里出现了“Redistemplate分布式锁定时任务重复执行”“分布式锁怎么实现”这类问题,这里我顺带说一下。共享Session本身的读写操作,利用的是Spring Session封装好的同步机制,同一时刻对同一个Session ID的并发访问并不会有数据错乱问题,因为Servlet容器对同一个Session对象做了同步锁。
但是在某些极端的并发登录场景下,比如同一个账号同时发来大量登录请求,会导致Redis中创建大量互相覆盖的Session。这种情况下可以考虑加分布式锁,但这不是Session共享方案的必选项,是额外的防刷手段。
分布式锁的典型实现是SET NX EX命令,命令的完整格式是SET key value NX EX seconds,这个命令是原子性的。用RedisTemplate实现的话:
java复制Boolean locked = stringRedisTemplate.opsForValue()
.setIfAbsent("lock:login:" + username, "1", Duration.ofSeconds(5));
if (Boolean.TRUE.equals(locked)) {
try {
// 执行登录逻辑
} finally {
stringRedisTemplate.delete("lock:login:" + username);
}
}
锁的过期时间非常重要,如果业务执行时间超过锁的过期时间,锁自动释放,另一个请求又进来了,锁就失效了。生产上建议用Redisson这样的成熟框架,它默认有看门狗机制,会自动续期,避免锁超时问题。
6. 常见问题排查与性能优化实录
6.1 登录后马上又掉线
典型表现:登录成功跳转首页,刷新一下就回到登录页,或者偶尔能访问,偶尔又掉线。
优先排查几件事:
- 确认请求是否真的带上了Cookie。打开浏览器开发者工具,在Network面板看请求头的
Cookie字段里有没有SESSION=xxxx。 - 确认多台应用服务器的Redis配置是否一致。如果有一台机器没有配Redis,或者配了另一个Redis实例,请求打到这台机器上就会查不到Session。
- 确认应用服务器之间时钟是否一致。这个点容易被忽略——如果服务器时间不同步,Redis过期时间计算会出偏差,极端情况下Session写进去就过期。
我在一次生产事故排查中发现,登录掉线的原因是两台应用服务器上部署的应用配置了不同的spring.session.redis.namespace,一个配了myapp:session,另一个没配,结果两台机器读的Redis Key完全不同,一定会出现间歇性掉线。
6.2 反序列化报错
登录功能刚上线时,一个高频报错是ClassNotFoundException或者SerializationException。这种情况十有八九是Session里存了一个没有实现Serializable接口的对象,或者对象的内部字段不可序列化。
解决方法是:给会话对象实现接口,排除不可序列化字段。用transient关键字修饰不需要序列化的字段,或者用@JsonIgnore注解(如果用的是JSON序列化)。
还有一个容易被忽视的问题:代码上线后,如果Session里的对象类名没变,但包名变了,旧Session反序列化时就可能报ClassNotFoundException,即“反序列化时找不到指定类”。我处理这种问题的通用做法是:上线前主动清理Redis中与会话相关的Key,或者等旧Session自然过期。如果是大版本迭代,建议专门写一个清理任务,把旧的Session Key按前缀批量删除。
6.3 Redis连接池被占满
大促前压测,遇到过Redis连接池超时的告警:RedisConnectionFailureException: Unable to connect to Redis。
排查后发现,压测时并发太高,默认的Redis连接池参数不够用。每个请求都要去Redis读写Session,连接占用时间是整个请求周期。
我的调整策略是:
yaml复制spring:
redis:
lettuce:
pool:
max-active: 32
max-idle: 16
min-idle: 4
max-wait: 1500ms
同时,在业务层面尽量缩短持有Session的时间。比如,有些用户信息可以从Redis独立缓存读取,不一定要塞到Session里来回搬运。这两个方向同时优化,连接池压力能降下来不少。
6.4 Redis中的SESSION过期时间不准
我在Redis Desktop Manager里看到Session的TTL有时候是三分钟、五分钟这种奇怪的值,排查了很久发现是配置和业务代码同时在设置超时时间。
比如application.yml里配置了30分钟,但登录接口又用session.setMaxInactiveInterval(5 * 60)设置了5分钟,后设置的优先级更高。最终表现就是用户5分钟不动就掉线。
这个问题的排查思路很简单:全局搜索代码里所有调用setMaxInactiveInterval的地方,确认业务代码里的设置确实是预期的。如果业务上没有特殊需求,建议只保留配置文件里的超时时间,减少双份配置带来的混乱。
6.5 共享Session与前后端分离的适配
如果你做的是前后端分离项目,而且前端和后端的域名不一样,就会碰到跨域携带Cookie的问题。浏览器同源策略默认不允许跨域请求携带Cookie的。
需要在后端配置CORS时显式允许携带凭证:
java复制@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("https://your-frontend-domain.com");
config.setAllowCredentials(true);
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
注意addAllowedOrigin不要用*,因为allowCredentials(true)和*通配符在浏览器端是冲突的,请求会被拦截。前端fetch或axios也要开启withCredentials: true。
6.6 Session共享不上但Redis正常
有时候Redis是通的,数据也写进去了,但另一台服务器就是读不到。这种情况多半是应用服务器之间代码版本不一致,比如A节点运行的代码是旧版,写入Session用的Attribute名是user,B节点新代码读取的是loginUser,自然读不到。
部署时务必保证所有节点代码版本一致。如果是灰度发布,在窗口期出现Session读不到是正常现象,不必惊慌,等全部发完再观察。
7. 从Session共享到无状态登录的演进思考
最后聊一点我个人对这套方案演进方向的思考。
Redis共享Session解决了集群下的一个核心痛点,但它并不是终极方案。随着业务规模增长,你会逐渐体会到“共享Session”的局限性:
- Session数据存放在服务端,意味着每次请求都要从Redis反序列化一次数据,如果用户基数大,Redis的压力会持续增大。
- 移动端App没有Cookie概念,天然不适合Session方案,JWT这类无状态Token更适合。
- Session是“记住状态”的思路,而现代微服务架构更倾向于“无状态化”设计,把用户状态放到网关层或客户端。
所以我现在做新项目时,会优先考虑JWT + 网关统一的登录态方案。JWT把用户信息经过签名后放在客户端,服务器不需要存储任何会话,天然支持分布式。它的缺点是退出登录比较难处理,Token在到期前无法真正作废,一般需要配合黑名单机制或者缩短Token有效期。
从我实际项目经验来看,两种方案各有适用场景:
- 管理后台、平台类系统,用户量不大,对安全要求高,用Redis共享Session依然是最稳妥的选择。
- C端大流量应用,移动端为主,对扩展性要求高,用JWT无状态Token更合适。
- 也可以混合用:浏览器端用Session方案,移动端用Token方案,通过网关统一鉴权。
回到标题本身,学习Session共享不是让你把它当作唯一答案,而是帮你理解“分布式环境下,如何让一个有状态的协议变得可扩展”这一核心命题。这个思路在后续学习分布式缓存、分布式事务、分布式锁时,都会反复出现。
我在实际使用这套方案时,最大的体会是:Redis共享Session不是银弹,但它给中小团队提供了一个极其平滑的分布式改造路径。至少你不用重写业务代码,不用调整架构,只改几个配置就能让老系统从单机平滑升级到集群。如果你的项目现在正卡在分布式改造这个坎上,不妨先从Session共享开始动手。
