一,MQ介绍

微服务一旦拆分,必然涉及到服务之间的相互调用,目前我们服务之间调用采用的都是基于 OpenFeign 的调用。这种调用中,调用者发起请求后需要等待 服务提供者执行业务返回结果后,才能继续执行后面的业务。也就是说调用者在调用过程中处于阻塞状态,因此我们称这种调用方式为同步调用,也可以叫 同步通讯 。但在很多场景下,我们可能需要采用异步通讯的方式,为什么呢?

我们先来看看什么是同步通讯和异步通讯。如图:

2953321-20250425193521454-1018839797.png

解读:

  • 同步通讯:就如同打视频电话,双方的交互都是实时的。因此同一时刻你只能跟一个人打视频电话。
  • 异步通讯:就如同发微信聊天,双方的交互不是实时的,你不需要立刻给对方回应。因此你可以多线操作,同时跟多人聊天。

两种方式各有优劣,打电话可以立即得到响应,但是你却不能跟多个人同时通话。发微信可以同时与多个人收发微信,但是往往响应会有延迟。

所以,如果我们的业务需要实时得到服务提供方的响应,则应该选择同步通讯(同步调用)。而如果我们追求更高的效率,并且不需要实时响应,则应该选择异步通讯(异步调用)。

1.1 同步调用

之前说过,我们现在基于 OpenFeign 的调用都属于是同步调用,那么这种方式存在哪些问题呢?

2953321-20250425193654689-467389891.png

目前我们采用的是基于 OpenFeign 的同步调用,也就是说业务执行流程是这样的:

  • 支付服务需要先调用用户服务完成余额扣减
  • 然后支付服务自己要更新支付流水单的状态
  • 然后支付服务调用交易服务,更新业务订单状态为已支付

三个步骤依次执行。

这其中就存在 3 个问题:

  1. 拓展性差

    我们目前的业务相对简单,但是随着业务规模扩大,产品的功能也在不断完善。

    在大多数电商业务中,用户支付成功后都会以短信或者其它方式通知用户,告知支付成功。假如后期产品经理提出这样新的需求,你怎么办?是不是要在上述业务中再加入通知用户的业务?

    某些电商项目中,还会有积分或金币的概念。假如产品经理提出需求,用户支付成功后,给用户以积分奖励或者返还金币,你怎么办?是不是要在上述业务中再加入积分业务、返还金币业务?

    最终你的支付业务会越来越臃肿:
    2953321-20250425193738339-1379857353.png

    也就是说每次有新的需求,现有支付逻辑都要跟着变化,代码经常变动,不符合开闭原则,拓展性不好。

  2. 性能下降

    由于我们采用了同步调用,调用者需要等待服务提供者执行完返回结果后,才能继续向下执行,也就是说每次远程调用,调用者都是阻塞等待状态。最终整个业务的响应时长就是每次远程调用的执行时长之和:

2953321-20250425193814602-555019084.png
假如每个微服务的执行时长都是 50ms,则最终整个业务的耗时可能高达 300ms,性能太差了。

  1. 级联失败

    由于我们是基于 OpenFeign 调用交易服务、通知服务。当交易服务、通知服务出现故障时,整个事务都会回滚,交易失败。

而要解决这些问题,我们就必须用异步调用的方式来代替同步调用

1.2 异步调用

异步调用方式其实就是基于消息通知的方式,一般包含三个角色:

  • 消息发送者:投递消息的人,就是原来的调用方
  • 消息Broker:管理、暂存、转发消息,你可以把它理解成微信服务器
  • 消息接收者:接收和处理消息的人,就是原来的服务提供方

2953321-20250425193921433-1219813132.png

在异步调用中,发送者不再直接同步调用接收者的业务接口,而是发送一条消息投递给消息 Broker。然后接收者根据自己的需求从消息 Broker 那里订阅消息。每当发送方发送消息后,接受者都能获取消息并处理。

这样,发送消息的人和接收消息的人就完全解耦了。

还是以余额支付业务为例:

2953321-20250425193950358-96958662.png

除了扣减余额、更新支付流水单状态以外,其它调用逻辑全部取消。而是改为发送一条消息到 Broker。而相关的微服务都可以订阅消息通知,一旦消息到达 Broker,则会分发给每一个订阅了的微服务,处理各自的业务。

假如产品经理提出了新的需求,比如要在支付成功后更新用户积分。支付代码完全不用变更,而仅仅是让积分服务也订阅消息即可:

2953321-20250425194029829-305955600.png

不管后期增加了多少消息订阅者,作为支付服务来讲,执行问扣减余额、更新支付流水状态后,发送消息即可。业务耗时仅仅是这三部分业务耗时,仅仅 100ms,大大提高了业务性能。

另外,不管是交易服务、通知服务,还是积分服务,他们的业务与支付关联度低。现在采用了异步调用,解除了耦合,他们即便执行过程中出现了故障,也不会影响到支付服务。

综上,异步调用的优势包括:

  • 耦合度更低
  • 性能更好
  • 业务拓展性强
  • 故障隔离,避免级联失败

当然,异步通信也并非完美无缺,它存在下列缺点:

  • 完全依赖于Broker的可靠性、安全性和性能
  • 架构复杂,后期维护和调试麻烦

1.3 技术选型

消息 Broker,目前常见的实现方案就是消息队列(MessageQueue),简称为 MQ.

目比较常见的 MQ 实现:

  • ActiveMQ
  • RabbitMQ
  • RocketMQ
  • Kafka

几种常见 MQ 的对比:

RabbitMQActiveMQRocketMQKafka
公司/社区RabbitApache阿里Apache
开发语言ErlangJavaJavaScala&Java
协议支持AMQP,XMPP,SMTP,STOMPOpenWire,STOMP,REST,XMPP,AMQP自定义协议自定义协议
可用性一般
单机吞吐量一般非常高
消息延迟微秒级毫秒级毫秒级毫秒以内
消息可靠性一般一般
  • 追求可用性:Kafka、 RocketMQ 、RabbitMQ
  • 追求可靠性:RabbitMQ、RocketMQ
  • 追求吞吐能力:RocketMQ、Kafka
  • 追求消息低延迟:RabbitMQ、Kafka

二,RabbitMQ

RabbitMQ 是基于 Erlang 语言开发的开源消息通信中间件,官网地址:https://www.rabbitmq.com/

2.1 安装

基于 Docker 来安装 RabbitMQ,使用下面的命令即可:

docker run -d \
  --name rabbit-mq \
  --hostname rabbit-mq \
  --network tool-net \
  --restart unless-stopped \
  -p 5672:5672 \
  -p 15672:15672 \
  -e RABBITMQ_DEFAULT_USER=root \
  -e RABBITMQ_DEFAULT_PASS=123456 \
  -v /opt/rabbitmq/data:/var/lib/rabbitmq \
  -v /opt/rabbitmq/log:/var/log/rabbitmq \
  -v mq-plugins:/plugins \
  rabbitmq:3.8-management

安装完成后,我们访问 http://ip:15672 即可看到管理控制台。首次访问需要登录,默认的用户名和密码在配置文件中已经指定了。

登录后即可看到管理控制台总览页面:

2953321-20250425200826840-1607256322.png

RabbitMQ 对应的架构如图:

2953321-20250425200545475-1006444911.png

其中包含几个概念:

  • publisher:生产者,也就是发送消息的一方
  • consumer:消费者,也就是消费消息的一方
  • queue:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理
  • exchange:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。
  • virtual host:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue

2.2 收发消息

2.2.1 交换机

我们打开 Exchanges 选项卡,可以看到已经存在很多交换机:

2953321-20250425201220589-1747771249.png

我们点击任意交换机,即可进入交换机详情页面。仍然会利用控制台中的 publish message 发送一条消息:

2953321-20250425201256836-666279493.png

2953321-20250425201307275-1705063342.png

这里是由控制台模拟了生产者发送的消息。由于没有消费者存在,最终消息丢失了,这样说明交换机没有存储消息的能力。

2.2.2 队列

我们打开Queues选项卡,新建一个队列:

2953321-20250425201344357-495034293.png

命名为hello.queue1

2953321-20250425201357157-1585600472.png

再以相同的方式,创建一个队列,密码为hello.queue2,最终队列列表如下:

2953321-20250425201410414-1648401711.png

此时,我们再次向amq.fanout交换机发送一条消息。会发现消息依然没有到达队列!怎么回事呢?

  • 发送到交换机的消息,只会路由到与其绑定的队列,因此仅仅创建队列是不够的,我们还需要将其与交换机绑定。

2.2.3 绑定关系

点击Exchanges选项卡,点击amq.fanout交换机,进入交换机详情页,然后点击Bindings菜单,在表单中填写要绑定的队列名称:

2953321-20250425201508085-1958033462.png

相同的方式,将 hello.queue2 也绑定到改交换机。

最终,绑定结果如下:

2953321-20250425201521148-1836930607.png

2.2.4 发送消息

再次回到 exchange 页面,找到刚刚绑定的amq.fanout,点击进入详情页,再次发送一条消息:

2953321-20250425201628904-1959711170.png

回到Queues页面,可以发现hello.queue中已经有一条消息了:

2953321-20250425201640695-649666032.png

点击队列名称,进入详情页,查看队列详情,这次我们点击 get message:

2953321-20250425201652562-1689463765.png

可以看到消息到达队列了:

2953321-20250425201704395-1257510536.png

这个时候如果有消费者监听了 MQ 的hello.queue1hello.queue2队列,自然就能接收到消息了。

2.3 数据隔离

2.3.1 用户管理

点击Admin选项卡,首先会看到 RabbitMQ 控制台的用户管理界面:

2953321-20250425201936832-2133538836.png

这里的用户都是 RabbitMQ 的管理或运维人员。目前只有安装 RabbitMQ 时添加的root这个用户。仔细观察用户表格中的字段,如下:

  • Nameroot,也就是用户名
  • Tagsadministrator,说明itheima用户是超级管理员,拥有所有权限
  • Can access virtual host/,可以访问的virtual host,这里的/是默认的virtual host

对于小型企业而言,出于成本考虑,我们通常只会搭建一套 MQ 集群,公司内的多个不同项目同时使用。这个时候为了避免互相干扰, 我们会利用virtual host的隔离特性,将不同项目隔离。一般会做两件事情:

  • 给每个项目创建独立的运维账号,将管理权限分离。
  • 给每个项目创建不同的virtual host,将每个项目的数据隔离。

比如,我们给商城创建一个新的用户,命名为user-shop

2953321-20250425202102209-217132483.png

你会发现此时 user-shop 用户没有任何virtual host的访问权限:

2953321-20250425202129397-27430179.png

2.3.2 virtual host

我们先退出登录:

2953321-20250425202219852-802158327.png

切换到刚刚创建的 user-shop 用户登录,然后点击Virtual Hosts菜单,进入virtual host管理页:

2953321-20250425202806781-431014222.png

可以看到目前只有一个默认的virtual host,名字为 /

我们可以给商城项目创建一个单独的virtual host,而不是使用默认的/

2953321-20250425202844715-980945356.png

创建完成后如图:

2953321-20250425202859262-187360709.png

由于我们是登录userShop账户后创建的virtual host,因此回到users菜单,你会发现当前用户已经具备了对/shop这个virtual host的访问权限了:

2953321-20250425202934879-2029817925.png

此时,点击页面右上角的virtual host下拉菜单,切换virtual host/hmall

2953321-20250425202955528-1130707384.png

然后再次查看 queues 选项卡,会发现之前的队列已经看不到了:

2953321-20250425203007255-214602405.png

这就是基于virtual host 的隔离效果。