當線上集羣時候,會出現session共享問題。
雖然Tomcat提供了session copy的功能,但是缺點比較明顯:
1:當Tomcat多的時候,session需要大量同步到多臺集羣上,佔用內網寬帶
2:同一個用戶session,需要在多個Tomcat中都存在,浪費內存空間。
如果要替換掉Tomcat的session共享,替代方案應該滿足:
1:數據共享
2:內存存儲
3:key\value結構
基於Redis實現共享session登錄
本文由凱哥Java(gzh:kaigejava),個人blog:www.kaigejava.com。發佈於開源中國
再來回顧下將驗證碼保存在session中業務流程
我們在session中存放的是:session.setAttribute("code", code); 因爲session的特點,每次訪問都是一個新的sessionId.我們可以直接使用code作爲key.思考:那麼如果換成了Redis,還能使用code作爲可以嗎?
將用戶信息存放在session中流程:
用戶信息在session中存放:session.setAttribute("user", user); 同樣思考:那麼如果換成了Redis,還能使用user作爲可以嗎?
將code和user信息存放在Redis中,流程如下:
驗證碼數據結構是:string類型的
用戶對象數據類型是:hash類型的
根據上面分析,我們修改原來代碼:
需要考慮的是:Redis的key規則、過期時間
1:發送驗證碼的時候,將驗證碼存放到Redis中時候,需要考慮過期時間。其核心代碼如下:
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
2:用戶登錄的時候,將校驗驗證碼以及用戶信息存放到Redis中後,返回token
需要考慮的:
1:token不能重複
2:用戶過期時間
3:登錄成功後,要將token返回給前端
4:用戶只要訪問,Redis中的過期時間就要延長-在攔截器中處理的
用戶登錄核心代碼修改:
//2.1:校驗驗證碼是否正確 //String code = (String) session.getAttribute("code"); String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone); if (StringUtils.isEmpty(code) || !code.equals(loginForm.getCode())) { return Result.fail("驗證碼錯誤!"); } //2.2:根據手機號查詢,如果不存在,創建新用戶 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.select("id", "phone", "nick_name"); queryWrapper.eq("phone", phone); User user = this.getOne(queryWrapper); if (Objects.isNull(user)) { log.info("新建用戶"); //新建用戶 user = createUserWithPhone(phone); } //2.3:保存用戶到session中 UserDTO userDTO = new UserDTO(); userDTO.setId(user.getId()); userDTO.setIcon(user.getIcon()); userDTO.setNickName(user.getNickName()); //session.setAttribute("user", userDTO); //2.3.1:獲取隨機的token,作爲用戶登錄的令牌 String token = UUID.randomUUID().toString(true); //2.3.2:將用戶以hash類型存放到Redis中==》將user對象轉換成map //user對象裏有非string類型的字段,用這個方法會報錯的 // Map<String,Object> userMap = BeanUtil.beanToMap(userDTO); Map<String,Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>() , CopyOptions.create() .setIgnoreNullValue(true) .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString())); stringRedisTemplate.opsForHash().putAll(LOGIN_USER_TOKEN_KEY+token,userMap); //LOGIN_USER_TOKEN_TTL stringRedisTemplate.expire(LOGIN_USER_TOKEN_KEY+token,LOGIN_USER_TOKEN_TTL,TimeUnit.MINUTES); //2.3.3: 將token返回 return Result.ok(token);
需要注意:
在使用stringRedisTemplate存放hash對象的時候,對象中所有的key只能是string類型,如果存在非string類型會報錯的。所以這裏使用了hootool的BeanUtil工具類:
Map<String,Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>() , CopyOptions.create() .setIgnoreNullValue(true) .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
攔截器修改代碼:
因爲攔截器是我們自定義的,所以不能被spring容器管理的,RedisTemplate就不能自動注入了。我們就使用有參構造器,傳遞:
public class LoginRedisInterceptor implements HandlerInterceptor { private StringRedisTemplate stringRedisTemplate; /** * 因爲這個類不能被spring管理,所以不能直接注入RedisTemplate對象。通過構造函數傳遞 * @param stringRedisTemplate */ public LoginRedisInterceptor(StringRedisTemplate stringRedisTemplate){ this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //1:從請求中獲取到token String token = request.getHeader("authorization"); if(StringUtils.isEmpty(token)){ response.setStatus(401); return false; } //2:基於token獲取redis中用戶對象 String key = LOGIN_USER_TOKEN_KEY+token; Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(key); //3:判斷 if(userMap.isEmpty()){ response.setStatus(401); return false; } //將map轉對象 UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); UserHolder.saveUser(user); //刷新token的過期時間 stringRedisTemplate.expire(key,LOGIN_USER_TOKEN_TTL, TimeUnit.MINUTES); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }
總結:
在使用Redis替換session的時候,需要考慮的問題:
1:選擇合適的數據結構
2:選擇合適的key
3:選擇合適的存儲粒度
凱哥自己開發的,領取外賣、打車、咖啡、買菜、各大電商的優惠券的公¥衆¥號。如下圖: