Redis實戰之session共享

當線上集羣時候,會出現session共享問題。

雖然Tomcat提供了session copy的功能,但是缺點比較明顯:

1:當Tomcat多的時候,session需要大量同步到多臺集羣上,佔用內網寬帶

2:同一個用戶session,需要在多個Tomcat中都存在,浪費內存空間。

如果要替換掉Tomcat的session共享,替代方案應該滿足:

1:數據共享

2:內存存儲

3:key\value結構

32513e73b243ec122ea183b9683cc5de.png

基於Redis實現共享session登錄

本文由凱哥Java(gzh:kaigejava),個人blog:www.kaigejava.com。發佈於開源中國

再來回顧下將驗證碼保存在session中業務流程

0f7ad2613d0c2945a3521a444ac48373.png

 

我們在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中時候,需要考慮過期時間。其核心代碼如下:

6a6187b3f0126a1ec1705e9ace4ca0a6.png

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就不能自動注入了。我們就使用有參構造器,傳遞:

243cb12436424394a79705bf719ff850.png

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:選擇合適的存儲粒度

 

 

凱哥自己開發的,領取外賣、打車、咖啡、買菜、各大電商的優惠券的公¥衆¥號。如下圖:

527c8a82983e56a30f4299fc76e1956a.png

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章