当前位置: 首页 > news >正文

Session粘滞性问题->Redis实现session共享

文章目录

  • 一、Session
    • 1.Session 的存储位置
    • 2.Session的生命周期
    • 3.Session的粘滞性问题
  • 二、解决方案:tomcat+redis+nginx 实现session共享
    • 1.登录校验
    • 2.拦截器设置

一、Session

当用户首次访问时,服务器为其创建一个唯一的 Session 对象,并生成一个 SessionId 写回客户端(通常通过 Cookie)。此后,客户端每次请求都会自动带上这个 SessionId,服务器便可根据它找到对应用户的会话,从而实现“记住”用户状态。

1.Session 的存储位置

在传统的 Java Web 应用中,Tomcat 默认把 Session 对象存放在 JVM 堆内存中。内部实际上是一个 ConcurrentHashMap,Key 为 SessionId,Value 为 HttpSession 对象,其中存储了用户相关的属性。

2.Session的生命周期

Session 在以下情况会被销毁:

  • 超过设定的过期时间(默认 30 分钟,可通过 web.xml 或 server.xml 中的 session-timeout 配置)。
  • 调用 session.invalidate() 方法手动销毁。
  • Tomcat 重启或宕机——由于数据只存在内存,重启后全部丢失。

3.Session的粘滞性问题

因为每个 Tomcat 实例都在自己的内存中维护 Session,一旦引入负载均衡和多个实例,就会出现经典的 Session 共享问题。

假设你有多台 Tomcat,前端使用 Nginx 轮询分发请求

  • 用户第一次请求 → Nginx → Tomcat 1 → 登录成功,Session 存在 Tomcat 1 的内存中
  • 用户第二次请求被分到 Tomcat 2,Tomcat 2 的内存里没有这个 Session,就会认为用户未登录。

这就是 Session 粘滞性问题。

二、解决方案:tomcat+redis+nginx 实现session共享

业界主流的方案是使用 Redis 作为集中式 Session 存储器。所有 Tomcat 实例在需要读写 Session 时都去访问同一个 Redis,从而达到会话共享。

1.登录校验

对于下面的验证码登录实践案例中,原本 session.setAttribute(“code”, code) 被替换为 Redis 写入。这样无论请求被 Nginx 分发到哪台机器,校验验证码时都从同一个 Redis 读取。

此时Redis中存储的数据如下,code对应验证码校验,token存储对应的用户 UserDTO

@OverridepublicResultsendCode(Stringphone,HttpSessionsession){//校验手机号if(RegexUtils.isPhoneInvalid(phone)){//手机号不符合returnResult.fail("手机号格式错误");}//手机号符合,生成验证码Stringcode=RandomUtil.randomNumbers(6);/*//保存验证码到session session.setAttribute("code", code);*///保存验证码到redisstringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL,TimeUnit.MINUTES);//发送验证码log.debug("发送验证码成功,验证码:{}",code);//返回okreturnResult.ok();}
publicResultlogin(LoginFormDTOloginForm,HttpSessionsession){//校验手机号Stringphone=loginForm.getPhone();if(RegexUtils.isPhoneInvalid(phone)){//手机号不符合returnResult.fail("手机号格式错误");}//从redis中获取验证码 校验验证码/* Object cacheCode = session.getAttribute("code");*/StringcacheCode=stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);Stringcode=loginForm.getCode();if(cacheCode==null||!cacheCode.equals(code)){//不一致 报错returnResult.fail("验证码错误");}//一致 根据手机号查询用户Useruser=baseMapper.selectOne(newLambdaQueryWrapper<User>().eq(User::getPhone,phone));//判断用户是否存在if(user==null){//不存在 创建新用户user=createUserWithPhone(phone);}/*//保存用户信息到session session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));*///生成tokenStringtoken=UUID.randomUUID().toString(true);//userDTO转mapUserDTOuserDTO=BeanUtil.copyProperties(user,UserDTO.class);Map<String,Object>map=BeanUtil.beanToMap(userDTO,newHashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((name,value)->value.toString()));//保存用户信息到redisstringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,map);//设置过期时间stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);returnResult.ok(token);}

2.拦截器设置

  • 第一个拦截器RefreshTokenInterceptor的作用:从请求头取 Token → 查 Redis → 得到用户数据 → 存入 ThreadLocal,刷新 Redis 中该 Token 的过期时间,请求结束后,清理 ThreadLocal。
  • 第二个拦截器LoginInterceptor的作用:检查 ThreadLocal 里有没有用户数据。有就放行,没有就返回 401

ThreadLocal相当于备份了Redis中的UserDTO数据,对于该次请求(该次线程)ThreadLocal中的数据存在,方便进行显示,不用再去Redis中查看了

两个拦截器为了解决Redis中token的过期时间问题,如果只设置一个拦截器,用户每次访问需要UserDTO登录校验的界面,会刷新token过期时间,但是如果访问商家页面这种不需要登录校验,此时token的过期时间就没有刷新

@ConfigurationpublicclassMvcConfigimplementsWebMvcConfigurer{@ResourceprivateStringRedisTemplatestringRedisTemplate;@OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){//登陆拦截器registry.addInterceptor(newLoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/upload/**","/voucher/**").order(1);//Token续命拦截器registry.addInterceptor(newRefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}}
publicclassLoginInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{//获取用户if(UserHolder.getUser()==null){//不存在用户 拦截response.setStatus(401);returnfalse;}//存在用户放行returntrue;}}
publicclassRefreshTokenInterceptorimplementsHandlerInterceptor{privatefinalStringRedisTemplatestringRedisTemplate;publicRefreshTokenInterceptor(StringRedisTemplatestringRedisTemplate){this.stringRedisTemplate=stringRedisTemplate;}@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{//从请求头中获取tokenStringtoken=request.getHeader("authorization");if(StringUtils.isEmpty(token)){//不存在tokenreturntrue;}//从redis中获取用户Map<Object,Object>userMap=stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY+token);//用户不存在if(userMap.isEmpty()){returntrue;}//hash转UserDTO存入ThreadLocalUserHolder.saveUser(BeanUtil.fillBeanWithMap(userMap,newUserDTO(),false));//token续命stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES);returntrue;}@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{UserHolder.removeUser();}}
http://www.jsqmd.com/news/724314/

相关文章:

  • 如何快速上手数字电路设计:Logisim-Evolution 完整实战指南
  • python学习笔记 | 8.1、函数式编程-高阶函数
  • 从一站式采购到前店后仓,乐居如何重塑汤原的“家”与“业”?
  • MCP协议服务健康检查工具mcp-checkup的设计与实战
  • 旧物回收系统源码 – go语言版
  • 开源知识管理工具Costea:基于间隔重复与知识图谱构建第二大脑
  • 大连做金融相关法律服务的品牌律所推荐,哪家更靠谱? - 工业推荐榜
  • 海康录像机提示“已达到通道资源添加上限”是什么原因---远程维修服务日记
  • 0.43%入选门槛6重筛选:2026年上海家装七强全维度标杆企业重磅揭晓 - 资讯焦点
  • 3步彻底解决Zotero中文文献管理难题:茉莉花插件完全指南
  • uboot学习笔记
  • 不止于Dotplot:解锁MUMmer套件的隐藏技能,从SNP检测到基因组结构变异分析
  • 猫抓cat-catch终极指南:浏览器资源嗅探神器让网页资源下载如此简单
  • 2025—2026年度上海装修市场深度调研:5家靠谱装企全解析 - 资讯焦点
  • 终极指南:如何免费永久备份微信聊天记录到电脑
  • 如何永久备份微信聊天记录?免费开源工具WeChatMsg终极使用指南
  • Docker 化 Java 应用与镜像瘦身完全指南
  • MiGPT:让小爱音箱变身智能AI语音助手,开启智能家居新体验
  • 基于Simulink的燃料电池-锂电池混合动力能量流管理​
  • 学习 C++能带给我们什么
  • LeetCode 哈希表搜索题解
  • VMware Unlocker:5步解锁VMware的macOS虚拟机支持
  • 【西瓜带你学Kafka | 第一期】Kafka的架构设计、核心组件、优缺点、常见应用场景(文含图解)
  • 深入解读C++中的指针变量
  • 猫抓cat-catch:浏览器资源嗅探的终极解决方案,让网页资源捕获变得高效智能
  • 数字线程:数字孪生的“中枢神经”,如何驱动产业智能升级?
  • 智融SW3203, 支持I2C控制的高效率同步升降压控制器。
  • 英雄联盟录像编辑神器:免费开源工具League Director完全指南
  • 2026第一季度上海家装深度调研:九家售后无忧与快速响应装企 - 资讯焦点
  • AI Agent 的七层架构:从 LLM 到自主智能体,中间到底隔了什么?