Redis解决Session共享问题

news2024/11/30 4:59:27

在这里插入图片描述

文章目录

  • 一、集群Session共享问题
  • 二、Redis存储验证码和对象
  • 三、解决状态登录刷新问题

一、集群Session共享问题

session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务器时导致数据丢失的问题
在这里插入图片描述
tomcat可以进行多台tomcat进行session拷贝,但是数据拷贝保存相同的内容会存在资源浪费,而且会有时间延迟,所以这种方案不可行

session的替代方案应该满足:

  • 数据共享
  • 内存存储
  • key、value结构

这里我们可以使用redis

二、Redis存储验证码和对象

发送短信:

@Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.校验手机号
        if (phone == null || str.matches("^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$")) {
            // 2.如何不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
        // 4.保存验证码到Redis
        stringRedisTemplate.opsForValue().set("login:code:" + phone,code,2, TimeUnit.MINUTES);
        //具体的发送逻辑 在这里就不实现了
        return Result.ok();
    }

首先,我们会校验前端传来的手机号格式,如果格式不正确直接返回。使用hutool的工具类生成6位随机验证码,然后将验证码作为value存入到Redis中,为了避免key重复,我们设置了固定格式的key,并且设置一个2分钟的超时时间,超过两分钟验证码自动失效。

登录功能:

public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1. 校验手机号
        String phone = loginForm.getPhone();
        if (phone == null || str.matches("^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$")) {
            // 如何不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 2. 校验验证码
        String cacheCode = stringRedisTemplate.opsForValue().get("login:code:" + phone);
        String code = loginForm.getCode();
        // 3. 不一致,报错
        if(cacheCode == null || !cacheCode.equals(code)) {
            return Result.fail("验证码错误!");
        }
        // 4. 一致,根据手机号查询用户 select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();
        // 5. 判断用户是否存在
        if (user == null) {
            // 6. 不存在,创建用户并保存
            user = createUserWithPhone(phone);
        }
        // 7. 保存用户信息到Redis
        String token = UUID.randomUUID().toString(true);
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>()
        , CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));
        stringRedisTemplate.opsForHash().putAll("login:token:" + token,userMap);
        stringRedisTemplate.expire("login:token:" + token,30,TimeUnit.MINUTES);
        return Result.ok(token);
    }

我们在进行登录时,首先会对手机号格式进行检验,如果手机号格式正确,我们从Redis中获取验证码和客户端传来的验证码进行比较,如果一致我们就放行,先去数据库查询该用户信息,如果用户不存在进行保存。
在这里插入图片描述
可能有的同学会有疑问,为什么这里要进行这么麻烦的操作呢?
在这里插入图片描述
因为我们UserDTO中的id是Long类型的,会报Long转String类型转换异常,因为我们这里使用的是StringRedisTemplate
在这里插入图片描述
该类型要求key和value都是String类型,但是我们将对象转为Map时,id为Long类型,所以就出现了该问题,两种方案:1.自定义Map手动put 2.使用BeanUtil,自定义规则

我们需要将用户对象存储在Redis中,这里用什么作为key呢?我们这里用token作为key,将token返回给客户端,客户端后面请求的时候使用该token来获取value。

我们value保存对象时,使用什么存储呢?
1.String:
在这里插入图片描述

2.Hash:
在这里插入图片描述
我们这里使用Hash存储对象,因为Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD,并且占用内存更少。

我们使用UUID随机生成token,但是我们value是哈希结构,我们使用BeanUtil将对象转为Hash存储,因为Redis是在内存存储的,如果一直只存会存在内存不够用的情况,所以我们这里仍然需要设置一个超时时间,那么设置多长时间呢?我们这里模仿Session的只要超过30分钟不访问就会销毁。
在这里插入图片描述
但是我们现在设置的是,从设置开始不管有没有用户访问30分钟后都会销毁,这样肯定是不行的,我们需要和session一样,只要有用户访问我们就需要更新超时时间,那么怎么做呢?可以借助拦截器
在这里插入图片描述
我们的拦截器不是Spring创建的对象,所以我们无法使用注入的方式获取StringRedisTemplate对象,我们需要使用构造方法的方法,那么谁来调用呢?
在这里插入图片描述
我们可以在MvcConfig注册拦截器时传入StringRedisTemplate对象
由于我们多处都需要用到ThreadLocal存储的对象,所以我们将ThreadLocal封装成一个工具类:

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}
public class LoginInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(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(StrUtil.isBlank(token)) {
            response.setStatus(401);
            return false;
        }
        // 2. 使用token获取Redis中的对象
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);
        // 3. 判断用户是否存在
        if(userMap == null) {
            response.setStatus(401);
            return false;
        }
        // 4. 将Hash 格式转为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 5. 将用户存入ThreadLocal中
        UserHolder.saveUser(userDTO);
        // 6. 刷新token超时时间
        stringRedisTemplate.expire("login:token:" + token,30,TimeUnit.MINUTES);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

大家需要注意的是我们需要remove ThreadLocal,因为ThreadLocal可能会存在内存泄露问题,因为强软引用的问题,这里我们不具体介绍。

三、解决状态登录刷新问题

在这里插入图片描述
但是这样会存在一些问题,该拦截器只会拦截需要登录的路径,其他路径是不会拦截了,也就不会进行token有效期的刷新了。怎么解决呢? 新加一个全部路径的拦截器
在这里插入图片描述

public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(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 (StrUtil.isBlank(token)) {
            return true;
        }
        // 2. 基于token获取Redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
        // 3. 判断用户是否存在
        if(userMap == null) {
            return true;
        }
        // 将查询到的Hash转为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 5. 存在 保存用户到ThreadLocal
        UserHolder.saveUser(userDTO);
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

我们创建一个拦截全部路径的拦截器来进行token有效期的刷新
在这里插入图片描述
我们在登录拦截器里,只需要判断ThreadLocal里是否存在有效的用户,如果有放行,否则拦截。

public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                   "/user/code",
                   "/user/login",
                   "/blog/hot",
                   "/shop/**",
                   "/shop-type/**",
                   "/upload/**",
                   "/voucher/**"
                );
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**");
    }
}

我们在注册刷新Token的拦截器,并且增加所有路径。
但是我们如何保证刷新Token的拦截器在登录拦截器之前执行呢?其实在MvcConfig中注册拦截器的顺序也就是拦截的顺序,但是这样不保险
在这里插入图片描述
其实我们在addInterceptor时会生成一个拦截器注册器对象
在这里插入图片描述
拦截器注册器中又有一个order属性,默认都是0,这个值决定拦截器的执行顺序,值越小执行优先级越高。
在这里插入图片描述
我们可以通过设置order来决定它们的执行顺序

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/741214.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

主数据管理:识别主数据

1.识别主数据的两个方法 1.1主数据特征识别法 主要评估企业全部数据中的各类主数据是否符合主数据的每个特征&#xff0c;如发现任何不符合主数据特征的数据&#xff0c;则将其剔除出主数据管理的范畴。 高价值性&#xff1a;主数据具备极高的业务价值。主数据描述企业最核心的…

全面了解JavaScirpt 的垃圾(garbage collection)回收机制

全面了解JavaScirpt 的垃圾(garbage collection)回收机制 一、垃圾回收机制—GC javascript具有自动垃圾回收机制(GC:Garbage Collecation)&#xff0c;也就是说&#xff0c;执行环境会负责管理代码执行过程中使用的内存。 原理&#xff1a;垃圾收集器会定期&#xff08;周期性…

超宽带(UWB)无线通信的技术特点

近年来&#xff0c;超宽带(UWB)无线通信成为短距离、高速无线网络最热门的物理层技术之一。 UWB的产生与发展 超宽带(UWB)有着悠久的发展历史&#xff0c;但在1989年之前&#xff0c;超宽带这一术语并不常用&#xff0c;在信号的带宽和频谱结构方面也没有明确的规定。1989年&am…

Django_haystack全文搜索

haystack是全文搜索的框架&#xff0c;支持whoosh、solr、Xapian、Elasticsearc四种全文检索引擎&#xff0c;点击查看官方网站。 whoosh是用纯Python编写的全文搜索引擎&#xff0c;虽然性能比不上sphinx、xapian、Elasticsearc等&#xff0c;但是无二进制包&#xff0c;程序…

C语言为什么是世界上最难的语言?看完你就明白了

在当前的信息技术领域&#xff0c;C语言在许多大型科技公司中扮演着重要角色&#xff0c;如谷歌、苹果和微软。这些公司不仅开发软件&#xff0c;还专门设计硬件&#xff0c;例如AMD、英传达和英特尔等公司。它们都使用C加速库来提高软件性能。 C语言之所以难以掌握&#xff0…

一个简单不报错的summernote 图片上传案例

一个简单不报错的summernote 图片上传案例 一个比较完整的summernote上传图片的案例&#xff0c;没有后台&#xff08;上传图片网上案例太多&#xff09;&#xff0c;只有前端js.修正了网上提供的&#xff0c;但是有bug的代码。 这个例子&#xff0c;js保证不报错。亲测可用 …

windows mysql服务自动启动 错误模块名称:ntdll.dll

在windows11家庭版系统安装mysql后&#xff0c;偶尔有mysql服务自动启动失败的问题&#xff0c;大体的原因就是MySQL的启动服务不能在Windows规定的启动时间内完成准备&#xff0c;于是就会停止其启动程序。windows事件日志如下&#xff1a; 方法一 &#xff1a; 检查确定文件…

计算机网络完结篇(从键入网址到响应页面-->HTTP常见面试题-->TCP三次握手及挥手-->TCP超时重传等特性-->Ip地址)

1. 基础篇 从键入网址到响应页面 1. 解析URL 生成HTTP请求信息 解析URL之后&#xff0c;浏览器确定了Web服务器和文件名&#xff0c;然后就是根据信息来生成HTTP请求信息。 2. 真实地址查询 DNS DNS服务器&#xff1a; 在第一步解析URL获取到对应的HTTP请求信息之后&#xf…

React 之 CSS编写方式

一、概述 整个前端已经是组件化的天下&#xff0c;而CSS的设计就不是为组件化而生的&#xff0c;所以在目前组件化的框架中都在需要一种合适的CSS解决方案 在组件化中选择合适的CSS解决方案应该符合以下条件&#xff1a; 可以编写局部css&#xff1a;css具备自己的具备作用域&a…

实现一个vscode插件:打开多个vscode项目时根据.nvmrc文件自动切换nvm

开发背景与最终功能 需要维护一些老项目&#xff0c;同时开发新项目时&#xff0c;切换nvm很烦人 最终实现vscode插件&#xff1a;每个vscode实例打开一个项目&#xff0c;切换vscode实例时能自动切换版本&#xff08;需要项目根目录有一个.nvmrc文件&#xff09; 插件下载 …

nextjs13中cssModule设置子标签的样式

前言 最近在学习nextjs中发现&#xff0c;如果在cssModule文件中直接设置子标签的样式比较麻烦&#xff0c;最后在网上看到一种方式可以解决&#xff0c;方式如下。 ps: 此方式不一定最优&#xff0c;因为在我看来此代码耦合性太高了&#xff0c;看着不太舒适&#xff0c;因为…

保姆级指南|APP原型设计怎么做?手把手教学超详细!

在数字化产品研发过程中&#xff0c;原型设计扮演着至关重要的角色。不夸张的说&#xff0c;产品原型很大程度决定了最终产品的成功与否。随着主流产品载体的更迭和发展&#xff0c;原型设计也在不断的演进。也产生了网页原型设计&#xff0c;桌面端app原型设计&#xff0c;移动…

PyTorch模型创建与nn.Module

文章和代码已经归档至【Github仓库&#xff1a;https://github.com/timerring/dive-into-AI 】或者公众号【AIShareLab】回复 pytorch教程 也可获取。 文章目录 模型创建与nn.Modulenn.Module 总结 模型创建与nn.Module 创建网络模型通常有2个要素&#xff1a; 构建子模块拼接…

Redis 优惠卷秒杀(二) 异步秒杀、基于Stream的消息队列处理

目录 基于Stream的消息队列 Redis优化秒杀 登录头 改进秒杀业务&#xff0c;调高并发性能 Redis消息队列实现异步秒杀 ​编辑基于List结构模拟消息队列 基于PuSub的消息队列 ​编辑 基于Stream的消息队列 Redis消息队列 基于Stream的消息队列 Redis优化秒杀 登录头 改…

skywalking安装

目录 skywalking部署示意图 server安装 裸机安装 docker单节点安装 docker集群安装 k8s安装 helm安装(官方) k8s yaml安装 动态配置安装 client agent安装 skywalking部署示意图 skywalking ui - web界面管理程序oap server - skywalking服务程序nacos - skywalking集…

数字孪生水务系统可视化管理平台有效缓解城市供水压力

针对传统自来水厂供水水质安全隐患大&#xff0c;运行管理落后等问题&#xff0c;基于数字孪生技术构建全厂三维立体模型,在电脑前就可以掌握全厂管线、设备运行情况,遇到预案中的突发事件还可以给出辅助决策方案。从根本上有效提高水厂运行管理效率,增强对水质变化的应对能力,…

分析shein独立站成功的原因

近年来&#xff0c;Shein独立站在快时尚领域声名鹊起&#xff0c;成为许多时尚消费者的首选网站。面对激烈的竞争&#xff0c;它依然能够站稳脚跟并不断壮大。那么&#xff0c;Shein独立站成功的原因是什么呢&#xff1f; Shein独立站——以消费者为中心的运营模式 Shein独立站…

【Python】Locust持续优化:InfluxDB与Grafana实现数据持久化与可视化分析

在进行性能测试时&#xff0c;我们需要对测试结果进行监控和分析&#xff0c;以便于及时发现问题并进行优化。 Locust在内存中维护了一个时间序列数据结构&#xff0c;用于存储每个事件的统计信息。 这个数据结构允许我们在Charts标签页中查看不同时间点的性能指标&#xff0c…

java中使用HttpRequest发送请求调用自己的接口

(539条消息) java中使用HttpRequest发送请求_java httprequest_thankful_chn的博客-CSDN博客 <dependency><groupId>com.github.kevinsawicki</groupId><artifactId>http-request</artifactId><version>5.6</version></dependenc…

华为云-hcip笔记-网络服务规划

华为云-hcip笔记-网络服务规划 网络服务规划 安全组和网络ACL 网络ACL对子网进行防护&#xff0c;安全组是对ECS进行防护。 对等连接VPC peering 两个vpc之间的网络连接&#xff0c;用户可以使用私有ip地址在两个vpc之间进行通信。 同账号中对等连接自动接受&#xff0c;跨…