【SpringCloud】7.SpringAMQP消息队列

专栏目录
0.docker快速入门
1.初识微服务
2.Gateway网关路由
3.Nacos配置管理
4.Sentinel微服务保护
5.Seata分布式事务管理
6.RabbitMQ消息队列
7.SpringAMQP消息队列

SpringAMQP消息队列

学习了RabbitMQ,对于消息队列的整体工作流程已经有了一定的了解,但是在实际开发的时候,肯定不会在控制台操作后再二次搬运到程序上,而是应该基于编程的方式,直接在程序运行时就进行,降低出错的概率。

虽然RabbitMQ提供了各种不同语言的客户端,但提供的Java客户端编码相对复杂。

而Spring的官方刚好基于RabbitMQ提供了这样一套消息收发的模板工具:SpringAMQP。并且还基于SpringBoot对其实现了自动装配,使用起来非常方便。

SpringAMQP提供了三个功能:

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

1. 相关地址

官方网站

2. 快速使用

2.1 导入依赖

在父工程中导入即可

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

②在Rabbit控制台中创建新队列,命名为demo.queue

在这里插入图片描述

2.2 消息发送

①在发送者的application.yaml中配置信息

spring:
  rabbitmq:
    host: # 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: /maxbao # 虚拟主机
    username: jiabao # 用户名
    password: yu123123 # 密码

②编写测试类,实现消息发送:采用消息生产者->队列->消息消费者,这种简易测试


@SpringBootTest
public class SpringAmqpTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

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

③运行后就可以在控制台看到发送到队列的消息

2.3 消息接收

①在消费者的application.yaml中配置信息

spring:
  rabbitmq:
    host: # 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: /maxbao # 虚拟主机
    username: jiabao # 用户名
    password: yu123123 # 密码

②创建消息接收类

@Component
public class SpringRabbitListener {
    // 利用RabbitListener来声明要监听的队列信息
    // 将来一旦监听的队列中有了消息,就会推送给当前服务,调用当前方法,处理消息。
    // 可以看到方法体中接收的就是消息体的内容
    @RabbitListener(queues = "demo.queue")
    public void listenSimpleQueueMessage(String msg) throws InterruptedException {
        System.out.println("spring 消费者接收到消息:【" + msg + "】");
    }
}

3. WorkQueues模型

该模型实现多个消费者共享同一个队列中的信息,共同进行消息处理,大大提高消息处理的速度

3.1 模拟大量消息发送

参考3.2.2的消息发送设置


@Test
public void testWorkQueue() throws InterruptedException {
    // 队列名称
    String queueName = "demo.queue";
    // 消息
    String message = "hello, message_";
    for (int i = 0; i < 50; i++) {
        // 发送消息,每20毫秒发送一次,相当于每秒发送50条消息
        rabbitTemplate.convertAndSend(queueName, message + i);
        Thread.sleep(20);
    }
}

3.2 消息接收

模拟多个消费者绑定同一个队列,我们在消费者服务的SpringRabbitListener中添加2个新的方法:

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

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

该实现方式是将消息平均分配给每个消费者,并没有考虑到消费者的处理能力。

会导致出现部分消费者空闲,部分消费者忙的情况。没有充分利用每一个消费者的能力。

3.3 优化改进

解决上述分配不合理问题,在消费者的application.yaml中配置信息即可

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

3.4 总结

Work模型的使用:

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

4. 交换机类型

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

交换机的类型有四种:

  • Fanout:广播,将消息交给所有绑定到交换机的队列。我们最早在控制台使用的正是Fanout交换机
  • Direct:订阅,基于RoutingKey(路由key)发送给订阅了消息的队列
  • Topic:通配符订阅,与Direct类似,只不过RoutingKey可以使用通配符
  • Headers:头匹配,基于MQ的消息头匹配,用的较少

4.1 Fanout交换机

广播式交换机,允许绑定多个队列,但生产者只能把消息发送到交换机上,交换机把消息发送给绑定过的所有队列,订阅队列的消费者都能拿到消息。

①创建交换机"exchange.fanout"

在这里插入图片描述

②创建队列“fanout.queue1”和“fanout.queue2”,并绑定到交换机上

在这里插入图片描述
在这里插入图片描述

③消息发送

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

④消息接收

@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 + "】");
}

4.2 Direct交换机

在Fanout模式中,一条消息,会被所有订阅的队列都消费。如果我们希望不同的消息被不同的队列消费,就要用到Direct类型的Exchange。

Direct类型的Exchange在进行队列绑定时需要指定一个 RoutingKey;消息的发送方在向 Exchange发送消息时,也必须指定消息的 RoutingKey;只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息

①创建direct类型exchange

在这里插入图片描述

②创建并绑定队列,且指定routingKey

在这里插入图片描述
在这里插入图片描述

③消息发送

@Test
public void testSendDirectExchange() {
    // 交换机名称
    String exchangeName = "exchange.direct";
    // 消息
    String message = "新年快乐!";
    // 发送消息
    rabbitTemplate.convertAndSend(exchangeName, "red", message);
}

④消息接收

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

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

4.3 Topic交换机

Topic类型的ExchangeDirect相比,都是可以根据RoutingKey把消息路由到不同的队列。

只不过Topic类型Exchange可以让队列在绑定BindingKey 的时候使用通配符!

BindingKey:由一个或多个单词组成,之间用.隔开

  • #:匹配一个或多个词语
  • *:匹配一个词

示例
假如此时发送的消息使用的RoutingKey共有四种:

  • china.news 代表有中国的新闻消息;
  • china.weather 代表中国的天气消息;
  • japan.news 则代表日本新闻
  • japan.weather 代表日本的天气消息;

topic.queue1:绑定的是china.# ,凡是以 china.开头的routing key 都会被匹配到,包括:

  • china.news
  • china.weather

topic.queue2:绑定的是#.news ,凡是以 .news结尾的 routing key 都会被匹配。包括:

  • china.news
  • japan.news

①创建Topic类型的Exchange

在这里插入图片描述

②绑定队列并设置BindingKey

在这里插入图片描述
在这里插入图片描述

③消息发送

/**
 * topicExchange
 */
@Test
public void testSendTopicExchange() {
    // 交换机名称
    String exchangeName = "exchange.topic";
    // 消息
    String message = "新年快乐!";
    // 发送消息
    rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}

④消息接收

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

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

5. 声明式绑定队列和交换机

在实际开发时,队列和交换机是程序员定义的。那么程序员就需要把程序中运行的所有队列和交换机都写下来,在这个过程中是很容易出现错误的。

因此推荐的做法是由程序启动时检查队列和交换机是否存在,如果不存在自动创建。

我们可以使用SpringAMQP提供的Queue接口来创建

5.1 实现Fanout交换机

import ...
    
@Configuration
public class FanoutConfig {
    /**
     * 声明交换机
     * @return Fanout类型交换机
     */
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("exchange.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);
    }
}

5.2 实现Direct交换机

实现的时候,每个Key都需要编写一个binding

import ...

@Configuration
public class DirectConfig {

    /**
     * 声明交换机
     * @return Direct类型交换机
     */
    @Bean
    public DirectExchange directExchange(){
        return ExchangeBuilder.directExchange("exchange.direct").build();
    }

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

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue1WithRed(Queue directQueue1, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue1).to(directExchange).with("red");
    }
    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue1WithBlue(Queue directQueue1, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue1).to(directExchange).with("blue");
    }

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

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue2WithRed(Queue directQueue2, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue2).to(directExchange).with("red");
    }
    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue2WithYellow(Queue directQueue2, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow");
    }
}

5.3 注解式绑定

通过注解的方式来创建队列并绑定交换机

5.3.1 注解式Direct
@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "direct.queue1"),
    exchange = @Exchange(name = "exchange.direct", type = ExchangeTypes.DIRECT),
    key = {"red", "blue"}
))
public void listenDirectQueue1(String msg){
    System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
}

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "direct.queue2"),
    exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT),
    key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg){
    System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】");
}
5.3.2 注解式Topic
@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "topic.queue1"),
    exchange = @Exchange(name = "exchange.topic", type = ExchangeTypes.TOPIC),
    key = "china.#"
))
public void listenTopicQueue1(String msg){
    System.out.println("消费者1接收到topic.queue1的消息:【" + msg + "】");
}

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

5.4 消息格式转换

Spring的消息发送代码接收的消息体是一个Object,在数据传输时,它会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。

但是默认情况下Spring采用的序列化方式是JDK序列化,会存在数据体积过大、有安全漏洞、可读性差的问题

我们可以使用JSON格式来解决这个问题

5.4.1 配置JSON转换器

①生产者和消费者都需要导入依赖

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

若已经引入spring-boot-starter-web依赖,则无需再次引入Jackson依赖

②配置消息转换器,在生产者和消费者服务的启动类中都需要添加一个Bean:

@Bean
public MessageConverter messageConverter(){
    // 1.定义消息转换器
    Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
    // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
    jackson2JsonMessageConverter.setCreateMessageIds(true);
    return jackson2JsonMessageConverter;
}
5.4.2 消费者接收消息

注意传参是Map<String,Object>

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

6. 总结

SpringAMQP可以基于SpringBoot对其实现了自动装配

使用时:

  • 引入依赖
  • 配置application
  • 注解式编写消息监听类
  • 编写消息发送逻辑,使用RabbitTemplate提供的接口
  • 注意灵活使用交换机的不同种类型,以及bindingKey
  • 37
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值