當線上集群時候,會出現(xiàn)session共享問題。
雖然Tomcat提供了session copy的功能,但是缺點比較明顯:
1:當Tomcat多的時候,session需要大量同步到多臺集群上,占用內(nèi)網(wǎng)寬帶
2:同一個用戶session,需要在多個Tomcat中都存在,浪費內(nèi)存空間。
凱哥推薦:
redis快速入門
【圖文教程】Redis分片集群搭建
【圖文教程】Redis哨兵集群的搭建
【圖文教程】Redis主從集群安裝
【圖文教程】centos單機安裝Redis
Redis實戰(zhàn)9-全局唯一ID
分庫分表之后,ID主鍵如何處理?
如果要替換掉Tomcat的session共享,替代方案應(yīng)該滿足:
1:數(shù)據(jù)共享
2:內(nèi)存存儲
3:keyvalue結(jié)構(gòu)
基于Redis實現(xiàn)共享session登錄
再來回顧下將驗證碼保存在session中業(yè)務(wù)流程
我們在session中存放的是:session.setAttribute("code", code); 因為session的特點,每次訪問都是一個新的sessionId.我們可以直接使用code作為key.思考:那么如果換成了Redis,還能使用code作為可以嗎?
將用戶信息存放在session中流程:
用戶信息在session中存放:session.setAttribute("user", user); 同樣思考:那么如果換成了Redis,還能使用user作為可以嗎?
將code和user信息存放在Redis中,流程如下
驗證碼數(shù)據(jù)結(jié)構(gòu)是:string類型的
用戶對象數(shù)據(jù)類型是:hash類型的
根據(jù)上面分析,我們修改原來代碼:
需要考慮的是:Redis的key規(guī)則、過期時間
1:發(fā)送驗證碼的時候,將驗證碼存放到Redis中時候,需要考慮過期時間。其核心代碼如下:
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
2:用戶登錄的時候,校驗驗證碼及將用戶存放返回token
需要考慮的:
1:token不能重復(fù)
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:根據(jù)手機號查詢,如果不存在,創(chuàng)建新用戶
QueryWrApper 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對象轉(zhuǎn)換成map
//user對象里有非string類型的字段,用這個方法會報錯的
// Map userMap = BeanUtil.beanToMap(userDTO);
Map 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 userMap = BeanUtil.beanToMap(userDTO,new HashMap<>()
, CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
攔截器修改代碼:
因為攔截器是我們自定義的,所以不能被spring容器管理的,RedisTemplate就不能自動注入了。我們就使用有參構(gòu)造器,傳遞
public class LoginRedisInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
* 因為這個類不能被spring管理,所以不能直接注入RedisTemplate對象。通過構(gòu)造函數(shù)傳遞
* @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 userMap = stringRedisTemplate.opsForHash().entries(key);
//3:判斷
if(userMap.isEmpty()){
response.setStatus(401);
return false;
//將map轉(zhuǎn)對象
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();
總結(jié):
在使用Redis替換session的時候,需要考慮的問題:
1:選擇合適的數(shù)據(jù)結(jié)構(gòu)
2:選擇合適的key
3:選擇合適的存儲粒度
大家好,我是凱哥JAVA(kaigejava),樂于分享技術(shù)文章,歡迎大家關(guān)注“凱哥Java”,及時了解更多。讓我們一起學Java。也歡迎大家有事沒事就來和凱哥聊聊~~~