使用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.pswp.cn/pingmian/76468.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/76468.shtml
英文地址,請注明出處:http://en.pswp.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;在我們做爬蟲的時候發現有些…