使用Redis解決:集群的Session共享問題
session共享問題:多臺Tomcat并不共享session存儲空間,當請求切換到不同的tomcat服務時導致數據丟失的問題。
- 問題背景
- ?無狀態HTTP協議:HTTP協議本身是無狀態的,服務器無法直接識別不同請求是否來自同一用戶。
- ?Session的作用:通過Session(通常存儲在服務器內存中)跟蹤用戶狀態(如登錄信息、購物車數據等)。
- ?集群環境的問題:當用戶請求被負載均衡分配到不同服務器時,每個服務器的Session數據是獨立的。若用戶第二次請求被路由到另一臺服務器,該服務器可能沒有對應的Session數據,導致用戶狀態丟失。
- 問題示例
假設用戶訪問一個由3臺服務器組成的集群:- 用戶首次訪問服務器A,登錄成功后,Session(含用戶ID)存儲在A的內存中。
- 下一次請求被負載均衡分配到服務器B,但B的內存中沒有該用戶的Session。
- 結果:用戶被迫重新登錄,體驗中斷。
- 核心挑戰
- ?數據一致性:多個服務器需要共享或同步Session數據。
- ?可用性:Session存儲服務需高可用,避免單點故障。
- 性能:頻繁的Session讀寫需低延遲,不影響用戶體驗。
- 常見解決方案
- ?方案1:Session復制(Replication)?
- 原理:將Session復制到集群中所有服務器。
- ?優點:無需外部依賴,簡單易實現。
- 缺點:
- 內存和帶寬開銷大(尤其節點多時)。
- 數據一致性問題(如網絡分區時)。
- 方案2:集中式存儲(推薦)?
- ?原理:使用獨立存儲(如Redis、Memcached、數據庫)保存Session,所有服務器共享訪問。
- ?優點:
- 解耦Session與服務器,擴展性強。
- 支持高可用(如Redis集群)。
- ?缺點:引入額外組件,增加系統復雜度。
- ?方案3:粘性會話(Sticky Session)?
- ??原理:負載均衡器將同一用戶的請求始終路由到同一臺服務器。
- ??優點:天然保證Session一致性。
- ??缺點:
- 負載不均衡(某些服務器可能過載)。
- 服務器故障時,其上的Session永久丟失。
- ?方案4:無狀態設計(如JWT)?
- ?原理:將用戶狀態直接存儲在客戶端(如Token中),服務器無需保存Session。
- ?優點:徹底解決共享問題,天然支持分布式。
- ?缺點:Token體積較大,且需處理加密和安全性。
- ?方案1:Session復制(Replication)?
- 實際應用場景
- ?電商/社交平臺:常用集中式存儲(如Redis)確保高并發下的Session一致性。
- ?微服務架構:通過JWT等無狀態方案簡化服務間通信。
- ?傳統Java EE集群:Tomcat可通過
Redis Session Manager
插件實現共享。
- 最佳實踐建議
- ??優先選擇集中式存儲?(如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;
發送短信驗證碼
@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();}
短信驗證碼登錄、注冊
@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登錄攔截器
校驗登錄狀態
/*** 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);}
}