【DAY 04】Spring Cloud学习日记 RabbitMQ入门

1. 消息队列(MQ)简介

1.1 同步和异步通讯

微服务之间的通讯有两种方式

  • 同步通信:就像打电话,需要实时的响应。程序调用另一个微服务,在另一个微服务响应过程中等待。
  • 异步通信:就像发微信,发过去就结束了,然后继续干别的事情。
    在这里插入图片描述
    两种方式各有优劣,打电话可以立即得到响应,但是你却不能跟多个人同时通话。发送邮件可以同时与多个人收发邮件,但是往往响应会有延迟。

1.2 同步通信

之前学习的Feign就属于同步通讯的方式,它可以实时的得到结果,但是存在以下的问题

  • 耦合度高,每次添加新的需求,都需要修改代码。比如之前下单业务只需要保存订单信息,现在要添加一个发送短信提醒的功能,那么就不得不修改代码。
  • 性能下降:当调用者等待服务者的响应的时候,只能等待,且如果调用者需要调用多个服务,那么整个的耗时就是多个服务响应时间之和。
  • 资源浪费,在等待过程中不能释放其他的资源,导致资源浪费
  • 级联失败:如果其中一个服务出现问题,那么所有的服务都会失败。
    在这里插入图片描述
    在这里插入图片描述

同步调用的优点:

  • 时效性较强,可以立即得到结果

同步调用的问题:

  • 耦合度高
  • 性能和吞吐能力下降
  • 有额外的资源消耗
  • 有级联失败问题

1.3 异步通讯

采用异步通讯可以避免上述的问题。
比如购买商品,用户完成支付后,需要调用订单服务完成订单状态的修改,调用物流服务准备发货,减少库存等。

在事件模式中,支付服务是事件发布者(publisher),在支付完成后只需要发布一个支付成功的事件(event),事件中带上订单id。然后支付服务就执行完毕了。

订单服务和物流服务是事件订阅者(Consumer),订阅支付成功的事件,监听到事件后完成自己业务即可。
在这里插入图片描述

此外,为了降低发送者和消费者之间的耦合,两者不是直接通讯的,需要有一个中间人,也就是消息队列。发送者将消息发送到队列,该队列的订阅者从队列中取出消息。
在这里插入图片描述
采用队列方式的好处:

服务解耦:在引入消息队列以后,这时候如果要添加新的功能,只需要让这个新功能的服务订阅这个消息队列即可,不需要修改支付服务的代码。
在这里插入图片描述
提高性能:不同的任务可以同时执行,支付服务的耗时不再是所有的服务耗时之和
在这里插入图片描述
解决了级联失败:不同的服务之间没有依赖关系,即使某一个服务挂掉了,其他的服务依然可以运行
在这里插入图片描述
流量削峰:当大量并发时,消息队列可以起到缓冲区的作用。
在这里插入图片描述

缺点:

  • 架构复杂了,业务没有明显的流程线,不好管理
  • 需要依赖于Broker的可靠、安全、性能

1.4 常见的消息队列

MQ,中文是消息队列(MessageQueue),字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。

比较常见的MQ实现:

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

2. RabbitMQ入门

2.1 RabbitMQ中的角色和消息模型

RabbitMQ中的一些角色:

  • publisher:生产者
  • consumer:消费者
  • exchange个:交换机,负责消息路由
  • queue:队列,存储消息
  • virtualHost:虚拟主机,隔离不同租户的exchange、queue、消息的隔离
    在这里插入图片描述
    RabbitMQ官方提供了5个不同的Demo示例,对应了不同的消息模型:
    在这里插入图片描述

2.2 基本消息队列

基本消息队列只包含三个角色:发送者,消费者和队列
在这里插入图片描述
向队列中发送一条消息
我们在发送者的代码中实现发送消息到到名为“simple.queue”队列
思路:

  • 建立连接
  • 创建Channel
  • 声明队列
  • 发送消息
  • 关闭连接和channel
public class PublisherTest {
    @Test
    public void testSendMessage() throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("95.179.138.182");
        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();

    }
}

从队列中读取一条消息
思路:

  • 建立连接
  • 创建Channel
  • 声明队列
  • 订阅消息

在消费者的代码中添加一下代码

public class ConsumerTest {

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("95.179.138.182");
        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.订阅消息
        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("等待接收消息。。。。");
    }
}

注意:这里为什么在消费者的代码里面也有创建队列的代码呢?

原因是发布者和消费者的执行顺序是随机的,因此不一定谁先执行,因此都要创建队列。创建重复名字的队列不会覆盖原有队列的消息

2.2.3 小结

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

  1. 建立connection

  2. 创建channel

  3. 利用channel声明队列

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

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

  1. 建立connection

  2. 创建channel

  3. 利用channel声明队列

  4. 定义consumer的消费行为handleDelivery()

  5. 利用channel将消费者与队列绑定

3. Spring AMQP

上一章我们演示了如何使用Java代码到队列中发送和读取消息,代码非常的复杂不够优雅,Spring为我们提供了一个框架封装了对RabbitMQ的一系列操作。
在这里插入图片描述
在这里插入图片描述
SpringAMQP提供了三个功能:

  • 自动声明队列、交换机及其绑定关系
  • 基于注解的监听器模式,异步接收消息
  • 封装了RabbitTemplate工具,用于发送消息

3.1 简单队列模型

3.1.1 消息发送

在父工程,引入Spring AMQP先依赖,这样发布者和消费者子工程就不需要再次引入

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

在publisher服务的application.yml中添加配置:

spring:
  rabbitmq:
    host: 95.179.138.182 # 主机名
    port: 5672 # 端口
    virtual-host: / # 虚拟主机
    username: itcast # 用户名
    password: 123321 # 密码

在publisher服务中编写测试类SpringAmqpTest,并利用RabbitTemplate实现消息发送:

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSimpleQueue() {
        // 队列名称
        String queueName = "simple.queue";
        // 消息
        String message = "hello, spring amqp!";
        // 发送消息
        rabbitTemplate.convertAndSend(queueName, message);
    }
}

对比之前发送消息的代码,可以看出使用Spring AMQP以后优雅了很多

3.1.2 消息接收

在consumer服务的application.yml中添加配置:

spring:
  rabbitmq:
    host: 95.179.138.182 # 主机名
    port: 5672 # 端口
    virtual-host: / # 虚拟主机
    username: itcast # 用户名
    password: 123321 # 密码

然后在消费者的cn.itcast.mq.listener包中新建一个类SpringRabbitListener,代码如下:

package cn.itcast.mq.listener;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class SpringRabbitListener {

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

3.2 工作队列模型

和简单队列模型不同,工作队列模型可以有多个消费者。
在这里插入图片描述
当消息比较多的时候,发送消息的速度可能远远大于一个消费者处理的速度,这个时候可以考虑使用这个模型,让多个消费者同时消费一个队列里面的消息,来加速消息处理的速度

3.2.1 消息发送

我们在发送者的代码中设置在1秒之内发送50条消息

@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);
    }
}
3.2.2 消息接收

在消费者中添加两个消费者同时消费。其中消费者1的处理速度是消费者2处理速度的10倍。

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

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

经过测试发现,消费者1很快处理完了25条消息,消费者2缓慢处理25条消息。也就是说消息是平均分配给每个消费者,并没有考虑到消费者的处理能力。这样显然是有问题的。

3.2.4 能者多劳

在spring中有一个简单的配置,可以解决这个问题。我们修改consumer服务的application.yml文件,添加配置:

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
3.2.5 小结

工作队列的使用

  • 多个消费者绑定到一个队列,同一条消息只会被一个消费者处理
  • 通过设置prefetch来控制消费者预取的消息数量

3.3 发布/订阅

在发布/订阅模型中,角色发生了变化,多了一个exchange角色。消息发布和接收的过程如下

  • 发布者将消息发送给exchange
  • exchange接收发布者发送的消息,然后根据exchange的类型,选择如何处理该消息
    • Fanout:广播模式,exhange将消息发送到所有和其绑定的队列中
    • Direct:exchange将消息交给符合指定的 routing key 的队列
    • Topic:exchange把消息交给符合routing pattern(路由模式) 的队列
  • 消费者依然是从队列中读取数据
  • 队列也和之前一样
    在这里插入图片描述
    Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

3.4 Fanout模型

在这里插入图片描述
在Fanout模式下,消息发送流程是这样的:

  • 可以存在多个队列,每一个队列都需要绑定Exchange
  • 发布者发布的消息交给exchange,由exhange转发到所有和其绑定的队列中
  • 消费者从队列中消费消息
3.4.1 案例
  • 创建一个交换机 itcast.fanout,类型是Fanout
  • 创建两个队列fanout.queue1和fanout.queue2,绑定到交换机itcast.fanout
    在这里插入图片描述
    创建exchange和队列并将其绑定在一起
@Configuration
public class FanoutConfig {
    /**
     * 声明交换机
     * @return Fanout类型交换机
     */
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("itcast.fanout");
    }

    /**
     * 第1个队列
     */
    @Bean
    public Queue fanoutQueue1(){
        return new Queue("fanout.queue1");
    }

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

    /**
     * 第2个队列
     */
    @Bean
    public Queue fanoutQueue2(){
        return new Queue("fanout.queue2");
    }

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

消息发送
在publisher服务的SpringAmqpTest类中添加测试方法:

@Test
public void testFanoutExchange() {
    // 队列名称
    String exchangeName = "itcast.fanout";
    // 消息
    String message = "hello, everyone!";
    rabbitTemplate.convertAndSend(exchangeName, "", message);
}

消息接收

在consumer服务的SpringRabbitListener中添加两个方法,作为消费者:

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

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

运行代码可以发现,两个消费者都收到了消息
在这里插入图片描述

3.5 Direct模型

在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
在这里插入图片描述
在Direct模型下:

  • 消费者和队列绑定时需要为自己设定一个RoutingKey,标明队列只接受符合RoutingKey的消息。
  • 消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey
  • Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息
3.5.1 案例
  1. 利用@RabbitListener声明Exchange、Queue、RoutingKey

  2. 在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2

  3. 在publisher中编写测试方法,向itcast. direct发送消息
    在这里插入图片描述

基于@Bean的方式声明队列和交换机比较麻烦,Spring还提供了基于注解方式来声明。
在consumer的SpringRabbitListener中添加两个消费者,同时基于注解来声明队列和交换机:

采用这种注解方式可以直接写队列名称,如果队列或exchange不存在则自动创建

消费者代码

@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.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
}

消息发送代码

@Test
public void testSendDirectExchange() {
    // 交换机名称
    String exchangeName = "itcast.direct";
    // 消息
    String message = "红色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!";
    // 发送消息
    rabbitTemplate.convertAndSend(exchangeName, "blue", message);
}

我们在代码中的Rounting Key是blue,因此只有消费者1可以收到我们发送的消息

3.6 Topic模型

和Direct模型相比,Topic模型允许绑定Routing key 的时候使用通配符!
通配符规则:

#:匹配一个或多个词

*:匹配不多不少恰好1个词

举例:

China.#:能够匹配China.News 或者 China.weather

#.weather:只能匹配Japan.weather China.weather

在这里插入图片描述

  • Queue1:绑定的是china.# ,因此凡是以 china.开头的routing key 都会被匹配到。包括china.news和china.weather
  • Queue2:绑定的是#.news ,因此凡是以 .news结尾的 routing key 都会被匹配。包括china.news和japan.news
3.6.1 案例
  1. 并利用@RabbitListener声明Exchange、Queue、RoutingKey

  2. 在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2

  3. 在publisher中编写测试方法,向itcast. topic发送消息
    在这里插入图片描述
    消息发送

@Test
public void testSendTopicExchange() {
    // 交换机名称
    String exchangeName = "itcast.topic";
    // 消息
    String message = "喜报!孙悟空大战哥斯拉,胜!";
    // 发送消息
    rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}

消息接收
在consumer服务的SpringRabbitListener中添加方法:

@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.out.println("消费者接收到topic.queue2的消息:【" + msg + "】");
}

我们发送的Rounting Keychina.news,因此消费者1和消费者2都能收到我们发送的消息

3.7 消息转换器

之前说过,Spring会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。
我们修改消息发送的代码,发送一个Map对象:

@Test
public void testSendMap() throws InterruptedException {
    // 准备消息
    Map<String,Object> msg = new HashMap<>();
    msg.put("name", "Jack");
    msg.put("age", 21);
    // 发送消息
    rabbitTemplate.convertAndSend("simple.queue","", msg);
}

到RabbitMQ的UI界面中查看消息内容
在这里插入图片描述
发现RabbitMQ使用是JDK的序列化方式,这种方式由一下缺点:

  • 数据体积过大
  • 有安全漏洞
  • 可读性差

如何更换序列化器成JSON的呢?

引入依赖
在publisher和consumer两个服务中都引入依赖:

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.9.10</version>
</dependency>

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

@Bean
public MessageConverter jsonMessageConverter(){
    return new Jackson2JsonMessageConverter();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Cloud项目中,我们可以通过使用Spring Cloud Stream模块来集成RabbitMQSpring Cloud Stream提供了一种简单的方式来构建消息驱动的应用程序。通过引入相关依赖,如spring-cloud-stream和spring-cloud-starter-stream-rabbit,我们可以轻松地实现与RabbitMQ的集成。在配置文件中,我们需要指定RabbitMQ的连接信息,如地址、端口、用户名和密码。同时,我们还需要定义消息的目的地和消费者组。通过使用SubscribableChannel和MessageChannel,我们可以分别实现消费者和生产者的功能。具体的代码实现可以参考相关文档和示例代码。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* [SpringCloud项目整合RabbitMQ](https://blog.csdn.net/weixin_43028393/article/details/129818186)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [SpringCloud Stream 整合RabbitMQ](https://blog.csdn.net/m0_67393827/article/details/124442134)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值