资讯动态

Redis替代Session:多节点登录掉线的解决方案

发布时间:2026/10/1 22:33:57 来源:尧图企业网站定制
前几天排查一个线上问题折腾到半夜。项目从单机扩到三台实例之后用户的登录状态开始随机掉落明明在A节点登录成功下一次请求被负载均衡转到B节点又提示未登录用户反复重新登录客服那边的工单都堆成山了。排查到最后锅不在代码而在session的存储位置——session还在Tomcat的本地内存里躺着多节点之间根本没有共享。把session从Tomcat内存搬到Redis让所有节点共享同一份会话数据这套改造在我们团队落地后登录掉线的工单直接清零。这篇文章就把这套Redis代替session的完整业务流程拆开讲一遍从为什么要换、怎么换、换了之后要注意什么到线上排错的经验一次说清楚。适合谁看准备把单机session改造成分布式会话的后端开发、正在做多节点部署但还没解决session共享问题的同学以及想理清Spring Session Redis整套原理的读者。下面全部基于我实际踩过的坑来写。1. 为什么一定要换掉传统session——单机会话方案的三个硬伤在动手改造之前先把为什么换这件事想透。很多团队上来就引入Redis存session但不清楚自己到底在解决什么问题结果改完反而引入新麻烦。传统session方案的硬伤总结下来是三个。1.1 多实例部署时session不同步单体应用时代一个Tomcat扛所有请求session存在本地内存里没有任何问题。一旦服务做水平扩展前面挂一个负载均衡请求会轮流打到不同的服务节点。Tomcat的session默认只保存在各自节点自己的内存里A节点上的session信息B节点根本不知道于是用户第二次请求落到B节点时B节点找不着session只能判定为未登录强制跳回登录页。有人会问那把负载均衡算法改成ip_hash同一个IP固定打到同一个节点不就行了吗这确实能缓解一部分问题但只适合内网或固定出口IP的场景。移动网络下用户的出口IP经常切换而且某个节点一旦宕机挂在这个节点上的所有在线用户session全部丢失所有人被迫重新登录体感非常糟糕。1.2 session复制方案治标不治本早期团队为了解决多节点共享session会采用Tomcat自带的session复制DeltaManager让每个节点之间互相广播同步session数据。看起来解决了问题实际上是把压力从单点转移到了网络。节点一多同步的数据量呈指数增长广播风暴能把内网带宽打满而且全量复制有延迟容易出现A节点刚写入、B节点还是旧数据的情况。更麻烦的是这种复制方案和业务代码耦合很深排查问题的时候非常痛苦。我当时测过一个四节点的集群启动之后内网的广播包明显增多高峰期CPU和带宽都在抖。这种方案本质上不适合大规模集群只能说是小集群环境下的过渡办法。1.3 重启即丢失数据生命周期不可控传统session还有一个很头疼的问题服务重启内存里的session全部清空。你在凌晨发一个版本早晨一到公司就收到一堆登录掉了的反馈。对用户体验来说一次正常的发版就意味着一次集体登出。而且session的销毁完全依赖于容器Tomcat的会话超时机制开发者很难对单个用户的会话做精细化管理比如把某个用户踢下线限制同一账号只能在一处登录这类需求用原生session实现起来非常别扭。我遇到过运营提的需求搞活动期间要把风险账号批量踢下线。用原生session你得遍历所有节点的session还要跨节点清理根本不现实。Redis方案里这就是一条DEL命令的事。所以换Redis的本质是把session的存储位置从进程内存搬到集中式缓存让会话数据变成所有服务节点共享、可统一管理的一份数据。2. Redis会话替代方案的原理与整体架构token无状态化说清楚了痛点再来看替代方案的核心原理。一句话总结服务器端不再用JSESSIONID 容器内存来标记会话改为自定义token Redis保存会话数据每次请求带着token上来服务端去Redis查这份会话数据。2.1 核心思路真正的无状态服务节点改造后业务节点本身不再保存任何会话状态所有状态集中在Redis。这样服务节点变成无状态stateless的伸缩性变得非常好——要扩容就加节点要缩容就摘节点对在线用户没有任何影响。负载均衡不需要配置ip_hash轮询就能跑。整个链路可以这样描述用户登录成功后服务端生成一个唯一的tokenUUID、雪花ID或更复杂的随机串都行以token为key把用户信息、登录时间、过期时间等数据写入Redis服务端把token通过Cookie或请求头返回给前端之后用户每次请求都带上这个token网关或拦截器统一从Redis读取会话信息校验通过后放行会话不存在或已过期直接返回401/重定向登录页这个流程里有个概念需要区分这里的token和JWT不是一回事。JWT是把用户信息签名后直接放在客户端服务端不保存缺点是没办法主动踢人下线Redis方案是token只作为凭证真正的用户数据在服务端所以想控制谁的会话随时可以控制。2.2 Redis的数据类型选型String还是Hash会话数据怎么存我见过很多团队直接用一个字符串把用户对象序列化之后怼进去SET session:token:abc123 user_json_string EX 1800这种做法简单粗暴但有一个问题如果你想只修改这个会话里的某一个字段比如更新一下最后活跃时间或者想知道这个用户上次登录的IP就不得不整串读出来、反序列化、改完再序列化写回浪费一次网络传输和时间。更好的做法是用Hash类型HSET session:token:abc123 userId 10001 username zhangsan loginIp 192.168.1.1 lastActiveTime 2025-01-01 12:00:00 EXPIRE session:token:abc123 1800字段独立存储读取和修改都可以精准操作。Spring Session的Redis实现其实内部底层也是用Hash来存会话属性的。如果你是自己实现我建议优先用Hash后续做会话管理比如只改某个字段、批量查询在线用户都方便很多。2.3 key设计可读、可管理、避免冲突我见过很多糟糕的key设计比如直接用login:123看不出业务归属更不知道是哪类数据。实际项目里建议带业务前缀、模块标识和token三部分项目名:模块:会话标识:token值 示例shop:user:session:a1b2c3d4做Redis key治理的人一定明白前缀清晰和没有前缀在排查问题时效率差好几倍。用Redis Desktop Manager或其他可视化工具看db的时候一眼就能区分哪部分是session数据哪部分是业务缓存数据。2.4 TTL过期时间设计和业务生命周期对齐会话数据必须设置过期时间TTL这是替代方案里最关键的安全边界。TTL太短用户用着用着就被踢下线TTL太长Redis里堆积大量僵尸会话数据内存压力大而且风险窗口长。我一般这样设计TTL基础有效期和原生session的30分钟空闲超时保持一致活跃续期用户每次请求都刷新TTL让持续活跃的用户不掉线绝对超时设置一个硬上限比如7天防止用户无限期在线Spring Session里有一个概念叫SessionRefreshInterval它不会每次请求都更新TTL而是隔一段时间才刷新一次目的是减少Redis的写压力。自己实现的话在请求量大的接口上要注意这个问题。3. 业务流程改造从登录到注销的全生命周期接下来是重头戏把改造后从登录到注销的完整业务流程过一遍每一步该做什么、容易出现什么坑都在这里。3.1 验证码存储session方案迁移的敲门砖很多系统的登录页有个图形验证码原来的做法是把验证码字符串存在session里。改造之后验证码也放进Redis这是一个非常好的切入练习。为什么这么说因为验证码天然是短生命周期数据放Redis里配合TTL逻辑非常优雅用户请求验证码接口后端生成验证码图片生成一个随机UUID作为验证码key把验证码文本和过期时间写入Redis验证码图片和UUID一起返回前端UUID一般放在隐藏域或请求参数用户提交登录时带上UUID后端用UUID从Redis查出验证码文本做比对比对完成后立即删除这个key保证验证码只能用一次// 生成验证码后写入Redis String codeId UUID.randomUUID().toString().replace(-, ); redisTemplate.opsForValue().set(shop:user:captcha: codeId, captchaText, 5, TimeUnit.MINUTES);注意比对完成一定要删除防止验证码被重复利用。这个细节很多人容易漏属于安全漏洞了。3.2 登录成功后的会话创建别再只存一个用户ID了登录校验通过后创建会话的这一步建议把后续所有需要用到的公共信息一次性查出来存进去。我见过有人只存了一个userId然后每个请求都拿userId去数据库查一次用户信息Redis的存在意义就大打折扣了。推荐的做法是把以下内容写入会话Hash字段说明userId用户主键username用户名/昵称avatar头像URLroleId / permissions角色或权限标识如果量不大loginIp登录IP用于风控device登录设备类型loginTime登录时间lastActiveTime最后活跃时间HSET shop:user:session:a1b2c3d4 userId 10001 username 张三 loginIp 1.2.3.4 loginTime 2025-01-01 12:00:00 EXPIRE shop:user:session:a1b2c3d4 1800登录接口响应里返回token前端存到localStorage或Cookie里。这里有一个成熟经验token建议通过响应体返回同时set到Cookie里HttpOnly这样既支持纯API调用也支持浏览器自动携带后端两种方式都能兼容。3.3 请求校验拦截器统一的会话读取与续期会话校验是所有请求的公共逻辑放在拦截器Interceptor或过滤器Filter里统一处理最合适。核心步骤从请求头Authorization或Cookie中取出token解析token得到Redis的key用HGETALL读取会话数据判断是否存在如果session不存在或已过期直接返回401如果存在刷新TTL把用户信息放到ThreadLocal或请求上下文里供业务代码使用每次请求都刷新TTL的话Redis的写操作会比较频繁所以Spring Session设计成了每N分钟刷一次。自己实现时可以考虑做一个阈值判断距离上次刷新时间超过比如5分钟才刷新TTL并发高的场景能省不少Redis请求。还有一个必须注意的坑不要每请求都做一次EXISTS再HGETALL再EXPIRE三步操作链路越长故障面越大。直接用Pipeline或者Lua脚本把查会话续期合并成一次Redis往返性能和可靠性都会提升。实际压测中减少一次往返接口RT能降低好几十毫秒。-- Lua脚本校验并发起续期如果键不存在返回nil local data redis.call(HGETALL, KEYS[1]) if data 0 or #data 0 then return nil end redis.call(EXPIRE, KEYS[1], ARGV[1]) return data3.4 注销、踢人下线与会话过期清理注销很好理解拿到token直接DEL对应的key会话立即失效。DEL shop:user:session:a1b2c3d4这里有个容易被忽略的点前端只删除本地token是不够的必须调用后端注销接口把Redis里的会话删掉否则token还在有效期被别有用心的人拿到照样能用。踢人下线管理员把某个用户强制下线的实现同样简单DEL shop:user:session:该用户的token紧急情况下还有更暴力的办法给每个用户绑定一个会话版本号踢下线时把版本号加一。请求校验时发现版本号对不上就判定会话失效。这样即使token被泄露到多个终端也能一次性全部踢掉。3.5 同账号并发登录控制单点登录的实现思路用Redis能很轻松地实现同一账号只能一个地方在线的常见需求。核心做法登录时把当前会话token和userId做一个关联存一个全局映射shop:user:login:userId - 当前token每次登录如果这个映射已存在就把老token对应的session删掉再写新的# 登录时 GET shop:user:login:10001 # 如果oldToken存在 DEL shop:user:session:oldToken # 写入新映射 SET shop:user:login:10001 a1b2c3d4这样后登录的会把先登录的挤下线实现就和微信PC端/手机端互踢类似。如果不想互踢而想多端共存那就不做这个映射或者按设备类型区分即可。这块灵活度很高属于Redis方案相对原生session的碾压性优势。4. 序列化、持久化与内存控制——线上最容易翻车的角落很多团队做Redis session改造代码跑通了、登录正常了就以为万事大吉。但有三个隐蔽的领域几乎都在上线一段时间后才爆雷提前说清楚。4.1 序列化方式JDK默认序列化是个坑使用Spring Data Redis的时候RedisTemplate默认使用的是JdkSerializationRedisSerializer。它的问题是序列化出来是一坨带类型头的二进制不但体积大、占用Redis内存而且别人用客户端工具查看的时候完全看不懂存的什么排查问题难上加难。更关键的是它会把Java类路径信息写进去类一顿重组或者改名反序列化直接报错。正确的做法是使用GenericJackson2JsonRedisSerializer或Jackson的序列化器把对象序列化成JSON字符串。可读、跨语言、体积小。这里有个通用经验设置RedisTemplate的序列化器要同时改key和value两个部分key建议用StringRedisSerializervalue用JSON序列化器这样key可读性高value也是清晰JSON。4.2 持久化策略对会话数据的影响Redis虽然快但它本质上是内存数据库数据存在内存里。如果没配持久化Redis一重启所有登录用户的session全部消失这是一场比Tomcat重启更可怕的全员登出事故。Redis提供RDB和AOF两种持久化RDB快照按时间间隔把内存数据落盘恢复快但两次快照之间的数据会丢AOF追加写把每条写命令追加到文件丢失数据少但文件大、恢复慢我当时做生产环境配置选择的是每天凌晨定时RDB AOF开启everysec模式。这样即使Redis宕机最多丢失一秒的会话数据基本不影响用户感知。如果你用云厂商的Redis一般默认持久化已经开了但也得确认一下别想当然。4.3 maxmemory与淘汰策略别让session把内存挤爆Redis作为共享缓存不止session在用还可能有大量业务缓存。如果不设置内存上限和淘汰策略Redis总会被塞满。内存满了会发生什么取决于配置noeviction不淘汰任何key写入直接报错整个系统雪崩allkeys-lru从所有key里淘汰最近最少使用的可能导致一些不常用的业务缓存被清掉volatile-ttl优先淘汰TLL短的keysession这种有TTL的数据反而最先被清这里有个安全边界问题需要特别注意如果你依赖Redis session做登录态淘汰策略千万别选优先淘汰TTL短的否则压力上来时最先被挤掉的恰好在线用户的会话用户会集体掉线。推荐的组合是给Redis设置合理maxmemory业务缓存使用allkeys-lru或者给session单独使用一个专门的Redis实例DB/db编号或者独立实例互不影响。实际项目里如果并发量大建议session和业务缓存分开用不同的Redis实例互相之间不会拖累。4.4 敏感数据别进session密码、手机号怎么处理放在Redis里的数据是可被有权限的后台人员看到的。把用户密码放进去是严重错误。正确的做法密码绝不明文存储登录校验只比对密码摘要如果一定要存用户手机号等敏感信息必须脱敏138****8000token本身要足够随机不要让客户端能猜测或解析出用户ID设置Redis访问密码是基础操作。开发环境下可能无所谓生产环境必须设置requirepass并且在内网访问。我见过把Redis端口直接暴露到公网的案例几分钟内就会被扫描工具盯上然后数据被删、被写挖矿脚本非常惨烈。5. 分布式场景下的扩展能力集群、主从与分布式锁把session放进Redis之后下一步要思考的是这个Redis本身怎么保证高可用以及如何借助Redis解决原来session方案做不了的事。5.1 Redis部署形态的演进路线单机Redis用在测试环境没问题生产环境建议按这个路线演进主从复制一主一从主节点挂了从节点顶上。注意只有主节点能写从节点只读Sentinel哨兵在主从之上加哨兵自动故障转移应用无需感知Cluster集群数据分片到多个节点每个节点又有从节点容量可以水平扩展我们的实践是从单机直接跳到Sentinel上线后Redis最大单点风险就消除了。如果你的session数据量很大或者Redis还承载了大量缓存可以考虑直接上Cluster。这里有个容易踩的坑配置了主从/哨兵以后连接池、读写的路由策略都要验证。特别是读操作如果配了读写分离从节点的数据可能会有毫秒级延迟登录后立刻请求会话在从节点可能暂时查不到导致偶发401。这种问题很难排查。稳妥做法是会话数据所有读写都走主节点或者写完后强制读主。等一致性要求不敏感再启用从节点读。5.2 用Redis分布式锁解决重复登录和数据竞争热词里redis分布式锁是高频词在session替代方案中确实和它有关联。举个典型场景做单点登录踢人同一账号并发登录时两个请求同时执行删除老token → 写入新token可能出现交错写入最终映射是旧的token但用户感知却是后登录的没生效。用Redis实现分布式锁来保护这个操作String lockKey shop:user:login:lock: userId; // 尝试加锁带过期时间防止锁泄漏注意setnx expire要原子化用不去重 boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 5, TimeUnit.SECONDS); if (!locked) { // 获取锁失败表示上一登录操作没完成给前端提示稍后重试 } try { // 执行踢人、写新映射、建新会话 } finally { // 释放锁 redisTemplate.delete(lockKey); }用Redis分布式锁有几个细节要注意setnxexpire必须原子执行Redis 2.6.12版本以后可以直接用set命令带NX和EX参数一个命令搞定释放锁时最好比较value如当前请求的唯一标识防止误删别人的锁。Redis分布式锁不是银弹极端场景Redis主节点宕机下仍有并发隐患但对登录这种低并发场景已经足够了。5.3 缓存治理把session纳入整体key治理体系热词里redis缓存治理讲的就是这个点。会话数据不应该成为游离在体系外的一堆脏数据。治理建议Key严格带业务前缀和模块标识前面写过后续用SCAN按前缀统计和定位都很方便定期检查Redis中的key数量和各前缀占比发现session数据异常膨胀立刻排查给session和缓存设置对应的告警内存使用率超过70%就要关注对无用的僵尸会话如7天都没活跃的做定时清理虽然TTL会自动过期但Redis的过期清理是惰性的很多key会一直占着内存直到被再次访问一个真实case我们某个项目上线半年后登录用户量涨了三倍Redis内存告警。排查发现session key有将近200万条很多是已经注销但没主动删除的加上TTL时间设了7天积压严重。后来加了一个定时任务批量清理最后活跃时间超过48小时的session key内存直接降了一半。5.4 Redis不可用的降级预案虽然做了高可用但还是要为Redis全挂了预案。生产环境出现过一次云厂商Redis主节点宕机导致切换我们整体的降级分三级一级限流降级前置在网关层防止请求全部打到后端二级登录接口降级直接提示服务繁忙请稍后再试避免创建大量无效会话三级只读接口降级为游客模式业务缓存读取失败时回源数据库注意降级方案一定要提前演练否则真出事了手忙脚乱。我记得第一次演练的时候Redis一停后端有十几个地方都在catch异常以后静默处理但异常处理逻辑完全不一样导致服务行为不一致花了一整个下午才全部调整好。预设降级开关出状况时一键生效这个很重要。6. 改造过程中的坑与排错思路几个真实case最后这部分把我在实际改造和上线运维中遇到的高频问题整理出来给读者参考。这些坑很多是搜索引擎搜不到答案或者搜到了答案但对应不上自己情况的希望看了能帮你省几天排查时间。6.1 case 1会话超时时间被token和Cookie双重背锅现象用户反馈明明一直在使用系统但一小时后就掉线了。排查发现Redis里的session TTL是30分钟每次请求也确实刷新了TTL。但用户抱怨的一小时恰好是Cookie的过期时间——前端把token放在Cookie里设置了1小时过期服务端的续期根本影响不到Cookie本身的存活。根因是token的存储载体过期时间和Redis里的TTL不一致。Cookie到点了就丢了浏览器不再发送token。解决办法把Cookie过期时间设置成和Redis TTL一致或更长比如7天靠Redis侧的TTL来控制真正的会话有效期。如果是localStorage存储token就不存在这个坑但需要前端配合处理。6.2 case 2could not open hibernate session问题两种session别混淆这个报错在热词里也出现了could not open hibernate session for transaction。很多做session改造的同学看到session两个字就开始慌了以为Redis改造改出了新问题。实际上这个session和用户登录会话完全是两码事——这是Hibernate的持久化会话数据库连接会话报错的含义是获取数据库连接失败、事务无法开启典型原因是数据库连接池耗尽、数据库挂掉或者事务管理器配置错误。排查思路完全不同先看数据库连接池是否满了再看有没有慢SQL占着连接不释放最后看事务配置有没有丢失比如Spring的事务注解失效。我当时排查一个现场时一度怀疑是Redis的问题导致Hibernate连不上数据库其实根本没有关系只是因为这两个东西都叫session误导了好几个人。记住这个报错下次遇到就不会走弯路。6.3 case 3Redis command timed out——连接池和网络的双重问题热词里的io.lettuce.core.RedisCommandTimeoutException非常典型。用Spring Boot默认的Lettuce连接池时高峰期会出现command timed out看起来像Redis性能问题实际上是Lettuce的连接数不够请求在排队超时时间到了还没执行。解决办法有三个方向调整连接池参数maxTotal、maxIdle根据并发量动态调整排查耗时操作有没有大key的读操作比如把整个list塞进session阻塞了连接检查网络Redis和应用服务器跨机房部署时延迟高尽量同机房或同VPC部署大key问题值得强调别把大对象比如用户购物车全部明细放进session否则每次请求都传输大体积数据Redis性能会急剧下降。会话里只放轻量级摘要信息明细数据走业务接口按需加载。6.4 改造后的验证如何判断方案真的生效最后说下验证方法。上线后不能只看能登录就完事建议做这几步多节点验证在两台及以上服务实例配置负载均衡登录后连续请求确认不会掉线故障演练手动重启其中一台实例观察用户会话是否还能继续使用应当完全无感可视化验证用Redis可视化工具查看key是否存在、TTL有没有生效、字段内容是否正常监控告警对Redis命中率、内存使用率、OPS设置告警会话key增长异常能第一时间发现实际上我改造完第一个环境就是用redis-cli monitor盯了几分钟的请求日志看到每个请求都在查询和续期session key心里就踏实了。另外有个小提醒session key的过期删除用的是惰性删除定期抽样删除策略批量注销的接口记得主动DEL不要依赖Redis自动过期尤其在内存紧张的环境下惰性删除会堆积大量过期key白白占着内存。从单机session改造到Redis会话方案整个闭环跑下来最大的体会是提升的不只是登录稳定性更重要的是对会话的控制力从容器黑盒变成了可编程数据。验证码、登录状态、踢人下线、单端互踢、会话续期这些能力全都变成了对Redis的一条条命令清晰、可控、可度量。如果你也在为多服务节点下的session同步头疼不妨按照这套流程从验证码存储这个小模块先切进去小步快跑等跑通了再逐步把登录会话整体迁移过去。这个经验在各个技术栈里是通用的祝顺利。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑