分布式session共享解决方案

news2024/9/23 17:20:43

分布式session共享解决方案

1.分布式 Session 问题

  • 示意图

image-20230219173607725

  • 解读上图,假如我们去购买商品
  1. 当 Nginx 对请求进行负载均衡后, 可能对应到不同的 Tomcat
  2. 比如第 1 次请求, 均衡到 TomcatA, 这时 Session 就记录在 TomcatA, 第 2 次请求,
    均衡到 TomcatB, 这时就出现问题了,因为 TomcatB 会认为该用户是第 1 次来,就会
    允许购买请求
  3. 这样就会造成重复购买

2.解决方案

2.1Session 绑定/粘滞

什么是 session 绑定/粘滞/黏滞

image-20230219173702703

  • 解读上图

概述: 服务器会把某个用户的请求, 交给 tomcat 集群中的一个节点,以后此节点就负责该保存该用户的session

  1. Session 绑定可以利用负载均衡的源地址 Hash(ip_hash)算法实现
  2. 负载均衡服务器总是将来源于同一个 IP 的请求分发到同一台服务器上,也可以根据 Cookie 信息将同一个用户的请求总是分发到同一台服务器上
  3. 这样整个会话期间,该用户所有的请求都在同一台服务器上处理,即 Session 绑定
    在某台特定服务器上,保证 Session 总能在这台服务器上获取。这种方法又被称为
    session 黏滞/粘滞

ps:nginx配置ip_hash示例

upstream llpservers{
	ip_hash;
	server 192.168.79.111:8081;
	server 192.168.79.111:8080;
}

优点: 不占用服务端内存

缺点:

  1. 增加新机器,会重新 Hash,导致重新登录
  2. 应用重启, 需要重新登录
  3. 某台服务器宕机,该机器上的 Session 也就不存在了,用户请求切换到其他机器后因为没有 Session 而无法完成业务处理, 这种方案不符合系统高可用需求, 使用较少

2.2Session 复制

image-20230219174427596

ps:可以通过配置tomcat实现session配置

  • Session 复制是小型架构使用较多的一种服务器集群 Session 管理机制
  • 应用服务器开启 Web 容器的 Session 复制功能,在集群中的几台服务器之间同步
    Session 对象,使每台服务器上都保存了所有用户的 Session 信息
  • 这样任何一台机器宕机都不会导致 Session 数据的丢失,而服务器使用 Session 时,
    也只需要在本机获取即可

优点: 不占用服务端内存

缺点:

  1. 增加新机器,会重新 Hash,导致重新登录
  2. 应用重启, 需要重新登录
  3. 某台服务器宕机,该机器上的 Session 也就不存在了,用户请求切换到其他机器后因为没有 Session 而无法完成业务处理, 这种方案不符合系统高可用需求, 使用较少

2.3前端存储

优点: 不占用服务端内存

缺点:

  1. 存在安全风险
  2. 数据大小受 cookie 限制
  3. 占用外网带宽

2.4 后端集中存储

优点:安全,容易水平扩展

缺点:增加复杂度,需要修改代码

3.代码实现

现在主流的解决方案还是将用户登录信息在后端集中存储,这里列举两种存储方式

3.1 SpringSession 实现分布式 Session

基本说明

将用户 Session 不再存放到各自登录的 Tomcat 服务器,而是统一存在 Redis,从而解决Session 分布式问题

  1. 如图, 将用户的 Session 信息统一保存到 Redis 进行管理
  2. 说明: SpringSession在默认情况下是以原生形式保存的

image-20230219200625731

引入依赖

<!--spring data redis 依赖, 即 spring 整合 redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.4.5</version>
</dependency>
<!--pool2 对象池依赖-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.9.0</version>
</dependency>
<!--实现分布式 session, 即将 Session 保存到指定的 Redis-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

image-20230219200828662

3.2直接将用户信息统一放入 Redis

基本说明

前面将 Session 统一存放到指定 Redis, 是以原生的形式存放, 在操作时, 还需要反序列化,不方便,我们可以直接将登录用户信息统一存放到 Redis, 利于操作

需求分析/图解

直接将登录用户信息统一存放到 Redis, 利于操作

image-20230219202714967

image-20230219203300481

image-20230219203241485

代码+配置实现

引入依赖

        <!--spring data redis 依赖, 即 spring 整合 redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.4.5</version>
        </dependency>
        <!--pool2 对象池依赖-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.9.0</version>
        </dependency>
        <!--实现分布式 session, 即将 Session 保存到指定的 Redis-->
        <!--<dependency>-->
        <!--    <groupId>org.springframework.session</groupId>-->
        <!--    <artifactId>spring-session-data-redis</artifactId>-->
        <!--</dependency>-->

redis配置

public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        //设置连接池工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        //首先解决key的序列化方式
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);

        //解决value的序列化方式
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        //将当前对象的数据类型也存入序列化的结果字符串中
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);

        // 解决jackson2无法反序列化LocalDateTime的问题
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.registerModule(new JavaTimeModule());
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);

        return redisTemplate;
    }
}
#配置redis
  redis:
    host: 192.168.79.202
    port: 6379
    database: 0
    timeout: 10000ms
    lettuce:
      pool:
        #最大连接数
        max-active: 12
        #最大链接阻塞等待时间,默认是-1
        max-wait: 10000ms
        #最大空闲链接,默认是8
        max-idle: 200
        #最小空闲数,默认是0
        min-idle: 5

改造登录接口

@Override
public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
    //接收到mobile和password[midPass]
    String mobile = loginVo.getMobile();
    String password = loginVo.getPassword();
    //判断手机号和密码是否为空
    // if (!StringUtils.hasText(mobile) || !StringUtils.hasText(password)) {
    //     return RespBean.error(RespBeanEnum.LOGIN_ERROR);
    // }
    //判断手机号码是否合格
    // if (!ValidatorUtil.isMobile(mobile)) {
    //     return RespBean.error(RespBeanEnum.MOBILE_ERROR);
    // }
    //查询DB,看看用户是否存在
    User user = userMapper.selectById(mobile);
    if (user == null) {
        // return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        throw new BusinessException(RespBeanEnum.LOGIN_ERROR);
    }
    //将中间密码(客户端|前端经过了一次加密加盐)转换为最终存储到数据库得密码并进行比对
    if (!MD5Util.midPassToDBPass(password, user.getSlat()).equals(user.getPassword())) {
        // return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        throw new BusinessException(RespBeanEnum.LOGIN_ERROR);
    }
    //登录成功
    //给每个用户生成一个ticket-唯一
    String ticket = UUIDUtil.uuid();
    //将登录成功的用户保存到session中
    //实现分布式session,将登录信息存放到redis中
    redisTemplate.opsForValue().set("user:" + ticket, user,30, TimeUnit.MINUTES);
    // request.getSession().setAttribute(ticket, user);
    CookieUtil.setCookie(request, response, "userTicket", ticket);
    return RespBean.success();
}
@Override
public User getUserByTicket(HttpServletRequest request, HttpServletResponse response, String userTicket) {
    if (!StringUtils.hasText(userTicket)) {
        return null;
    }
    User user = (User) redisTemplate.opsForValue().get("user:" + userTicket);
    //获取用户登录信息,更新cookie,刷新过期时间
    if (user != null) {
        CookieUtil.setCookie(request, response, "userTicket", userTicket);
        return user;
    }
    return null;
}
@RequestMapping("/toList")
public String toList(Model model, @CookieValue("userTicket") String userTicket, HttpServletRequest request, HttpServletResponse response) {
    //如果cookie没有生成,则表示没有登录
    if (!StringUtils.hasText(userTicket)) {
        return "login";
    }
    User user = userService.getUserByTicket(request, response, userTicket);
    //用户没有成功登录
    if (null == user) {
        return "login";
    }
    //将user放入到model中
    model.addAttribute("user", user);
    return "goodsList";
}

3.3 实现 WebMvcConfigurer ,优化登录

需求分析/图解

  1. 获取浏览器传递的 cookie 值,进行参数解析,直接转成 User 对象,继续传递
@RequestMapping("/toList")
//通过自定义参数解析器,封装user信息供controller层方法使用
public String toList(Model model, User user) {
    //用户没有成功登录
    if (null == user) {
        return "login";
    }
    //将user放入到model中
    model.addAttribute("user", user);
    return "goodsList";
}

代码+配置实现

自定义参数解析器

/**
 * 自定义参数解析器
 */
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    @Resource
    private UserService userService;

    /**
     * 如果这个方法返回 true 才会执行下面的 resolveArgument 方法
     * 返回 false 不执行下面的方法
     *
     * @param parameter
     * @return
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> parameterType = parameter.getParameterType();
        //如果controller层方法中含有User类型的参数,则执行下面的resolveArgument方法
        return parameterType == User.class;
    }


    /**
     * 这个方法,类似拦截器,将传入的参数,取出 cookie 值,然后获取对应的 User 对象
     * 并把这个 User 对象作为参数继续传递
     *
     * @param parameter
     * @param mavContainer
     * @param webRequest
     * @param binderFactory
     * @return
     * @throws Exception
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest request =
                webRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response =
                webRequest.getNativeResponse(HttpServletResponse.class);
        String userTicket = CookieUtil.getCookieValue(request, "userTicket");
        if (!StringUtils.hasText(userTicket)) {
            return null;
        }
        return userService.getUserByTicket(request, response, userTicket);

    }
}

添加自定义参数解析器到解析器列表中

@EnableWebMvc
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Resource
    private UserArgumentResolver userArgumentResolver;

    /**
     * 静态资源加载
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
    }

    /**
     * 将自定义参数解析器添加到解析器列表中
     * @param resolvers
     */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userArgumentResolver);
    }

}

改造controller层代码

//登录功能
@RequestMapping("/doLogin")
@ResponseBody
public RespBean doLogin
(@Validated LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
    log.info("{}", loginVo);
    return userService.doLogin(loginVo, request, response);
}

3.4使用拦截器进行登录校验

自定义登录认证注解

/**
 * 登录认证注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Authorization {
}

登录认证拦截器

public class AuthorizationInterceptor implements HandlerInterceptor {



    @Resource
    private RedisTemplate redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        Authorization annotation = method.getAnnotation(Authorization.class);
        String userTicket = CookieUtil.getCookieValue(request, "userTicket");
        if (annotation != null) {
            User user = (User) redisTemplate.opsForValue().get("user:" + userTicket);
            if (user != null) {
                //TODO 还可以进一步封装,比如将用户信息封装到ThreadLocal中便于后续接口获取
                return true;
            }
        }
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Resource
    private UserArgumentResolver userArgumentResolver;

    /**
     * 静态资源加载
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //表示拦截所有请求
        registry.addInterceptor(authorizationInterceptor()).addPathPatterns("/**")
                .excludePathPatterns("classpath:/static/")
                .excludePathPatterns("/toLogin")
                .excludePathPatterns("classpath:/templates/");
    }

    @Bean
    AuthorizationInterceptor authorizationInterceptor(){
        return new AuthorizationInterceptor();
    }

}

AuthorizationInterceptor对添加了@Authorization注解的controller层方法统一进行登录认证,无需再每个方法都去做用户是登录的校验操作

@Authorization   
@RequestMapping("/toList")
public String toList(Model model) {
    //将user放入到model中
    model.addAttribute("user", user);
    return "goodsList";
}

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

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

相关文章

【Mysql8.0取消严格区分大小】已安装的mysql8.0取消严格区分大小写及mysql8.0重装与赋权限详解(2023年亲测有效)

【写在前面】其实故事要从my.cnf为空&#xff0c;且lower-case-table-names为0开始&#xff0c;linux环境下mysql8.0及其之后的版本对表名和数据库是严格区分大小写的&#xff0c;从而导致我们运行项目时候会报错Table xxx.QRTZ_LOCKS doesnt exist。但是我已经装好了mysql8.0咋…

17.CSS伪类

举一个简单的例子来说明什么是伪类&#xff1f; 从之前的代码中&#xff0c;如下图&#xff0c;我们像给这两个列表中的某一列单独设置样式&#xff0c;我们该如何做呢&#xff1f; 我们肯定会选择在li标签上添加class去实现&#xff0c;如下 开始标记结束标记实际元素 <…

python--matplotlib(2)

前言 Matplotlib画图工具的官网地址是 http://matplotlib.org/ Python环境下实现Matlab制图功能的第三方库&#xff0c;需要numpy库的支持&#xff0c;支持用户方便设计出二维、三维数据的图形显示&#xff0c;制作的图形达到出版级的标准。 实验环境 Pycharm2020.2.5社区版,w…

算法练习-链表(二)

算法练习-链表&#xff08;二&#xff09; 文章目录算法练习-链表&#xff08;二&#xff09;1. 奇偶链表1.1 题目1.2 题解2. K 个一组翻转链表2.1 题目2.2 题解3. 剑指 Offer 22. 链表中倒数第k个节点3.1 题目3.2 题解3.2.1 解法13.2.2 解法24. 删除链表的倒数第 N 个结点4.1 …

中国智能物流行业市场规模及未来发展趋势

中国智能物流行业市场规模及未来发展趋势编辑中国智能物流行业市场规模正在快速增长。随着电子商务、物流配送、物联网等行业的发展&#xff0c;物流行业需求不断提高&#xff0c;智能物流产品应运而生。智能物流行业主要通过智能化管理、智能路径规划、智能定位、物流配送等方…

Java特性之设计模式【工厂模式】

一、工厂模式 概述 工厂模式&#xff08;Factory Pattern&#xff09;是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式&#xff0c;它提供了一种创建对象的最佳方式 在工厂模式中&#xff0c;我们在创建对象时不会对客户端暴露创建逻辑&#xff0c;并且是通…

投票需要什么流程微信投票互助平台的免费投票平台搭建

“最美家政人”网络评选投票_免费小程序投票推广_小程序投票平台好处手机互联网给所有人都带来不同程度的便利&#xff0c;而微信已经成为国民的系统级别的应用。现在很多人都会在微信群或朋友圈里转发投票&#xff0c;对于运营及推广来说找一个合适的投票小程序能够提高工作效…

Java-集合(5)

Map接口 JDK8 Map接口实现子类的特点 Map和Collection是并列关系&#xff0c;Map用于保存具有映射关系的数据&#xff1a;Key-ValueMap中的key和value可以是任何引用类型的数据&#xff0c;会封装到HashMap$Node对象中Map中的key不允许重复&#xff0c;原因和HashSet一样Map…

2023年美赛MCM 问题C:预测Wordle结果 ​

目录2023年美赛MCM 问题C: 预测Wordle结果 ​1. 背景2. 要求3. 附件1. 数据文件。2. 纽约时报网站上发布的 Wordle 指南4. 参考2023年美赛MCM 问题C: 预测Wordle结果 ​ 1. 背景 Wordle 是纽约时报目前每天提供的流行拼图。 玩家尝试通过在六次或更少的尝试中猜测一个五个字母…

记录一次Binder内存相关的问题导致APP被杀的BUG排查过程

事情的起因的QA压测过程发生进程号变更&#xff0c;怀疑APP被杀掉过&#xff0c;于是开始看日志 APP的压测平台会上报进程号变更时间点&#xff0c;发现是在临晨12&#xff1a;20分&#xff0c;先大概确定在哪个日志文件去找关键信息一开始怀疑是crash&#xff0c;然后就在日志…

shiro CVE-2020-1957

0x00 前言 在之前只是单纯的复现了漏洞&#xff0c;没有记笔记&#xff0c;所以补充了这篇分析笔记。 影响版本&#xff1a;shiro < 1.5.2 0x01 环境搭建 环境用的是&#xff1a;https://github.com/lenve/javaboy-code-samples/tree/master/shiro/shiro-basic 0x02 漏…

用python实现对AES加密的视频数据流解密

密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijndael加密法。 在做网络爬虫的时候,会遇到经过AES加密的数据,可以使用python来进行解密。 在做爬虫的时候,通常可以找到一个key,这个key是一个十六进制的一串字符,这传字符是解密的关键。所以对于…

SpringBoot2.X整合ClickHouse项目实战-从零搭建整合(三)

一、ClickHouseSpringBoot2.XMybatisPlus整合搭建 二、需求描述和数据库准备 三、ClickHouse统计SQL编写实战和函数使用 四、ClickHouseSpringBoot2.X案例-基础模块搭建 controller/request层 mapper层 model层 service层 五、ClickHouseSpringBoot2.X案例-数据统计接口 …

城市轨道交通供电系统研究(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密…

一文告诉你什么是财务数据治理?

大家好&#xff0c;我是梦想家Alex&#xff0c;今天是周末&#xff0c;就不给大家分享技术文了&#xff5e;应出版社老师推荐&#xff0c;文末给大家送几本DAMA中国主席力荐&#xff0c;20位行业专家历时2年共同打造的《财务数据治理实战》&#xff0c;将数据治理理论应用于财务…

怎样查询PMP成绩?

【如何查询成绩】1、输入网址&#xff08;PMI官网&#xff0c;不知道网址的私戳&#xff09;&#xff0c;点击 Log In如果忘记 PMI 的账号和密码了&#xff0c;怎么办&#xff1f;可以在你报名机构官网的个人中心的学习中心的我的报名处查看 PMI 的注册名和密码2、点击 Exam An…

CMake 入门学习4 软件包管理

CMake 入门学习4 软件包管理一、Linux下的软件包管理1. 检索已安装的软件包2. 让自己编译软件支持pkg-config搜索3. 在CMakeLists查找已安装的软件包二、适合Windows下的包管理工具1. vcpkg2. Conan(1) 安装Conan(2) 配置Conan(3) 创建工程(4) 安装依赖库(5) 使用依赖库三、CMa…

汉字----dgfont

Abstract 字符生成是一个具有挑战性的问题,特别是对于一些由大量字符组成的书写系统,近年来受到了广泛的关注。然而,现有的字体生成方法通常是在监督学习中。它们需要大量的配对数据,这是劳动密集型和昂贵的收集。此外,常见的图像到图像转换模型通常将风格定义为纹理和颜…

golang入门笔记——内存管理

文章目录自动内存管理概念自动内存管理-相关概念&#xff1a;追踪垃圾回收&#xff1a;分代GC&#xff08;Generational GC&#xff09;引用计数内存分配Go内存分配-分块Go内存分配——多级缓存Go内存管理优化Balanced GC自动内存管理 概念 1.动态内存 程序在运行时根据需求…

大数据全系安装

内容版本号CentOS7.6.1810ZooKeeper3.4.6Hadoop2.9.1HBase1.2.0MySQL5.6.51HIVE2.3.7Sqoop1.4.6flume1.9.0kafka2.8.1scala2.12davinci3.0.1spark2.4.8flink1.13.5 1. 下载CentOS 7镜像 CentOS官网 2. 安装CentOS 7系统——采用虚拟机方式 2.1 新建虚拟机 2.2.1 [依次选择]-&…