Spring Session + Redis 实现分布式会话共享的完整实践

发布时间:2026/10/19 13:35:59
Spring Session + Redis 实现分布式会话共享的完整实践 接手过一个某平台旧系统的服务拆分改造当时遇到的最头疼问题不是接口性能而是用户登录态在多个服务节点之间互相不认识。用户在首页登录成功跳到订单页马上又变成未登录排查下来才知道是Session存储在了各自节点的内存里负载均衡一转发下一个节点根本不认识这个用户。那段时间我把市面上主流的Session共享方案都过了一遍最后还是选了Spring Session配合Redis这条路。这篇文章就把从原理到落地、从配置到踩坑的完整过程记录下来给正在做多节点部署或者微服务拆分的朋友做个参考。1. 从“单机够用”到“多节点掉线”Session一致性问题的真实场景1.1 传统Session的宿命容器内存在没有引入Spring Session之前我们的Session生命周期完全由Web容器管理。以Tomcat为例用户第一次请求过来容器生成一个sessionId往Set-Cookie里写JSESSIONID同时在后端内存里挂一个HttpSession对象。后续请求只要带上这个Cookie容器就能在本地内存里找到对应的Session。单机部署时这套机制运行得非常好因为所有请求都落在同一个进程里。一旦节点数变成两个、三个问题立刻暴露假设负载均衡器把登录请求转发到了节点A节点A的内存里存了一份Session下一次普通请求被转发到了节点B节点B翻遍自己的内存也找不到对应的Session于是把它当作新访客重新生成一个新的Session返回给浏览器。用户感知就是页面里状态丢失登录态时有时无。这里有一个容易被忽略的细节当节点B生成新Session时浏览器里的JSESSIONID也会被覆盖。所以即使用户刷新页面请求再次回到节点A由于Cookie已经变成节点B生成的新的sessionId节点A依旧找不到旧Session。最终表现就是无论怎么刷新用户都处于“永远登录不进去”的状态。1.2 常见解决思路的优劣对照当时团队内部讨论过几个方案我整理了一张对比表贴出来供参考方案核心思路优点缺陷负载均衡粘性会话通过IP Hash等方式固定后端节点改造量小节点故障后Session丢失扩容缩容要重新Hash数据库存储Session把Session数据持久化到数据库实现简单每次请求读写数据库性能差数据库压力大Redis共享SessionSession统一存到Redis性能较好节点无状态引入额外中间件需要处理序列化与缓存一致性粘性会话看起来最省事但实际上问题很多。如果某个节点宕机这个节点上挂着的所有在线用户的Session会全部失效而且当容器动态扩缩容时Hash结果会剧烈变化Session大量失效。数据库方案在低并发场景下可以跑但并发一上来每个页面请求都要查一次Session表这个开销很难让人接受。1.3 为什么最终选择Spring Session落地最终决定用Spring Session主要是看重它“应用无感”的特性。Spring Session并没有要求我们手写Session存储逻辑而是把HttpSession的操作统一接管业务代码里写的还是request.getSession()、session.setAttribute()这些老API底层存储却被替换成了Redis。这意味着改造期间不需要改动大量业务代码只需引入依赖、调整配置再对Redis数据结构做验证就能逐步替换。另一个考虑是Spring Session具有良好的隔离性。它不会影响Spring容器中其他基于HttpSession的组件适配自己的安全性管理和超时策略也相对独立。对于需要长期维护的项目来说这种低侵入的改造方式比重新造一套Session体系要稳妥得多。2. Spring Session的“无感替换”原理2.1 过滤器如何拦截Session读写Spring Session的核心是一个过滤器这个过滤器在整个请求链的最前端生效。当请求到达应用时过滤器会基于当前的HttpServletRequest和HttpServletResponse创建出两个“包装对象”这两个包装对象把原生的请求、响应对象包在里面之后往下传给Spring MVC或Servlet的时候调用的其实就是被包装过的方法。关键点在于request.getSession()方法被做了手脚。业务代码里调用getSession()时包装对象不再去访问容器内存而是把请求转给SessionRepository让它根据sessionId从Redis中加载数据。如果Redis里没有对应的Session包装对象会根据配置决定是返回null还是新建一个Session。整个读写过程对业务代码完全透明业务方甚至感觉不到Session已经从内存换到了Redis。2.2 装饰模式下ServletRequest行为变化装饰器模式是理解Spring Session的关键。我举一个具体的例子原生ServletRequest的getSession(boolean create)在被包装后会先判断当前请求SessionRepository中是否已存在有效记录。如果用户带上了一个Cookie但Cookie里的sessionId在Redis中不存在那getSession(true)会创建新Session并写入Redis同时通过响应里的Cookie回写给浏览器。这里有个细节经常让人困惑Spring Session自己的Cookie默认名称是SESSION而不是JSESSIONID。如果不做额外配置浏览器里会同时存在两个不同的会话Cookie这会导致某些场景下会话ID混乱。我们在配置时可以把Cookie名称统一调整为熟悉的JSESSIONID也可以结合自己系统的前端策略来决定这个后面章节会详细讲。另一个行为变化是Session失效处理。业务代码调用session.invalidate()时包装对象会调用SessionRepository的deleteById方法把Redis中对应的Hash结构删除同时清理Cookie。这个操作是即时生效的不像之前容器内存中还要等待GC回收。2.3 Redis数据结构和TTL机制Spring Session在Redis中的存储结构本质上是一个Hash。默认的key格式是spring:session:sessions: sessionId里面存的字段包括creationTime、lastAccessedTime、maxInactiveInterval以及业务方自定义的attribute等。除此之外Spring Session还会维护一个名为spring:session:expirations的Sorted Set结构用来记录会话的过期时刻从而实现后台定时清理。TTL机制是Spring Session实现会话超时的核心。每次请求访问Session时系统会更新lastAccessedTime同时根据maxInactiveInterval重新计算过期时间。Redis的expire和过期Sorted Set两种策略共同协作既保证了Redis从存储层自动淘汰过期数据又保证了Spring Session自身能主动清理过期Session。这套机制比单靠Redis过期时间要严谨因为它还考虑到了清理被过期Session引用的相关属性。3. 环境搭建与最小可运行配置3.1 依赖引入与版本对齐搭建环境的第一步是引入依赖。如果项目使用的是Spring Boot直接添加spring-boot-starter-data-redis和spring-session-data-redis两个依赖即可。需要注意版本对齐问题尤其是Spring Boot 3.x之后底层的Redis客户端已经从Jedis切换为Lettuce某些老项目里如果同时存在Jedis的旧依赖很容易出现连接池冲突。我当时使用的是Spring Boot 2.7.x版本依赖配置大致如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.session/groupId artifactIdspring-session-data-redis/artifactId /dependency如果用的是Spring Boot 3.x基本依赖不变但要注意Spring Session版本已经升级到3.x部分配置项的名称和默认值有调整。最好把依赖的版本交给Spring Boot的依赖管理BOM统一控制不要手动指定免得出现版本不匹配导致的方法签名错误。3.2 连接配置与EnableRedisHttpSession细节依赖引入之后配置Redis连接信息spring: redis: host: 127.0.0.1 port: 6379 password: timeout: 3000ms在启动类或者配置类上加上EnableRedisHttpSession注解SpringBootApplication EnableRedisHttpSession(maximumInactiveIntervalInSeconds 1800) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }maximumInactiveIntervalInSeconds是会话最大空闲时间这里设置成30分钟业务方可以根据自己的安全要求调整。这个注解默认使用RedisIndexedSessionRepository作为SessionRepository的实现它把会话数据以Hash结构存储同时维护过期索引。3.3 验证Session真的进了Redis配置完成之后可以做一个非常简单的验证启动一个接口往Session里放一个用户对象然后打开Redis客户端看key是否存在。我用Redis CLI来查看127.0.0.1:6379 keys spring:session*正常情况下会看到两类keyspring:session:sessions:xxxx-xxxx-xxxxspring:session:expirations:yyyyyyyy再用HGETALL查看Hash内容127.0.0.1:6379 HGETALL spring:session:sessions:xxxx-xxxx-xxxx能看到creationTime、lastAccessedTime、maxInactiveInterval这些元数据如果业务里设置了attributeRedis里也会出现对应的字段。这个验证很关键它确认了业务代码使用的Session确实已经落到了Redis。4. Session生命周期与Cookie行为容易忽视的隐性配置雷区4.1 超时时间有多个入口关于Session超时很多开发者会以为只配置一个地方就够了实际上这里存在多个入口优先级还不太一样。首先是EnableRedisHttpSession里的maximumInactiveIntervalInSeconds这是Spring Session层面默认的最大空闲时间其次是Spring Boot里的spring.session.timeout这个配置在某些版本中会影响Session的有效期再往下还有Web容器的session-timeout设置虽然被Spring Session接管后这个值不一定生效但在老项目迁移时依然容易造成迷惑。从实际操作来看最稳妥的方式是统一通过Spring Session的配置来控制超时时间同时把application配置里其他容易产生歧义的会话超时配置注释掉。如果项目中有部分接口需要更长的会话时间可以通过自定义CookieSerializer或重写SessionRepository的方法来单独控制不要试图用同一套配置去覆盖所有场景。4.2 Cookie默认名与JSESSIONID的差异前面提到过Spring Session默认的Cookie名是SESSION。这本身没什么问题但对于迁移项目就需要注意了旧的系统使用JSESSIONID前端在登录成功后往往存的是旧的Cookie如果后端切换成Spring Session而前端没有适配新Cookie名那么所有用户都会被当成新访客。解决方案是在配置类中自定义一个CookieSerializerBean public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer new DefaultCookieSerializer(); serializer.setCookieName(JSESSIONID); serializer.setCookiePath(/); serializer.setCookieMaxAge(30 * 60); return serializer; }cookieMaxAge我这里设置成和Session超时一致可以避免浏览器端的Cookie在Redis里的Session还活着时就被提前清掉。需要留意setCookiePath如果应用跑在某个子路径下不设置路径就会导致Cookie在各个路径之间不可共享。4.3 Session创建时机与节省序列化的开销还有一个容易忽略的点是Session的创建时机。如果业务代码在登录接口里调用了request.getSession(true)Session会被立即创建并写入Redis但很多登录流程实际上会先通过业务逻辑校验再在成功后把用户信息塞进Session。比较合理的做法是在确认为合法请求后才通过request.getSession()获取或创建Session避免无效请求预先占用Redis空间。另外不是每一个请求都需要访问Session。有些页面是完全公开的比如静态资源、开放API等。如果过滤器对每个请求都去Redis检查一遍Session会额外增加一次网络IO和序列化开销。Spring Session本身有策略可以关闭部分URL的Session追踪也可以通过自定义过滤器对公开路径进行跳过。量大的时候这个优化能省下不少Redis请求量。5. 序列化策略的选择从“能跑”到“能在Redis里看懂数据”5.1 JDK序列化的乱码问题刚接入Spring Session时项目直接用了默认的序列化方式Session里的业务对象确实是存进Redis了但打开Redis里一看全都是\xAC\xED\x00\x05t...这种二进制乱码。原因很简单Spring Session默认使用JDK自带的序列化机制对象变成了一串Java特有的字节流不仅肉眼无法阅读而且只有Java应用才能反序列化。JDK序列化还有一个潜在问题被序列化的对象必须实现java.io.Serializable接口而且一旦业务对象的属性结构发生变化旧数据可能无法被反序列化。这意味着上线之后如果改了一个字段名或类型可能导致之前存的Session读取失败用户会被强制踢下线。5.2 用Jackson/自定义序列化器的注意点为了解决乱码问题业内通用的做法是改成JSON序列化器。Spring Data Redis针对RedisTemplate和Session存储都提供了对应序列化器配置入口。我们可以注册一个RedisSerializer注册Bean的方式如下Bean public RedisSerializerObject springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); }注意这里Bean的名称必须是springSessionDefaultRedisSerializerSpring Session在创建Repository时才会优先注入这个序列化器。用了JSON序列化之后Redis里的数据变成明文JSON排查问题时直接用HGETALL就能读懂用户Session内容方便很多。5.3 一个实际踩坑某字段反序列化后丢失JSON序列化也不是没有坑。有一次我们把一个用户对象放在Session里这个对象有一个LocalDateTime类型的字段。上线后第二天有用户反馈登录状态异常通过Redis查看Session数据发现JSON里LocalDateTime字段变成了一段带数组格式时间的表示。数据本身还在但反序列化回对象之后字段值变成null。原因是GenericJackson2JsonRedisSerializer默认对Java 8时间类型的支持不完整。解决方案是在注册序列化器之前引入JSR310模块或者改用一个自定义的ObjectMapper为LocalDateTime配置全局的序列化与反序列化格式。ObjectMapper mapper new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); GenericJackson2JsonRedisSerializer serializer new GenericJackson2JsonRedisSerializer(mapper);这种字段丢失问题在测试环境不容易发现因为测试数据往往比较固定不会覆盖所有时间字段场景。建议上线前把Session里的典型对象全部造一遍数据写入后立即读出对比字段值是否完整。6. 多节点部署验证与故障排查6.1 本地双实例验证步骤验证Session共享最直接的方式是本地启动两个不同端口的服务实例比如8080和8081两个实例连同一个Redis然后通过浏览器分别访问两个端口的页面。正常情况下用户从8080端口登录带着Cookie去请求8081端口依然能读到登录态。如果是在本地模拟需要注意Cookie不能跨端口混淆。最简单的方法是用curl手动指定Cookiecurl -v -c cookies.txt http://127.0.0.1:8080/user/login curl -v -b cookies.txt http://127.0.0.1:8081/user/info只要第二个请求能正确返回用户信息说明Session已经在两个节点间共享。如果拿不到数据先用HGETALL检查Redis中是否存在对应的Session记录。若Redis里有记录但接口读取不到大概率是Cookie未带上或者Cookie的path配置不一致。6.2 登录后立刻掉线的排查路线上线后我遇到频率比较高的故障是“登录成功跳转两次后掉线”。排查这类问题有一定的固定路线先确认浏览器Cookie里的name和value是否存在再看这个sessionId在Redis里是否存在最后看返回给浏览器的Set-Cookie是否覆盖了原有Cookie。第二条链路里有一个常见陷阱多个服务集群如果各自制定了不同的CookieName或CookiePath那么一次请求可能写入两个不同类型的Cookie。用户带上旧Cookie到新节点新旧Cookie互相干扰导致Session读取出现随机性。解决方法是全局统一Cookie配置并且用环境变量或配置中心管理避免各节点配置漂移。6.3 上线前要做的Redis安全与监控检查Session数据敏感性比较高上线前Redis的安全检查不能省。至少需要确认Redis没有暴露在公网设置了访问密码并且关闭了危险命令的自动执行。如果应用和Redis部署在同一内网建议参考基线安全配置限制来源IP连接Redis端口。监控方面Spring Boot Actuator的health端点中已经包含Redis健康检查。除了基础健康状态我还会额外关注Redis的平均操作耗时和连接数因为Session读取会出现在用户每一次请求的链路里一旦Redis出现阻塞用户会有明显的感知。我们之前遇到过慢查询导致个别请求的Session读取超过1秒整体接口耗时被拉高。排查之后发现是某个大字段的JSON反序列化过于复杂后来对这个字段做了精简存储问题才解决。另外建议对Session过期数量、Session占比内存大小做个简单的定期巡检。Spring Session在Redis里产生的key数量级会跟着在线用户数线性增长如果用户基数很大要注意Redis内存容量规划设置合适的淘汰策略和持久化策略。Session虽然允许一定程度的丢失但尽量不要因为Redis内存满了触发key淘汰导致全体用户被下线。写在最后的经验小结从最初被Session一致性搞得焦头烂额到最终把Spring Session和Redis稳定跑起来整个过程最深的体会是技术选型其实不难关键是理解数据到底存在哪里、什么时候被创建、什么时候被清理。Spring Session把会话数据从容器内存里抽离出来本质上是把Session当成了一种可扩展的存储数据来看待这让多节点部署和微服务拆分都变得轻盈很多。如果你当前也正在被多实例登录态问题困扰建议先按本文的验证步骤做一次最小化复现再根据自己业务的用户对象和响应时间要求调整超时、序列化和Cookie设置。这样落地的成功率会高很多也不会被一些莫名其妙的配置问题挡住脚步。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询