章节索引 :

使用RabbitMQ优化用户登录功能

1. 前言

Hello,大家好。通过上述几个小节的介绍,我们已经对 RabbitMQ 中的消息发送模式有了代码实现层面的了解,这些实操代码是应用 RabbitMQ 基础中的基础,同学们必须要掌握并记忆。

那么,从本节开始,会为大家带来,在我们的实际的一个日常开发工作中,针对常用的几个功能,我们应该如何使用 RabbitMQ 来进行实现,同时,通过对常用功能的 RabbitMQ 集成,可以提升传统功能实现中的一些性能等指标,这对后续传统功能的优化是很有必要的。

本小节会为同学们介绍,如何使用 RabbitMQ 消息中间件,去优化我们使用传统的方式,来实现的用户登录功能点,包括对传统用户登录功能的概述,以及使用 RabbitMQ 进行优化的关键步骤,希望同学们可以对本节内容有所了解。

本节主要内容:

  • 传统用户登录功能概述;

  • 使用 RabbitMQ 优化传统用户登录功能。

2.传统用户登录功能概述

2.1 传统实现方案介绍

众所周知,我们现在的信息化系统项目,项目的首要功能模块,基本上都会要求我们来设计并实现一个完备的用户模块,而这个用户模块中,必定会包含用户注册与用户登录功能点。

只有在项目中实现了用户注册与用户登录这两个最基本的功能点,我们的项目功能才能继续向后进行开发,因为项目后续的功能,或多或少都会与这两个功能点相联系。

所以,这就要求我们在实现用户注册与用户登录功能时,要从多方面进行考虑, 如果有一点我们没有考虑到,很可能在进行项目后续功能开发时,还要回过头去,修改这两个基本功能,从而与项目后续的功能相结合。

那么,传统的用户登录功能的实现方案是什么样的呢?

传统用户登录功能流程介绍

我们首先接触用户登录功能或者用户注册功能,有绝大部分同学是在自己的学校内,也就是学校老师教授我们的内容,往往这种实现方式都会存在诸多问题,下面让我们来看一下这种方式是如何实现的。

针对用户登录功能,对于后端开发的同学,我们首先需要接收前端传递过来的数据,也就是用户的登录数据,然后我们会根据这一用户登录数据来进行相应的逻辑检验。

这些逻辑校验包括对用户的名称进行检测,以查验当前要登录的用户是否在我们的系统中存在,如果不存在,则提示用户不存在与本系统中,如果存在,则检测用户所输入的登录密码是否与用户注册时所填写的一致,如果不一致,则提示用户,用户登录密码错误。

如果当前要登录的用户在本系统中存在,且用户所输入的登录密码与用户注册时所填写的登录密码一致,则表明用户登录成功,此时,需要提示用户登录成功,并跳转到我们的系统首页。下面,我们来通过一个功能流程图来体现上述的业务流转过程:

通过上述功能流程图,我们可以很清楚地理解上述传统用户登录功能的实现过程,如果没有理解的同学,建议多看几次,理解这一流程之后我们再继续学习下面的内容。

传统用户登录功能代码实现

以 SpringBoot 框架为例,我们来实现一下上述的用户登录功能,实现代码如下所示:

实现代码:

public Response<User> userLogin(User user, HttpSession session){
    int userExistsCount = userMapper.selectUserExixtsByUsername(user.getUsername());
    if (userExistsCount <= 0){
        return Response.createByError("用户不存在");
    }
    User userLogin = userMapper.selectLoginUserByUsernamePassword(user.getUsername(), user.getPassword());
    if (userLogin != null){
        userLogin.setPassword("");
        session.setAttribute("loginUser", userLogin);
        return Response.createBySuccess("登录成功", userLogin);
    }
    return Response.createByError("用户登录密码输入错误,请重新输入");
}

代码解释:

第 1 行,我们定义了一个名为 userLogin 的方法来处理用户登录的业务逻辑,该方法的返回值是 Response 类型,并且泛型被指定为 User 类型,这表明,我们的方法最终会返回 User 用户数据。

在该方法中,我们定义了两个参数,分别是 User 类型的 user 参数,以及 HttpSession 类型的 session 参数。其中,user 参数就表示用户登录时所填入的登录数据,session 参数则表示存储用户登录状态和登录数据的参数。

第 2-5 行,我们通过获取到用户的用户名,并且调用数据库 ORM 层的方法,来对系统数据库进行查询,查询当前用户名是否存在我们的数据库中,如果存在,则表明用户注册过我们的系统,即这个用户是在我们的系统中的。如果不存在,则会提示用户当前用户名不存在。

第 6-14 行,我们通过获取到的用户的用户名和用户登录密码,调用数据库 ORM 层的犯法,来对系统数据库进行查询,查询当前用户名和密码是否在系统中匹配。

即,只有用户名匹配的话是不行的,还要将这个用户名和其所对应的密码进行匹配,当使用这两个条件进行匹配查询,且可以正确查询到结果时,表明用户已经登录成功了,此时,我们可以将用户登录的数据放到 session 中,以备后续功能的使用,与此同时,我们还应该提示用户登录成功了。

如果根据这两个查询条件,没有查询到任何数据,即 userLogin 变量为 null 时,则表明当前用户名是存在的,但是当前登录用户输入的登录密码和注册时所输入的是不一样的,此时,系统会提示用户,用户登录密码错误,可以进行重试。

Tips:
1. 在我们的 userLogin 方法中存在一个参数 session ,我们只是对 session 进行了简单介绍,因为这不属于我们的重点内容,所以,有不了解的同学可以自行查阅资料来了解;
2. 可以看到,在 userLogin 方法中,存在一行 userLogin.setPassword("") ,这句代码的作用是将用户登录成功后的密码设置为空,并不会同步更新数据库,并将 userLogin 用户数据返回给前端,出于安全考虑,这里将密码设置为空了,前端是获取不到用户的登录密码的;
3. userLogin 方法中所使用的 userMapper 为 MyBatis 数据库 ORM 框架所定义的内容,有不了解的同学可以自行查阅资料进行了解,本小节不会对其进行介绍。

通过上述代码的编写,我们基本上实现了传统的用户登录功能,但是这种登录功能是最简单的实现,完全没有考虑其他因素,如果我们在实际项目中这样来实现,那么在开发后续功能时,必定会出现一些意料之中的问题。

那么,我们采用这种实现方式所实现的用户登录功能到底有哪些弊端呢?下面就让我们一探究竟。

2.2 传统实现方案中存在的弊端

整体来说,我们传统的实现用户登录功能的业务逻辑还是正确的, 不管我们再怎么对用户登录功能进行优化,这个传统的用户登录功能业务流程始终不会发生改变,发生改变的只是我们的代码实现层。

那么,传统实现方案中存在的弊端,也就存在于我们的功能代码实现层上。

弊端一 用户登录状态的处理

对于上述代码来说,我们处理用户登录状态所采用的手段,是通过将用户登录成功后的数据存放在服务端中的 session 中,后续如果需要使用到用户登录状态,我们可以直接对 session 进行判断,并返回相应的判断结果。

但是,这种模式如果遇到了前后端分离的项目,我们就无法再使用 session 了,因为前端框架不能直接操控我们后端的 session ,所以,我们就要把 session 替换掉,替换成前后端均可操控的手段来实现。

弊端二 高并发环境下容易出现的问题

如果我们的项目在使用过程中出现了用户激增的情况,即会有越来越多的人来访问我们的用户登录功能,如果我们只是采用上述的实现方式来处理,不会这种情况进行考虑,那么我们的用户登录功能就会在非常短的一段时间过后瘫痪,我们的用户登录功能不会再返回任何响应数据。

这就是高并发环境下传统的用户登录功能最容易出现问题的地方,而这一地方则体现在用户登录前和用登录后的数据不一致问题上。

即,在高并发环境下,我们的用户 A 进行了登录,与此同时,我们的用户 B 也进行了登录,这两个用户进行登录的时刻恰好重叠了,由于我们并没有对这种场景进行处理,所以,在这两个用户登录之后,就有可能出现返回给用户 A 的用户数据,其实是用户 B 的。

除了数据不一致问题,还有一种问题也比较容易出现,那就是当我们的用户数激增时,同一时刻进行登录的用户也随之激增,我们的用户登录功能接口无法在一瞬间响应这么多的用户登录请求,导致了后续登录的用户只能等待,只能等待当先用户登录完成之后,才能进行登录。

这个等待过程是不确定的,可能很长,也可能很短,这就对用户的体验造成了非常严重的不良影响。

上述就是两个在传统用户登录功能中,出现的显而易见的问题,我们会对这两个功能进行优化,并且会使用 RabbitMQ 对第二个弊端进行优化。

3 使用 RabbitMQ 优化传统用户登录功能

3.1 优化用户登录状态的处理问题

前面我们已经介绍了,在传统用户登录功能中,当我们的项目架构采用前后端分离的架构方式来管理时,出现的用户登录状态的问题,那么我们可以怎样来优化这个问题呢?

优化出现的用户登录状态问题,不需要我们使用 RabbitMQ 来处理,我们可以将 session 参数使用一种 token 机制来替代掉。

即在处理用户状态时,我们不使用 session 来存储,而是使用 token 来进行存储,这个 token 我们一般会使用 JWT 框架来生成,并为生成的 token 设置一个有效期。

这样一来,当前端发来用户登录请求时,就会将用户登录成功的 token 进行传递,然后我们后台需要接收这个 token ,并将这个加密的 token 进行解析。这样一来,前端在用户登录成功之后,会将这个 token 保存到前端的状态机中,而后端默认是会存储这个 token 的。

这就实现了一种前后端都可以操作这个 token 的功能,通过这样的优化,无论是前后端分离项目,还是其他类型的一些项目,我们都可以很好地处理用户的登录状态和用户数据。

3.2 优化高并发环境下的用户登录功能

在高并发环境下,我们的用户登录功能需要在同一时刻,处理大量的用户请求,如果我们的用户登录功能没有考虑到这样的业务场景,也就不会对这种业务场景进行优化,所以,一旦请求量上来之后,我们的用户登录功能肯定就会瘫痪,不能正常使用了。

我们可以从两个角度去优化高并发环境下的用户登录功能。第一个角度就是优化我们的数据库访问频率。

角度一:优化数据库访问频率

我们都知道,在后台程序代码中,比较耗时的操作无疑就是访问数据库了,因为访问数据库之前,我们需要先通过代码的方式,来将我们的项目与数据库相连接,最后,通过 ORM 框架来达到对数据库中的数据增删改查的一个目的。

我们在对数据库中的数据进行增删改查时,需要我们将处理好的数据传入到数据库中,然后数据库通过执行 SQL 脚本来根据我们的数据去处理数据库中的数据,并最后返回给我们处理数据的结果。

在用户登录功能中,访问数据库的次数一般都是两次,第一次是校验当前要登录的用户名在系统中是否存在;第二次是校验当前要登录的用户名和密码是否在系统中匹配。 只有这两个校验通过之后,用户才能成功登录,而数据库的耗时操作也就出现在这两个过程中。

那么我们应该怎么来优化呢?

我们可以使用 Redis 来缓存我们的用户请求数据,即当有用户请求数据请求我们的用户登录功能接口时,我们会首先进行第一次的数据库操作,此时,在数据库将操作结果返回给我们时,我们可以将这一结果缓存在 redis 中,后续如果再有相同的请求时,我们可以直接访问 redis ,而不是再次重复地去访问我们的数据库。

这样可以提高我们的服务响应速度,因为访问 redis 的速度要比单纯访问数据库的速度快很多。所以,第二次的数据库操作我们也可以这样来进行处理。

Tips: 关于什么是 Redis ,以及 redis 的一些基本操作,我们会在后续的小节中进行介绍,这里只介绍功能优化的思路,感兴趣的同学可以自行查阅资料实现。

这是第一种优化思路,接下来让我们看第二种优化思路,即优化我们的用户请求序列。

角度二:优化用户请求序列

此种优化措施,就需要使用我们的 RabbitMQ 了。

我们都知道,高并发环境下的请求数量是非常多的,那么,对于一个用户登录功能接口而言,在高并发环境下接收的用户请求也是非常多的,而且这些用户请求都是无序的。

即,例如我们在一瞬间有 5000 个用户登录请求,根据计算机 CPU 以及操作系统的处理时序可以得出,在这 5000 个用户登录请求中,只要谁先获取到了 CPU 资源,谁就会先执行,那么其他没有获取到 CPU 资源的请求就只能等待,这就是高并发环境下的资源抢占问题。

对于 CPU 来说,我们的一个用户请求就代表着是一个计算机线程,比如我们现在有两个用户请求,即两个线程来访问我们的用户登录接口,我们分别使用线程 A 和线程 B 来命名。

假设我们的线程 A 先访问到了我们的用户登录接口,而线程 B 比线程 A 晚 1 秒才访问到,那么,当我们的线程 A 在执行业务逻辑时,线程 B 也会跟着执行。

如果我们的线程 A 的用户数据比较长,需要执行数据库查询所需的时间较长,而线程 B 的用户数据则比较端,需要执行数据库查询所需的时间较短,那么,在统一时刻,可能晚于线程 A 才访问到的用户登录接口线程 B 就会先于线程 A 执行结束。

这就会造成:我们实际上是处理的线程 A 的业务逻辑,但是返回的时候却将线程 B 的用户数据进行了返回,这也是高并发环境下非常容易出现的一个问题。

那么我们应该怎么来优化呢?

我们可以使用 RabbitMQ 来作为一个消息队列,当用户请求来请求我们的服务时,我们可以将所有的用户请求放入到我们的 RabbitMQ 消息队列中,由于 RabbitMQ 消息队列可以设置单条消息进行消费,所以,我们的用户请求在处理时就会逐条进行处理,不会出现上述所描述的问题。

我们来简单看下这种问题的优化代码,优化代码如下所示:

实现代码:

public Response<User> userLogin(User user, HttpSession session){
    rabbitTemplate.convertAndSend("userLoginQueue", "user.login", user);
    // 获取用户数据并处理用户登录业务逻辑
}

代码解释:

第 2 行,我们使用了 rabbitTemplate 的 convertAndSend 方法,来将我们的用户登录数据发送到 RabbitMQ Server 中。

由于消费者的实现比较复杂,考虑到篇幅原因,所以在这里代码就没有给出。

通过上述代码,当有用户请求我们的用户登录接口时,会首先将该请求的用户登录数据存储到我们的 RabbitMQ Server 中,存储完成后,接着会从 RabbitMQ Server 中将该消息进行取出并消费,只有一条消息被消费之后,RabbitMQ 才会继续消费下一条消息,这就保证了用户请求的有序性,也就不会出现上述所提到的问题。

Tips: 我们在优化用户登录功能时,一定要先理解我们所优化问题的产生原因,这样我们才能从根本上优化这一问题。

4. 小结

本小节为同学们详细介绍了传统的用户登录功能的基本概念和基本实现方法,以及在传统的用户登录功能中所出现的可优化的问题,针对出现频率较高的问题,我们分别介绍了不同问题的不同优化措施,并针对可以使用 RabbitMQ 进行优化的场景,我们使用了 Spring 生态中的 RabbitMQ 中的方法来进行了优化。

RabbitMQ 简介
RabbitMQ 简介
RabbitMQ 基础
Win环境-SpringBoot集成MQ Mac OS环境下RabbitMQ的安装与集成 Linux环境下RabbitMQ安装与服务命令实操 RabbitMQ 核心基础概念详解 RabbitMQ 基础核心配置文件介绍 RabbitMQ 消息发送原理概述 RabbitMQ 消息发送模式详解 RabbitMQ 交换机详解 RabbitMQ 消息监控平台介绍
RabbitMQ 基础特性与进阶
RabbitMQ的幂等性概念 RabbitMQ中消息确认与返回机制 RabbitMQ中消费者ACK与重回队列机制 RabbitMQ中的TTL消息是什么 死信队列基础概念详解与配置
RabbitMQ 整合 Spring 生态链
RabbitAdmin基础概念详解与配置 RabbitTemplate基础概念详解与配置 消息容器介绍 消息适配器概念讲解与基本属性介绍 消息适配器应用实操 消息转换器概念讲解与基本属性介绍 消息转换器应用实操
RabbitMQ 集群基础
Warren模式与Shovel模式介绍 Mirror模式与Federation模式介绍 RabbitMQ集群配置文件概述 KeepAlived组件基础属性介绍 HaProxy组件基础属性介绍 RabbitMQ集群故障排查与恢复概述
RabbitMQ 实战
消息发送模式实战之直接模式与主题模式 消息发送模式实战之发布订阅模式 消息发送模式实战之普通队列模式与工作队列模式 使用RabbitMQ优化用户登录功能 使用RabbitMQ优化用户注册功能 RabbitMQ集成KeepAlived组件实操 RabbitMQ集群集成HaProxy组件实操 使用RabbitMQ打造扛得住的高并发环境(一) 使用RabbitMQ打造扛得住的高并发环境(二) 使用RabbitMQ打造扛得住的高并发环境(三)