Redis实战之session共享
当线上集群时候,会出现session共享问题。
虽然Tomcat提供了session copy的功能,但是缺点比较明显:
1:当Tomcat多的时候,session需要大量同步到多台集群上,占用内网宽带
2:同一个用户session,需要在多个Tomcat中都存在,浪费内存空间.
凯哥自己开发的,领取外卖、打车、咖啡、买菜、各大电商的优惠券的公¥众¥号。如下图:
正文开始:
如果要替换掉Tomcat的session共享,替代方案应该满足:
1:数据共享
2:内存存储
3:key\value结构
基于Redis实现共享session登录
本文由凯哥Java(gz#h:kaigejava),个人blog:www#kaigejava#.com。发布于凯哥Java个人blog
再来回顾下将验证码保存在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中的过期时间就要延长-在拦截器中处理的
用户登录核心代码修改:
1 | //2.1:校验验证码是否正确<br> //String code = (String) session.getAttribute("code");<br> String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);<br> if (StringUtils.isEmpty(code) || !code.equals(loginForm.getCode())) {<br> return Result.fail("验证码错误!");<br> }<br> //2.2:根据手机号查询,如果不存在,创建新用户<br> QueryWrapper<User> queryWrapper = new QueryWrapper<>();<br> queryWrapper.select("id", "phone", "nick_name");<br> queryWrapper.eq("phone", phone);<br> User user = this.getOne(queryWrapper);<br> if (Objects.isNull(user)) {<br> log.info("新建用户");<br> //新建用户<br> user = createUserWithPhone(phone);<br> }<br> //2.3:保存用户到session中<br> UserDTO userDTO = new UserDTO();<br> userDTO.setId(user.getId());<br> userDTO.setIcon(user.getIcon());<br> userDTO.setNickName(user.getNickName());<br> <br> //session.setAttribute("user", userDTO);<br> //2.3.1:获取随机的token,作为用户登录的令牌<br> String token = UUID.randomUUID().toString(true);<br> //2.3.2:将用户以hash类型存放到Redis中==》将user对象转换成map<br> //user对象里有非string类型的字段,用这个方法会报错的<br> // Map<String,Object> userMap = BeanUtil.beanToMap(userDTO);<br> Map<String,Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>()<br> , CopyOptions.create()<br> .setIgnoreNullValue(true)<br> .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));<br> <br> stringRedisTemplate.opsForHash().putAll(LOGIN_USER_TOKEN_KEY+token,userMap);<br> //LOGIN_USER_TOKEN_TTL<br> stringRedisTemplate.expire(LOGIN_USER_TOKEN_KEY+token,LOGIN_USER_TOKEN_TTL,TimeUnit.MINUTES);<br> //2.3.3: 将token返回<br> return Result.ok(token); |
需要注意:
在使用stringRedisTemplate存放hash对象的时候,对象中所有的key只能是string类型,如果存在非string类型会报错的。所以这里使用了hootool的BeanUtil工具类:
1 | Map< String , Object > userMap = BeanUtil.beanToMap(userDTO, new HashMap<>()<br> , CopyOptions.create()<br> .setIgnoreNullValue( true )<br> .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString())); |
拦截器修改代码:
因为拦截器是我们自定义的,所以不能被spring容器管理的,RedisTemplate就不能自动注入了。我们就使用有参构造器,传递:
1 | public class LoginRedisInterceptor implements HandlerInterceptor {<br> <br> private StringRedisTemplate stringRedisTemplate;<br> <br> /**<br> * 因为这个类不能被spring管理,所以不能直接注入RedisTemplate对象。通过构造函数传递<br> * @param stringRedisTemplate<br> */ <br> public LoginRedisInterceptor(StringRedisTemplate stringRedisTemplate){<br> this .stringRedisTemplate = stringRedisTemplate;<br> }<br> <br> @Override<br> public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {<br> //1:从请求中获取到token<br> String token = request.getHeader("authorization");<br> if(StringUtils.isEmpty(token)){<br> response.setStatus(401);<br> return false;<br> }<br> //2:基于token获取redis中用户对象<br> String key = LOGIN_USER_TOKEN_KEY+token;<br> Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(key);<br> //3:判断<br> if(userMap.isEmpty()){<br> response.setStatus(401);<br> return false;<br> }<br> //将map转对象<br> UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);<br> UserHolder.saveUser(user);<br> //刷新token的过期时间<br> stringRedisTemplate.expire(key,LOGIN_USER_TOKEN_TTL, TimeUnit.MINUTES);<br> return true;<br> }<br> <br> <br> @Override<br> public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {<br> UserHolder.removeUser();<br> }<br>} |
总结:
在使用Redis替换session的时候,需要考虑的问题:
1:选择合适的数据结构
2:选择合适的key
3:选择合适的存储粒度
本文来自博客园,作者:kaizi1992,转载请注明原文链接:https://www.cnblogs.com/kaigejava/p/17095321.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 【全网最全教程】使用最强DeepSeekR1+联网的火山引擎,没有生成长度限制,DeepSeek本体