SpringBoot整合RabbitMQ:Direct类型交换机

前言:主要介绍SpringBoot中如何根据直连型交换机去使用RabbitMQTemplate,Direct模式的Exchange根据消息携带的路由值将消息投递给对应队列。


一、RabbitMQ通信模型和Exchange类型

1.1、RabbitMQ通信模型

在代码中使用MQ发送消息的过程是异步执行的,消息到达RabbitMQ后,会在通信模型中找到适合的队列进行入队。

RabbitMQ通信模型

下面来看看消息到达RabbitMQ会发生什么,Exchange会将消息通过RoutingKey将消息路由到相应的队列,每当有消息进入到队列中时,消费端就会监听到该消息进行消费。

1.2、Exchange类型

RabbitMQ中对于Exchange有以下几种类型:

1.3、Direct直连型交换机

这里着重说下Direct直连型交换机,根据消息携带的路由值将消息投递给对应绑定路由值的队列。

Direct交换机大致流程,有一个队列绑定到一个直连交换机上,同时赋予一个路由键routing key。然后当一个消息携带着路由值为X,这个消息通过生产者发送给交换机时,交换机就会根据这个路由值X去寻找绑定值也是X的队列。


二、SpringBoot的RabbitMQTemplate实战

比如说我们想实现个多渠道消息通知服务系统,用来发送邮件、短信和微信通知,使用RabbitMQ做异步解耦和流量削峰。那么就可以使用Direct交换机把三种不同类型的通知消息放在对应路由值的三个队列EmailQueue、SmsQueue、WeChatQueue上。

2.1、引入Maven依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    <version>2.3.7.RELEASE</version>
</dependency>

2.2、修改yml配置文件

SpringBoot的RabbitMQ具有丰富的配置,比如消费失败重试、消息确认模式和超时等,这里可以不做复杂的配置,用于简单的应用。

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: root
    password: root

     #虚拟host 可以不设置,使用默认host为/
    virtualHost: /
    listener:
      simple:
        concurrency: 1        # Minimum number of consumers.
        max-concurrency: 20   # Maximum number of consumers.
#       acknowledge-mode: manual  # 手动确定(默认自动确认)
        prefetch: 50   #从spring-amqp 2.0版开始,默认的prefetch值是250
        default-requeue-rejected: true  #消息被拒后(即未消费),重新(true)放入队列
        retry:
          enabled: true     #是否开启消费者重试(为false时关闭消费者重试,这时消费端代码异常会一直重复收到消息)
          max-attempts: 3   #消费端发现了异常,尝试了规定次数后,这条“问题消息”就会被解决(如果设置了死信队列,就被送到了死信队列;否则直接扔掉)。如果不开启消费者重试尝试模式,那么会无限的循环下去控制台一直报错
          initial-interval: 5000ms

2.3、编写RabbitConfig配置类

采用Java Configuration的方式配置支持消息体为对象的RabbitTemplate、Exchange和Queue等信息

package com.cernet.notice.rabbitmq;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 声明队列和交换机,并设置队列和交换机绑定的路由键
 *
 * 这里声明了Direct模式的Exchange,声明一个Queue,并通过routing-key为EmailDirectRouting
 * 将EmailDirectQueue绑定到EmailDirectExchange,
 * 这样EmailDirectQueue就可以接收到EmailDirectExchange发送的消息了。
 */

@Configuration
public class DirectRabbitConfig
{
    /**
     *  配置RabbitMQ的消息体为对象。配置MessageConverter为Jackson2JsonMessageConverter即可
     */
    @Bean
    public RabbitTemplate rabbitTemplate(CachingConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(jackson2JsonMessageConverter());
        return rabbitTemplate;
    }

    @Bean
    public MessageConverter jackson2JsonMessageConverter(){
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        return jackson2JsonMessageConverter;
    }

    //队列 起名:EmailDirectQueue
    @Bean
    public Queue EmailDirectQueue()
    {
        // 声明队列参数列表:new Queue("EmailDirectQueue",true,true,false);
        // name 队列名称
        // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
        // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
        // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。

        //一般设置一下队列的持久化就好,其余两个就是默认false
        return new Queue("EmailDirectQueue",true);
    }

    //Direct交换机 起名:EmailDirectExchange
    @Bean
    DirectExchange EmailDirectExchange() {
        //声明交换机参数列表
        //name:交换机名称。
        // durable:是否持久化,否则RabbitMQ服务端重启,队列就不再存在。
        // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
        return new DirectExchange("EmailDirectExchange",true,false);
    }

    //绑定  将队列和交换机绑定, 并设置用于路由匹配键:EmailDirectRouting
    @Bean
    Binding bindingDirectEmail() {
        return BindingBuilder.bind(EmailDirectQueue()).to(EmailDirectExchange()).with("EmailDirectRouting");
    }

    //队列 起名:WeChatDirectQueue
    @Bean
    public Queue WeChatDirectQueue()
    {
        return new Queue("WeChatDirectQueue",true);
    }

    //Direct交换机 起名:WeChatDirectExchange
    @Bean
    DirectExchange WeChatDirectExchange() {
        return new DirectExchange("WeChatDirectExchange",true,false);
    }

    //绑定  将队列和交换机绑定, 并设置用于路由匹配键:EmailDirectRouting
    @Bean
    Binding bindingDirectWeChat() {
        return BindingBuilder.bind(WeChatDirectQueue()).to(WeChatDirectExchange()).with("WeChatDirectRouting");
    }

    //队列 起名:SmsDirectQueue
    @Bean
    public Queue SmsDirectQueue()
    {
        return new Queue("SmsDirectQueue",true);
    }

    //Direct交换机 起名:SmsDirectExchange
    @Bean
    DirectExchange SmsDirectExchange() {
        return new DirectExchange("SmsDirectExchange",true,false);
    }

    //绑定  将队列和交换机绑定, 并设置用于路由匹配键:EmailDirectRouting
    @Bean
    Binding bindingDirectSms() {
        return BindingBuilder.bind(SmsDirectQueue()).to(SmsDirectExchange()).with("SmsDirectRouting");
    }
}
  • 声明队列参数列表:

    • name:队列名称。

    • durable:是否持久化,否则RabbitMQ服务端重启,队列就不再存在。

    • exclusive:是否排他,即该队列是否只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参数优先级高于durable

    • autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。

  • 声明交换机参数列表:

    • name:交换机名称。

    • durable:是否持久化,否则RabbitMQ服务端重启,队列就不再存在。

    • autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。

2.4、生产者RabbitMQProducer

生产者使用RabbitMQ的直连型交换机的形式,根据消息携带的路由值将消息投递给对应队列

RabbitTemplate是Spring AMQP和RabbitMQ整合的产物,是进行发送消息的关键类。使用RabbitTemplate,可以发送String、Map和对象类型的消息。

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * 大致流程,有一个队列绑定到一个直连交换机上,同时赋予一个路由键routing key。
 * 然后当一个消息携带着路由值为X,这个消息通过生产者发送给交换机时,交换机就会根据这个路由值X去寻找绑定值也是X的队列。
 */
@Component
@Slf4j
public class RabbitMQProducer {

    //使用RabbitTemplate,提供了接收/发送等等方法,可以发送String、Map和对象
    @Autowired
    RabbitTemplate rabbitTemplate;

    public boolean produceEmailMessage(String receiverEmail,String title,String content)
    {
//          Map<String,Object> map =new HashMap<>();
//          map.put("senderID",messageId);
//          map.put("receiverID",messageData);
//          map.put("tilte",createTime);
//          map.put("content",createTime);
//          rabbitTemplate.convertAndSend("directExchange", "directRouting", map);

            EmailNotice emailNotice = new EmailNotice();
            emailNotice.setReceiverEmail(receiverEmail);
            emailNotice.setTilte(title);
            emailNotice.setContent(content);

            try {
                //将消息携带绑定键值:EmailirectRouting 发送到交换机EmailDirectExchange:convertAndSend(交换机名",“路由键”,“消息内容”)
                rabbitTemplate.convertAndSend("EmailDirectExchange", "EmailDirectRouting", emailNotice);
            }
            catch (Exception e)
            {
                e.printStackTrace();
                log.error("邮件生产者发送异常:{}",e.getMessage());
                return false;
            }

            return true;
    }

    public boolean produceSmsMessage(String receiverPhone,String variableArray)
    {
            SmsNotice smsNotice = new SmsNotice();
            smsNotice.setReceiverPhone(receiverPhone);
            smsNotice.setVariableArray(variableArray);
            try
            {
                //将消息携带绑定键值:SmsDirectRouting 发送到交换机SmsDirectExchange:convertAndSend(交换机名",“路由键”,“消息内容”)
                rabbitTemplate.convertAndSend("SmsDirectExchange", "SmsDirectRouting", smsNotice);
            }
            catch (Exception e)
            {
                e.printStackTrace();
                log.error("短信生产者发送异常:{}",e.getMessage());
                return false;
            }
            return true;
    }

    public boolean produceWeChatMessage(String openid,String content)
    {
            WeChatNotice wechatNotice = new WeChatNotice();
            wechatNotice.setOpenId(openid);
            wechatNotice.setContent(content);

            try
            {
                //将消息携带绑定键值:WeChatDirectRouting 发送到交换机WeChatDirectExchange:convertAndSend(交换机名",“路由键”,“消息内容”)
                rabbitTemplate.convertAndSend("WeChatDirectExchange", "WeChatDirectRouting", wechatNotice);
            }
            catch (Exception e)
            {
                e.printStackTrace();
                log.error("短信生产者发送异常:{}",e.getMessage());
                return false;
            }
            return true;
    }
}

convertAndSend函数发送到指定的交换机上,同时指定路由键。RabbitMQ会根据路由键把消息推送至和该交换机绑定的队列中。

2.5、消费者RabbitMQConsumer 

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;

/**
 * 消费者根据路由键去监听相应队列,当队列中有消息时,取出消息进行相应消费
 */
@Slf4j
@Component
public class RabbitMQConsumer {

    //监听的队列名称 EmailDirectQueue
    @RabbitListener(queues = "EmailDirectQueue")
    @RabbitHandler
    public void process(EmailNotice emailNotice)
    {
        ObjectMapper objectMapper = new ObjectMapper();
        String message = null;
        try
        {
            message = objectMapper.writeValueAsString(emailNotice);
        }
        catch (JsonProcessingException e)
        {
            e.printStackTrace();
            log.error("Json解析异常,参数格式不正确!");
        }
        catch (Exception exception)
        {
            exception.printStackTrace();
        }

        log.info("Email消费者收到消息:{}",message);
    }

    //监听的队列名称 SmsDirectQueue
    @RabbitListener(queues = "SmsDirectQueue")
    @RabbitHandler
    public void process(SmsNotice smsNotice) throws JsonProcessingException
    {
        ObjectMapper objectMapper = new ObjectMapper();
        String message = null;

        try
        {
            message = objectMapper.writeValueAsString(smsNotice);
        }
        catch (JsonProcessingException e)
        {
            e.printStackTrace();
            log.error("Json解析异常,参数格式不正确!");
        }
        catch (Exception exception)
        {
            exception.printStackTrace();
        }

        log.info("Sms消费者收到消息:{}",message);
    }

    //监听的队列名称 WeChatDirectQueue
    @RabbitListener(queues = "WeChatDirectQueue")
    @RabbitHandler
    public void process(WeChatNotice weChatNotice) throws JsonProcessingException
    {
        ObjectMapper objectMapper = new ObjectMapper();
        String message = null;

        try
        {
            message = objectMapper.writeValueAsString(weChatNotice);
        }
        catch (JsonProcessingException e)
        {
            e.printStackTrace();
            log.error("Json解析异常,参数格式不正确!");
        }
        catch (Exception exception)
        {
            exception.printStackTrace();
        }

        log.info("WeChat消费者收到消息:{}",message);
    }

//    @RabbitHandler
//    public void process(Map testMessage)
//    {
//        log.info("DirectReceiver消费者收到消息:{}", JSONUtil.toJsonPrettyStr(testMessage));
//        JSONObject jsonObject = new JSONObject(testMessage);
//        String senderID = jsonObject.getStr("senderID");
//    }

}

通过注解@RabbitListener指定要消费的队列,当监听到队列 XXX 中有消息时则会进行接收并处理

2.6、通过RabbitMQ的后台管理界面,查看交换机和队列

可以看到交换机已经被创建出来了:

可以看到队列也已经被创建出来了:

可以看到和队列的绑定关系:


三、RabbitMQ配置参数解释

3.1、yaml版本配置

 rabbitmq:
    addresses: 127.0.0.1:6605,127.0.0.1:6606,127.0.0.1:6705 #指定client连接到的server的地址,多个以逗号分隔(优先取addresses,然后再取host)
#    port:
    ##集群配置 addresses之间用逗号隔开
    # addresses: ip:port,ip:port
    password: admin
    username: 123456
    virtual-host: / # 连接到rabbitMQ的vhost
    requested-heartbeat: #指定心跳超时,单位秒,0为不指定;默认60s
    publisher-confirms: #是否启用 发布确认
    publisher-reurns: # 是否启用发布返回
    connection-timeout: #连接超时,单位毫秒,0表示无穷大,不超时
    cache:
      channel.size: # 缓存中保持的channel数量
      channel.checkout-timeout: # 当缓存数量被设置时,从缓存中获取一个channel的超时时间,单位毫秒;如果为0,则总是创建一个新channel
      connection.size: # 缓存的连接数,只有是CONNECTION模式时生效
      connection.mode: # 连接工厂缓存模式:CHANNEL 和 CONNECTION
    listener:
      simple.auto-startup: # 是否启动时自动启动容器
      simple.acknowledge-mode: # 表示消息确认方式,其有三种配置方式,分别是none、manual和auto;默认auto
      simple.concurrency: # 最小的消费者数量
      simple.max-concurrency: # 最大的消费者数量
      simple.prefetch: # 指定一个请求能处理多少个消息,如果有事务的话,必须大于等于transaction数量.
      simple.transaction-size: # 指定一个事务处理的消息数量,最好是小于等于prefetch的数量.
      simple.default-requeue-rejected: # 决定被拒绝的消息是否重新入队;默认是true(与参数acknowledge-mode有关系)
      simple.idle-event-interval: # 多少长时间发布空闲容器时间,单位毫秒
      simple.retry.enabled: # 监听重试是否可用
      simple.retry.max-attempts: # 最大重试次数
      simple.retry.initial-interval: # 第一次和第二次尝试发布或传递消息之间的间隔
      simple.retry.multiplier: # 应用于上一重试间隔的乘数
      simple.retry.max-interval: # 最大重试时间间隔
      simple.retry.stateless: # 重试是有状态or无状态
    template:
      mandatory: # 启用强制信息;默认false
      receive-timeout: # receive() 操作的超时时间
      reply-timeout: # sendAndReceive() 操作的超时时间
      retry.enabled: # 发送重试是否可用
      retry.max-attempts: # 最大重试次数
      retry.initial-interval: # 第一次和第二次尝试发布或传递消息之间的间隔
      retry.multiplier: # 应用于上一重试间隔的乘数
      retry.max-interval: #最大重试时间间隔

3.2、properties版本配置

# base
spring.rabbitmq.host: 服务Host
spring.rabbitmq.port: 服务端口
spring.rabbitmq.username: 登陆用户名
spring.rabbitmq.password: 登陆密码
spring.rabbitmq.virtual-host: 连接到rabbitMQ的vhost
spring.rabbitmq.addresses: 指定client连接到的server的地址,多个以逗号分隔(优先取addresses,然后再取host)
spring.rabbitmq.requested-heartbeat: 指定心跳超时,单位秒,0为不指定;默认60s
spring.rabbitmq.publisher-confirms: 是否启用【发布确认】
spring.rabbitmq.publisher-returns: 是否启用【发布返回】
spring.rabbitmq.connection-timeout: 连接超时,单位毫秒,0表示无穷大,不超时
spring.rabbitmq.parsed-addresses:

# ssl
spring.rabbitmq.ssl.enabled: 是否支持ssl
spring.rabbitmq.ssl.key-store: 指定持有SSL certificate的key store的路径
spring.rabbitmq.ssl.key-store-password: 指定访问key store的密码
spring.rabbitmq.ssl.trust-store: 指定持有SSL certificates的Trust store
spring.rabbitmq.ssl.trust-store-password: 指定访问trust store的密码
spring.rabbitmq.ssl.algorithm: ssl使用的算法,例如,TLSv1.1

# cache
spring.rabbitmq.cache.channel.size: 缓存中保持的channel数量
spring.rabbitmq.cache.channel.checkout-timeout: 当缓存数量被设置时,从缓存中获取一个channel的超时时间,单位毫秒;如果为0,则总是创建一个新channel
spring.rabbitmq.cache.connection.size: 缓存的连接数,只有是CONNECTION模式时生效
spring.rabbitmq.cache.connection.mode: 连接工厂缓存模式:CHANNEL 和 CONNECTION

# listener
spring.rabbitmq.listener.simple.auto-startup: 是否启动时自动启动容器
spring.rabbitmq.listener.simple.acknowledge-mode: 表示消息确认方式,其有三种配置方式,分别是none、manual和auto;默认auto
spring.rabbitmq.listener.simple.concurrency: 最小的消费者数量
spring.rabbitmq.listener.simple.max-concurrency: 最大的消费者数量
spring.rabbitmq.listener.simple.prefetch: 指定一个请求能处理多少个消息,如果有事务的话,必须大于等于transaction数量.
spring.rabbitmq.listener.simple.transaction-size: 指定一个事务处理的消息数量,最好是小于等于prefetch的数量.
spring.rabbitmq.listener.simple.default-requeue-rejected: 决定被拒绝的消息是否重新入队;默认是true(与参数acknowledge-mode有关系)
spring.rabbitmq.listener.simple.idle-event-interval: 多少长时间发布空闲容器时间,单位毫秒

spring.rabbitmq.listener.simple.retry.enabled: 监听重试是否可用
spring.rabbitmq.listener.simple.retry.max-attempts: 最大重试次数
spring.rabbitmq.listener.simple.retry.initial-interval: 第一次和第二次尝试发布或传递消息之间的间隔
spring.rabbitmq.listener.simple.retry.multiplier: 应用于上一重试间隔的乘数
spring.rabbitmq.listener.simple.retry.max-interval: 最大重试时间间隔
spring.rabbitmq.listener.simple.retry.stateless: 重试是有状态or无状态

# template
spring.rabbitmq.template.mandatory: 启用强制信息;默认false
spring.rabbitmq.template.receive-timeout: receive() 操作的超时时间
spring.rabbitmq.template.reply-timeout: sendAndReceive() 操作的超时时间
spring.rabbitmq.template.retry.enabled: 发送重试是否可用
spring.rabbitmq.template.retry.max-attempts: 最大重试次数
spring.rabbitmq.template.retry.initial-interval: 第一次和第二次尝试发布或传递消息之间的间隔
spring.rabbitmq.template.retry.multiplier: 应用于上一重试间隔的乘数
spring.rabbitmq.template.retry.max-interval: 最大重试时间间隔

3.3、在默认情况下的主要配置如下


四、RabbitMQ设置消费者多线程

使用@RabbitListener注解指定消费方法,默认情况是单线程监听队列,可以观察当队列有多个任务时消费端每次只消费一个消息,单线程处理消息容易引起消息处理缓慢导致消息堆积,不能最大利用CPU资源。

可以配置RabbitMQ的容器工厂参数,增加并发处理数量即可实现多线程处理监听队列,实现多线程处理消息;也可以在RabbitListeners注解上加上concurrency属性。

4.1、在RabbitListeners注解上加上concurrency属性

    @RabbitListener(queues = "DirectQueue",concurrency =  "10")
    public void process(Message message) throws Exception
    {
        // 执行程序
    }

或者在配置文件中加入:

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    listener:
      simple:
#        acknowledge-mode: manual  # 手动确定(默认自动确认)
        concurrency: 10 # 消费端的监听个数(即@RabbitListener开启几个线程去消费数据)
        max-concurrency: 20 # 消费端的监听最大个数
        prefetch: 10
    connection-timeout: 15000   # 超时时间

4.2、配置工厂的时候设置并发

(1)在RabbitmqConfig.java中添加容器工厂配置

 @Bean("customContainerFactory")
    public SimpleRabbitListenerContainerFactory containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConcurrentConsumers(10);  //设置线程数
        factory.setMaxConcurrentConsumers(10); //最大线程数
        configurer.configure(factory, connectionFactory);
        return factory;
    }

(2)在@RabbitListener注解中指定容器工厂

@RabbitListener(queues = {"监听队列名"},containerFactory = "customContainerFactory")

再次测试当队列有多个任务时消费端的并发处理能力。


 参考链接:

[RabbitMQ]SpringBoot的RabbitMQTemplate实战

Springboot整合RabbitMQ(一)——Direct直连交换机

@RabbitListener的concurrency属性

SpringBoot2.x下RabbitMQ的并发参数(concurrency和prefetch)

Springboot整合RabbitMQ的手动执行确认ACK

Kafka 六战 RabbitMQ,这差距还不够明显吗?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java架构何哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值