微服务—RabbitMQ

目录

初识MQ

同步和异步通讯

同步通讯的优缺点

异步调用方案

异步通信优缺点

常见MQ技术对比

RabbitMQ快速入门

安装RabbitMQ

RabbitMQ整体架构与相关概念

常见消息模型​编辑

入门案例

SpringAMQP

基本介绍

SpringAMQP案例------模拟HelloWorld消息模型

SpringAMQP案例------模拟WorkQueue消息模型

SpringAMQP案例------模拟发布订阅消息模型

发布订阅消息模型介绍

案例------FanoutExchange

案例------DirectExchange

案例------TopicExchange

消息转换器


初识MQ

同步和异步通讯

微服务间通讯有同步和异步两种方式:同步通讯就像打电话,需要实时响应,异步通讯就像发邮件,不需要马上回复。两种方式各有优劣,打电话可以立即得到响应,但是你却不能跟多个人同时通话。发送邮件可以同时与多个人收发邮件,但是往往响应会有延迟。


同步通讯的优缺点

优点:

时效性较强,可以立即得到结果。
缺点:

  1. 耦合度高:每次加入新的需求,都要修改原来的代码;

  2. 性能下降:调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和;

  3. 资源浪费:调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源;

  4. 级联失败:如果服务提供者出现问题,所有调用方都会跟着出问题如同多米诺骨牌一样,迅速导致整个微服务群故障。


异步调用方案

我们以购买商品为例,用户支付后需要调用订单服务完成订单状态修改,调用物流服务,从仓库分配响应的库存并准备发货。

在事件模式中,支付服务是事件发布者,在支付完成后只需要发布一个支付成功的事件,事件中带上订单id。

订单服务和物流服务是事件订阅者,订阅支付成功的事件,监听到事件后完成自己业务即可。

为了解除事件发布者与订阅者之间的耦合,两者并不是直接通信,而是有一个中间Broker。发布者发布事件到Broker,不关心谁来订阅事件。订阅者从Broker订阅事件,不关心谁发来的消息。


异步通信优缺点

优点:

  1. 吞吐量提升:无需等待订阅者处理完成,响应更快速;

  2. 故障隔离:服务没有直接调用,不存在级联失败问题;

  3. 调用间没有阻塞,不会造成无效的资源占用;

  4. 耦合度极低,每个服务都可以灵活插拔,可替换;

  5. 流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件。
    缺点:

  6. 架构复杂了,业务没有明显的流程线,不好管理;

  7. 需要依赖于Broker的可靠、安全、性能。


常见MQ技术对比


RabbitMQ快速入门

安装RabbitMQ

步骤1 在线拉取镜像

复制代码
​​​​​​​docker pull rabbitmq:3-management

步骤2 执行下面的命令来运行MQ容器,在命令行中设置用户名和密码

复制代码
docker run \
 -e RABBITMQ_DEFAULT_USER=root \
 -e RABBITMQ_DEFAULT_PASS=123456 \
 --name mq \
 --hostname mq1 \
 -p 15672:15672 \
 -p 5672:5672 \
 -d \
 rabbitmq:3-management

步骤3 进入RabbitMQ管理平台

安装好后,安装好后通过IP+端口访问管理界面。管理界面端口是15672,tcp连接的端口是5672。在浏览器中访问192.168.237.128:1567进入RabbitMQ管理平台,其中192.168.237.128为虚拟机ip地址。


RabbitMQ整体架构与相关概念

RabbitMQ架构图

RabbitMQ中的几个概念

channel:操作MQ的工具

exchange:路由消息到队列中

queue:缓存消息

virtualhost:虚拟主机,是对queue、exchange等资源的逻辑分组


常见消息模型


入门案例

发布者发送消息代码:

java 复制代码
public class PublisherTest {
    @Test
    public void testSendMessage() throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.150.101");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("itcast");
        factory.setPassword("123321");
        // 1.2.建立连接
        Connection connection = factory.newConnection();

        // 2.创建通道Channel
        Channel channel = connection.createChannel();

        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);

        // 4.发送消息
        String message = "hello, rabbitmq!";
        channel.basicPublish("", queueName, null, message.getBytes());
        System.out.println("发送消息成功:【" + message + "】");

        // 5.关闭通道和连接
        channel.close();
        connection.close();

    }
}

运行之后可以在RabbitMQ管理平台看到队列里已经有一个消息

点击该消息,通过Get Message可以查看接收到的消息内容

消费者建立连接代码:

java 复制代码
public class ConsumerTest {
    @Test
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.237.128");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("root");
        factory.setPassword("123456");
        // 1.2.建立连接
        Connection connection = factory.newConnection();

        // 2.创建通道Channel
        Channel channel = connection.createChannel();

        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);

        // 4.订阅消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 5.处理消息
                String message = new String(body);
                System.out.println("接收到消息:【" + message + "】");
            }
        });
        System.out.println("等待接收消息。。。。");
    }
}

运行之后可以在RabbitMQ管理平台发现已经建立起一个连接,并且能够在idea控制台观察到从消息队列里获取的消息。

​​​​​​​

小结:

发布者和生产者的代码中,都有创建队列这一部分,是否会产生冲突?

由于无法确定发布者和生产者运行的前后顺序,为避免寻找不到所需绑定的队列,因此都需要创建队列,如果该队列已经存在,也不会产生冲突。

基本消息队列的消息发送流程

1.建connection

2.创建channel

3.利用channel声明队列

4.利用channel向队列发送消息

基本消息队列的消息接收流程

1.建connection

2.创建channel

3.利用channel声明队列

4.定义consumer的消费行为handleDelivery05.利用channel将消费者与队列绑定


SpringAMQP

基本介绍

AMQP(Advanced Message Queuing Protocol)是用于在应用程序或之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求。

Spring AMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象spring-rabbit是底层的默认实现。


SpringAMQP案例------模拟HelloWorld消息模型

模拟消息发送:

步骤1. 在工程中引入AMQP依赖

java 复制代码
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

步骤2. 配置MQ地址:在publisher服务的application.yml中添加配置

java 复制代码
spring:
  rabbitmq:
    host: 192.168.237.128 # rabbitMQ的ip地址
    port: 5672 # 端口
    username: root
    password: 123456
    virtual-host: /

步骤3. 在publisher服务中设置测试类,利用convertAndSend方法实现信息发送

java 复制代码
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendMessage2SimpleQueue() {
        String queueName = "simple.queue";
        String message = "hello,spring amqp";
        rabbitTemplate.convertAndSend(queueName,message);
    }
}

运行之后可以看到消息队列中存在一条消息,消息内容为我们发送的内容


模拟消息接收:

步骤1. 在工程中引入AMQP依赖

java 复制代码
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

步骤2. 配置MQ地址:在consumer服务的application.yml中添加配置

java 复制代码
spring:
  rabbitmq:
    host: 192.168.237.128 # rabbitMQ的ip地址
    port: 5672 # 端口
    username: root
    password: 123456
    virtual-host: /

步骤2. 在consumer服务中新建一个类,编写消费逻辑

java 复制代码
@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg){
        System.out.println("消费者接收到simple.queue的消息:【" + msg + "】");
    }
}

运行之后,通过idea控制台可以看到已经成功接收到消息

**注意:**消息一旦消费就会从队列删除,RabbitMQ没有消息回溯功能。


SpringAMQP案例------模拟WorkQueue消息模型

根据WorkQueue消息模型,有一个发布者和两个消费者,因此我们模拟消费者发送大量消息的情况。

java 复制代码
/**
     * workQueue
     * 向队列中不停发送消息,模拟消息堆积。
     */
@Test
public void testWorkQueue() throws InterruptedException {
    // 队列名称
    String queueName = "simple.queue";
    // 消息
    String message = "hello, message_";
    for (int i = 0; i < 50; i++) {
        // 发送消息
        rabbitTemplate.convertAndSend(queueName, message + i);
        Thread.sleep(20);
    }
}

再设置两个消费者,通过设置sleep休眠,明显区分两个消费者的性能。

java 复制代码
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue1(String msg) throws InterruptedException {
    System.out.println("消费者1接收到的消息:【" + msg + "】" + LocalTime.now());
    Thread.sleep(20);
}

@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue2(String msg) throws InterruptedException {
    System.err.println("消费者2接收到的消息:【" + msg + "】" + LocalTime.now());
    Thread.sleep(200);
}

先运行发布者,发布50条消息,再运行消费者,控制台信息如下:

通过观察控制台信息,我们可以发现,消费者1只接收单数的消息,而消费者2只接收双数的消息。当消费者1很快完成了自己的25条消息,消费者2却在缓慢的处理自己的25条消息。

这是由WorkQueue消息模型中的消息预取机制所导致的。在该机制下,消费者无论当前是否能够处理消息,都会提前从队列中取出消息。因此,所有的消息会被平均分配给两个消费者。结果是,无论哪个消费者的接收速度较快,整体的消息接收速度都取决于接收速度较慢的消费者。

为了解决这个问题,我们可以通过设置消费者每次获取消息的数量来进行限制。只允许每个消费者一次获取一条消息,并且要求在当前消息处理完毕之后才能获取下一条消息。通过这种方式,我们可以实现能者多劳的效果。

经过设置后,我们再次运行程序,可以发现结果不再像之前一样,消费者1能够根据自己的性能接收更多消息。

​​​​​​​


SpringAMQP案例------模拟发布订阅消息模型

发布订阅消息模型介绍

可以看到,在订阅模型中,多了一个exchange角色,而且过程略有变化:

Publisher:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)

  • Exchange:交换机,图中的X。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有以下3种类型:

    • Fanout:广播,将消息交给所有绑定到交换机的队列

    • Direct:定向,把消息交给符合指定routing key 的队列

    • Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

  • Consumer:消费者,与以前一样,订阅队列,没有变化

  • Queue:消息队列也与以前一样,接收消息、缓存消息。


案例------FanoutExchange

步骤1. 在consumer中创建一个类,用于声明队列和交换机,并将队列与交换机进行绑定

java 复制代码
@Configuration
public class FanoutConfig {
    // 声明交换机 itcast.fanout
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange(("itcast.fanout"));
    }

    // 声明fanout.queue1
    @Bean
    public Queue fanoutQueue1() {
        return new Queue("fanout.queue1");
    }

    // 绑定队列1到交换机
    @Bean
    public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
        return BindingBuilder
                .bind(fanoutQueue1)
                .to(fanoutExchange);
    }

    // 声明fanout.queue2
    @Bean
    public Queue fanoutQueue2() {
        return new Queue("fanout.queue2");
    }

    // 绑定队列2到交换机
    @Bean
    public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
        return BindingBuilder
                .bind(fanoutQueue2)
                .to(fanoutExchange);
    }

}

重新运行后,在RabbitMQ管理平台中可以看到已经生成该交换机,并且与两个队列绑定。

步骤2:在consumer服务的SpringRabbitListener中添加两个方法,作为消费者,对两个队列进行监听

java 复制代码
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg){
    System.out.println("消费者接收到fanout.queue1的消息:【" + msg + "】");
}

@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg){
    System.out.println("消费者接收到fanout.queue2的消息:【" + msg + "】");
}

步骤3. 在publisher服务的SpringAmqpTest类中添加测试方法,将消息发送到交换机

java 复制代码
@Test
public void testSendFanoutExchange() {
    //交换机名称
    String exchangeName = "itcast.fanout";
    //消息
    String message = "hello, every one!";
    //发送消息
    rabbitTemplate.convertAndSend(exchangeName,"", message);
}

运行该测试方法,可以在控制台中看到消费者成功接收到消息。

总结

交换机的作用是什么?

  1. 接收publisher发送的消息

  2. 将消息按照规则路由到与之绑定的队列

  3. 不能缓存消息,路由失败,消息丢失

  4. FanoutExchange的会将消息路由到每个绑定的队列

绑定的队列声明队列、交换机、绑定关系的Bean是什么?

Queue、FanoutExchange、Binding


案例------DirectExchange

在Direct模型下:

  1. 队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由key)

  2. 消息的发送方在向 Exchange发送消息时,也必须指定消息的 RoutingKey

  3. Exchange不再把消息交给每一个绑定的队列,而是根据消息的 outing key 进行判断,只有队列的Routingkey 与消息的 Routing key 完全一致,才会接收到消息

基于@Bean的方式声明队列和交换机比较麻烦,Spring还提供了基于注解方式来声明。

步骤1. 在consumer的SpringRabbitListener中添加两个消费者,同时基于注解来声明队列和交换机

java 复制代码
    @RabbitListener( bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1"),
            exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
            key = {"red", "blue"}
    ))
    public void listenDirectQueue1(String msg) {
        System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");
    }

    @RabbitListener( bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2"),
            exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
            key = {"red", "yellow"}
    ))
    public void listenDirectQueue2(String msg) {
        System.err.println("消费者接收到direct.queue2的消息:【" + msg + "】");
    }

步骤2. 在publisher服务的SpringAmqpTest类中添加测试方法

java 复制代码
@Test
public void testSendDirectExchange() {
    //交换机名称
    String exchangeName = "itcast.direct";
    //消息
    String message = "hello, blue!";
    //发送消息
    rabbitTemplate.convertAndSend(exchangeName,"yellow", message);
}

当key为red时,控制台输出如下:

当key为blue时,控制台输出如下:

​​​​​​​

当key为yellow时,控制台输出如下:

总结

描述下Direct交换机与Fanout交换机的差异?

  1. Fanout交换机将消息路由给每一个与之绑定的队列

  2. Direct交换机根据RoutingKey判断路由给哪个队列

  3. 如果多个队列具有相同的RoutingKey,则与Fanout功能类似

基于@RabbitListener注解声明队列和交换机有哪些常见注解?

@Queue、@Exchange


案例------TopicExchange

步骤1. 在consumer的SpringRabbitListener中添加两个消费者,同时基于注解来声明队列和交换机

java 复制代码
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "topic.queue1"),
        exchange = @Exchange(name = "itcast.topic" ,type = ExchangeTypes.TOPIC),
        key = "china.#"
))
public void listenTopicQueue1(String msg) {
    System.out.println("消费者接收到Topic.queue1的消息:【" + msg + "】");
}

@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "topic.queue2"),
        exchange = @Exchange(name = "itcast.topic" ,type = ExchangeTypes.TOPIC),
        key = "#.news"
))
public void listenTopicQueue2(String msg) {
    System.err.println("消费者接收到Topic.queue2的消息:【" + msg + "】");
}

步骤2. 在publisher服务的SpringAmqpTest类中添加测试方法

javascript 复制代码
@Test
public void testSendTopicExchange() {
    //交换机名称
    String exchangeName = "itcast.topic";
    //消息
    String message = "世界新闻";
    //发送消息
    rabbitTemplate.convertAndSend(exchangeName,"world.news", message);
}

当key为china.news时,控制台输出如下:

当key为china.weather时,控制台输出如下:

当key为workd.news时,控制台输出如下:


消息转换器

Spring会把发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。只不过,默认情况下Spring采用的序列化方式是JDK序列化,而JDK序列化存在一些问题,如数据体积过大、有安全漏洞、可读性差等。

我们希望消息体的体积更小、可读性更高,因此可以使用JSON方式来做序列化和反序列化。

步骤1. 在publisher和consumer两个服务中都引入依赖:

XML 复制代码
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.9.10</version>
</dependency>

步骤2. 配置消息转换器,在启动类中添加一个Bean即可:

java 复制代码
@Bean
public MessageConverter jsonMessageConverter(){
    return new Jackson2JsonMessageConverter();
}
相关推荐
这周也會开心1 小时前
RabbitMQ知识点
分布式·rabbitmq
KYGALYX9 小时前
服务异步通信
开发语言·后端·微服务·ruby
yunteng52111 小时前
通用架构(同城双活)(单点接入)
架构·同城双活·单点接入
麦聪聊数据11 小时前
Web 原生架构如何重塑企业级数据库协作流?
数据库·sql·低代码·架构
程序员侠客行12 小时前
Mybatis连接池实现及池化模式
java·后端·架构·mybatis
bobuddy13 小时前
射频收发机架构简介
架构·射频工程
桌面运维家14 小时前
vDisk考试环境IO性能怎么优化?VOI架构实战指南
架构
洛豳枭薰15 小时前
消息队列关键问题描述
kafka·rabbitmq·rocketmq
一个骇客15 小时前
让你的数据成为“操作日志”和“模型饲料”:事件溯源、CQRS与DataFrame漫谈
架构