使用Redis解决:集群的Session共享问题

使用Redis解决:集群的Session共享问题


session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同的tomcat服务时导致数据丢失的问题。

在这里插入图片描述


  1. 问题背景
    • 无状态HTTP协议:HTTP协议本身是无状态的,服务器无法直接识别不同请求是否来自同一用户。
    • Session的作用:通过Session(通常存储在服务器内存中)跟踪用户状态(如登录信息、购物车数据等)。
    • 集群环境的问题:当用户请求被负载均衡分配到不同服务器时,每个服务器的Session数据是独立的。若用户第二次请求被路由到另一台服务器,该服务器可能没有对应的Session数据,导致用户状态丢失。
  2. 问题示例
    假设用户访问一个由3台服务器组成的集群:
    1. 用户首次访问服务器A,登录成功后,Session(含用户ID)存储在A的内存中。
    2. 下一次请求被负载均衡分配到服务器B,但B的内存中没有该用户的Session。
    3. 结果:用户被迫重新登录,体验中断。
  3. 核心挑战
    • ​数据一致性:多个服务器需要共享或同步Session数据。
    • ​可用性:Session存储服务需高可用,避免单点故障。
    • 性能:频繁的Session读写需低延迟,不影响用户体验。
  4. 常见解决方案
    • ​方案1:Session复制(Replication)​
      • 原理:将Session复制到集群中所有服务器。
      • ​优点:无需外部依赖,简单易实现。
      • 缺点:
        • 内存和带宽开销大(尤其节点多时)。
        • 数据一致性问题(如网络分区时)。
    • 方案2:集中式存储(推荐)​
      • ​原理:使用独立存储(如Redis、Memcached、数据库)保存Session,所有服务器共享访问。
      • ​优点:
        • 解耦Session与服务器,扩展性强。
        • 支持高可用(如Redis集群)。
      • ​缺点:引入额外组件,增加系统复杂度。
    • ​方案3:粘性会话(Sticky Session)​
      • ​​原理:负载均衡器将同一用户的请求始终路由到同一台服务器。
      • ​​优点:天然保证Session一致性。
      • ​​缺点:
        • 负载不均衡(某些服务器可能过载)。
        • 服务器故障时,其上的Session永久丢失。
    • 方案4:无状态设计(如JWT)​
      • ​原理:将用户状态直接存储在客户端(如Token中),服务器无需保存Session。
      • ​优点:彻底解决共享问题,天然支持分布式。
      • ​缺点:Token体积较大,且需处理加密和安全性。
  5. 实际应用场景
    • ​电商/社交平台:常用集中式存储(如Redis)确保高并发下的Session一致性。
    • ​微服务架构:通过JWT等无状态方案简化服务间通信。
    • ​传统Java EE集群:Tomcat可通过Redis Session Manager插件实现共享。
  6. 最佳实践建议
    • ​​优先选择集中式存储​(如Redis),兼顾性能和扩展性。
    • ​设置合理的Session过期时间,减少存储压力。
    • ​启用存储的高可用模式​(如Redis Sentinel或Cluster)。
    • ​结合HTTPS和加密,防止Token或Session被窃取。

在这里插入图片描述

文章目录

  • 使用Redis解决:集群的Session共享问题
    • 一、基于Session实现的用户登录
    • 二、使用Redis替代Session,解决集群的Session共享问题
      • 1.UserServiceImpl业务层实现
        • 前置条件,注入 StringRedisTemplate
        • 发送短信验证码
        • 短信验证码登录、注册
      • 2.LoginInterceptor登录拦截器
        • 校验登录状态
      • 3.添加登录拦截器到WebMvcConfigurer
    • 三、优化登录拦截器
      • 1.引入刷新token拦截器:RefreshTokenInterceptor
      • 2.重构LoginInterceptor登录拦截器逻辑
      • 3. 添加刷新token拦截器到WebMvcConfigurer配置中


一、基于Session实现的用户登录

传送门:基于Session实现用户登录

二、使用Redis替代Session,解决集群的Session共享问题

将原存储到session的地方,替换为存储到redis中。

1.UserServiceImpl业务层实现

前置条件,注入 StringRedisTemplate
    @Resourceprivate StringRedisTemplate stringRedisTemplate;

发送短信验证码
符合
不符合
开始
提交手机号
校验手机号
生成验证码
保存验证码到Redis
发送验证码
结束
    @Overridepublic Result sendCode(String phone) {// 1. 校验手机号if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 3.符合,生成验证码String code = RandomUtil.randomNumbers(6);// 4.保存验证码到redisstringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);// 5.发送验证码log.debug("发送短信验证码成功,验证码:{}", code);return Result.ok();}

短信验证码登录、注册
一致
存在
不一致
不存在
开始
提交手机号和验证码
校验验证码
根据手机号查询用户
判断用户是否存在
保存用户到session
结束
创建新用户
保存用户到数据库
    @Overridepublic Result login(LoginFormDTO loginForm) {// 1.校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误!");}// 2.从redis获取验证码String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)) {// 3.不一致,报错return Result.fail("验证码错误");}// 4.一致,根据手机号查询用户 select * from tb_user where phone = ?User user = lambdaQuery().eq(User::getPhone, phone).one();// 5.判断用户是否存在if (user == null) {// 6.不存在,创建新用户并保存user = createUserWithPhone(phone);}// 7.保存用户信息到redis中// 7.1 随机生成token(不带划线),作为登录令牌String token = UUID.randomUUID().toString(true);// 7.2 将User对象转化为HashMap存储UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create() // 数据拷贝选项.setIgnoreNullValue(true) // 忽略null值.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()) // 字段值编辑器);// 7.3 存储String tokenKey = RedisConstants.LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);// 7.4 设置token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);return Result.ok(token);}private User createUserWithPhone(String phone) {// 1.创建用户User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));// 2.保存用户save(user);return user;}

2.LoginInterceptor登录拦截器

校验登录状态
非空
存在
token为空
开始
获取请求头中的token
判断token是否为空
从Redis获取用户
判断用户是否存在
保存用户到ThreadLocal
刷新token有效期
放行
结束
拦截
拦截
/*** LoginInterceptor 登录拦截器*/
public class LoginInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;/*** 未加入Spring IOC容器管理,使用构造方法初始化 stringRedisTemplate*/public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {response.setStatus(401);return false;}// 2.基于token获取redis中的用户String tokenKey = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);// 3.判断用户是否存在if (userMap.isEmpty()) {// 4.不存在,拦截response.setStatus(401);return false;}// 5.将查询到的Hash数据转为UserDTO对象UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.存在,保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}

3.添加登录拦截器到WebMvcConfigurer

@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns("/user/code", "/user/login", "/othoer/**");}
}

三、优化登录拦截器

为解决访问未拦截接口时,不刷新token导致用户登录过期的问题,引入RefreshTokenInterceptor

在这里插入图片描述

1.引入刷新token拦截器:RefreshTokenInterceptor

// 关键包
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;/*** RefreshTokenInterceptor 刷新Token拦截器*/
public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;/*** 未加入Spring IOC容器管理,使用构造方法初始化 stringRedisTemplate*/public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {// 放行return true;}// 2.基于token获取redis中的用户String tokenKey = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);// 3.判断用户是否存在if (userMap.isEmpty()) {// 放行return true;}// 5.将查询到的Hash数据转为UserDTO对象UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.存在,保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}

2.重构LoginInterceptor登录拦截器逻辑

// 关键包
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.concurrent.TimeUnit;/*** LoginInterceptor 登录拦截器*/
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.判断是否需要拦截(ThreadLocal中是否有用户)if (UserHolder.getUser() == null) {// 没有,需要拦截,设置状态码response.setStatus(401);// 拦截return false;}// 有用户则放行return true;}}

3. 添加刷新token拦截器到WebMvcConfigurer配置中

// 关键包
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configurationpublic class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 登录拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code", "/user/login", "/other/**").order(1);// 刷新token拦截器,order=0优先加载registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/76468.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux 内核知识体系[1]

1 Linux内核知识体系 2.Linux内核学习路线 2.1基础知识准备 操作系统基础&#xff1a;了解操作系统的概念和基本原理&#xff0c;包括进程管理、内存管理、文件系统、输入输出等。 书籍&#xff1a;《操作系统&#xff1a;设计与实现》&#xff08;Andrew S. Tanenbaum&…

KiActivateWaiterQueue函数和Queue->Header.WaitListHead队列等待列表的关系

第一部分&#xff1a; if (Thread->ApcState.KernelApcPending && (Thread->SpecialApcDisable 0) && (Thread->WaitIrql < APC_LEVEL)) { } else { // // Insert wait block in ob…

让DeepSeek API支持联网搜索

引子 DeepSeek官网注册的API token是不支持联网搜索的&#xff0c;这导致它无法辅助分析一些最新的情况或是帮忙查一下互联网上的资料。本文从实战角度提供一种稳定可靠的方法使得DeepSeek R1支持联网搜索分析。 正文 首先登录火山方舟控制台&#xff0c;https://www.volcen…

生物信息Rust-01

前言-为什么想学Rust&#xff1f; 一直想多学一门编译语言&#xff0c;主要有几个原因吧&#xff08;1. 看到一位老师实验室要求需要掌握一门编译语言&#xff1b;2. 自己享想试着开发一些实用的生信工具&#xff0c;感觉自己现在相比于数据分析&#xff0c;探索生物学层面的意…

字符串与相应函数(上)

字符串处理函数分类 求字符串长度&#xff1a;strlen长度不受限制的字符串函数&#xff1a;strcpy,strcat,strcmp长度受限制的字符串函数:strncpy,strncat,strncmp字符串查找&#xff1a;strstr,strtok错误信息报告&#xff1a;strerror字符操作&#xff0c;内存操作函数&…

asm汇编源代码之文件操作相关

提供7个子程序:   1. 关闭文件 FCLOSE   2. 打开文件 FOPEN   3. 文件大小 FSIZE   4. 读文件 FREAD   5. 写文件 FWRITE   6. 建立文件 FCREATE   7. 读取或设置文件指针 FPOS 具体功能及参数描述如下 ; ---------------------------- FCLOSE PROC  FAR ; IN…

[Dify] 使用 Docker 本地部署 Dify 并集成 Ollama 模型的详细指南

在 AI 应用快速发展的今天&#xff0c;开源项目如 Dify 正成为构建本地化 AI 应用的利器。通过 Dify&#xff0c;你可以轻松地集成不同的大语言模型&#xff08;LLM&#xff09;&#xff0c;如 Ollama&#xff0c;并快速创建可交互的 AI 应用。本篇文章将带你一步步通过 Docker…

Spring Boot 测试详解,包含maven引入依赖、测试业务层类、REST风格测试和Mock测试

Spring Boot 测试详解 1. 测试依赖引入 Spring Boot 默认通过以下 Maven 依赖引入测试工具&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</s…

DevOps与功能安全:Perforce ALM通过ISO 26262合规认证,简化安全关键系统开发流程

本文来源perforce.com&#xff0c;由Perforce中国授权合作伙伴、DevSecOps解决方案提供商-龙智翻译整理。 近日&#xff0c;Perforce ALM&#xff08;原Helix ALM&#xff09;通过了国际权威认证机构 TV SD的ISO 26262功能安全流程认证&#xff01;该认证涵盖Perforce ALM解决方…

Android11车载WiFi热点默认名称及密码配置

一、背景 基于车厂信息安全要求,车载热点默认名称不能使用统一的名称,以及默认密码不能为简单的1~9。 基于旧项目经验,组装工厂自动化测试及客户整车组装的时候均存在多台设备同时打开,亦不太推荐使用统一的热点名称,连接无法区分。 二、需求 根据客户的要求,默认名称…

MacOs java环境配置+maven环境配置踩坑实录

oracl官网下载jdk 1.8的安装包 注意可能需要注册&#xff01;&#xff01;&#xff01; 下载链接&#xff1a;下载地址点击 注意晚上就不要下载了 报错400 &#xff01;&#xff01;&#xff01; 1.点击安装嘛 2.配置环境变量 export JAVA_HOME/Library/Java/Java…

如何解读 /proc/net/netstat

在刷了屏的川普&#xff0c;关税&#xff0c;AI 大模型和 RDMA 之外的一股清流&#xff0c;来点实用的。 众所周知 /proc/net/netstat 很难读&#xff0c;且 netstat 并不是每个系统上都支持 -s&#xff0c;那么对齐该文件给出一个可读的输出就是一件高尚的事。可以用 column …

汉化进度100%

P3834 #include<bits/stdc.h> #define int long long #define 定义整型变量 int #define 这是一个常量 const #define 无返回值函数 void #define 这是一个循环条件在后面 for #define 定义结构体 struct #define 如果 if #define 否则 else #define 定义无返回值的 sig…

基于SpringBoot的动物救助中心系统(源码+数据库)

500基于SpringBoot的动物救助中心系统&#xff0c;本系统共分为2个角色&#xff1a;系统管理员、用户&#xff0c;主要功能如下 【管理员】&#xff1a; 1. 登录&#xff1a;管理员可以通过登录系统来管理各种功能。 2. 用户管理&#xff1a;管理员可以查看用户列表&#xff0…

rockylinux 8 9 升级到指定版本

rockylinux 8 update 指定版本 rockylinux 历史版 所有版本rockylinux 最新版 所有版本vault历史版 pub最新版(https://dl.rockylinux.org)地址后面增加不同名称 echo "delete repos" rm -rf /etc/yum.repos.d/*echo "new rockylinux repo" cat <<EO…

聚焦AI与大模型创新,紫光云如何引领云计算行业快速演进?

【全球云观察 &#xff5c; 科技热点关注】 随着近年来AI与大模型的兴起&#xff0c;云计算行业正在发生着一场大变局。 “在2025年春节期间&#xff0c;DeepSeek两周火爆全球&#xff0c;如何进行私域部署成了企业关心的问题。”紫光云公司总裁王燕平强调指出&#xff0c;AI与…

React8+taro开发微信小程序,实现lottie动画

安装核心依赖 npm install lottie-miniprogram tarojs/plugin-html --save修改 Taro 配置 (config/index.js) const config {plugins: [tarojs/plugin-html,// 其他插件...],mini: {canvas: true,webpackChain(chain) {chain.merge({module: {rule: {lottie-loader: {test: …

有效压缩 Hyper-v linux Centos 的虚拟磁盘 VHDX

参考&#xff1a; http://www.360doc.com/content/22/0505/16/67252277_1029878535.shtml VHDX 有个不好的问题就是&#xff0c;如果在里面存放过文件再删除&#xff0c;那么已经使用过的空间不会压缩&#xff0c;导致空间一直被占用。那么就需要想办法压缩空间。 还有一点&a…

【力扣hot100题】(089)最长有效括号

这题目真是越做越难了。 但其实只是思路很难想到&#xff0c;一旦会了方法就很好做。 但问题就在方法太难想了…… 思路还是只要遍历一遍数组&#xff0c;维护动态规划数组记录截止至目前位置选取该元素的情况下有效括号的最大值。 光是知道这个还不够&#xff0c;看了答案…

Ajax------免刷新地前后端交互

本文略带PHP代码需要在PHP环境下使用 介绍 AJAX (Asynchronous JavaScript and XML) 是一种创建快速动态网页应用的开发技术&#xff0c;它允许网页在不重新加载整个页面的情况下&#xff0c;与服务器交换数据并更新部分网页内容。例如&#xff0c;在我们做爬虫的时候发现有些…