SpringBoot接入消息中间件RabbitMQ

RabbitMQ介绍

RabbitMQ是一个开源的消息代理的队列服务器,用来通过普通协议在完全不同的应用之间共享数据。

RabbitMQ是使用Erlang语言来编写的,并且RabbitMQ是基于AMQP协议的。

Erlang语言在数据交互方面性能优秀,有着和原生Socket一样的延迟,这也是RabbitMQ高性能的原因所在。

AMQP协议:Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。

RabbitMQ消息模型

所有 MQ 产品从模型抽象上来说都是一样的过程,如下:

消费者(consumer)订阅某个队列。生产者(producer)创建消息,然后发布到队列(queue)中,最后将消息发送到监听的消费者。

RabbitMQ核心概念

Broker:表示消息队列服务器实体。接受客户端的连接,实现AMQP实体服务。简单来说就是消息队列服务器实体。

Connection:网络连接,比如一个TCP连接。应用程序与Broker的网络连接。

Channel:网络信道,几乎所有的操作都在Channel中进行,Channel是进行消息读写的通道。客户端可建立多个Channel,每个Channel代表一个会话任务。也就是说,一般情况是程序起始建立TCP连接,第二步就是建立这个Channel。

Message:消息,服务器和应用程序之间传送的数据,由Properties和Body组成。Properties可以对消息进行修饰,比如消息的优先级、延迟等高级特性;Body则就是消息体内容。

Virtual host:虚拟地址,用于进行逻辑隔离,最上层的消息路由。一个Virtual Host里面可以有若干个Exchange和Queue,同一个VirtualHost 里面不能有相同名称的Exchange或Queue。

每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。

Exchange:交换机,接收消息,根据路由键转发消息到绑定的队列。它指定消息按什么规则,路由到哪个队列

BindingExchange和Queue之间的虚拟连接,binding中可以包含routing key。用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。它的作用就是把exchange和queue按照路由规则绑定起来。

Routing key:一个路由规则,虚拟机可用它来确定如何路由一个特定消息。exchange根据这个关键字进行消息投递

Queue:也称为Message Queue,消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。

Producer:消息生产者,就是投递消息的程序。也是一个向交换器发布消息的客户端应用程序。

Consumer:消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。

AMQP的消息路由过程

Exchange 说明

四种类型:direct、fanout、topic、headers 

1)headers:headers 匹配 AMQP 消息的 header 而不是路由键,此外 headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了。

2)direct:Exchange路由规则很简单,它会把消息路由到那些binding key与routing key完全匹配的Queue中,不匹配的则不会转发,它是单播的模式,如图:

3)fanout:每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上,fanout 类型转发消息是最快的。如图:

4)topic:前面讲到direct类型的Exchange路由规则是完全匹配binding key与routing key,但这种严格的匹配方式在很多情况下不能满足实际业务需求。topic类型的Exchange在匹配规则上进行了扩展,它与direct类型的Exchage相似,也是将消息路由到binding key与routing key相匹配的Queue中,但这里的匹配规则有些不同,它约定:

  • routing key为一个句点号“. ”分隔的字符串(我们将被句点号“. ”分隔开的每一段独立的字符串称为一个单词),如“stock.usd.nyse”、“nyse.vmw”、“quick.orange.rabbit”
  • binding key与routing key一样也是句点号“. ”分隔的字符串
  • binding key中可以存在两种特殊字符“*”与“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)

示意图如下:

以上图中的配置为例,routingKey=”quick.orange.rabbit”的消息会同时路由到Q1与Q2,routingKey=”lazy.orange.fox”的消息会路由到Q1,routingKey=”lazy.brown.fox”的消息会路由到Q2,routingKey=”lazy.pink.rabbit”的消息会路由到Q2(只会投递给Q2一次,虽然这个routingKey与Q2的两个bindingKey都匹配);routingKey=”quick.brown.fox”、routingKey=”orange”、routingKey=”quick.orange.male.rabbit”的消息将会被丢弃,因为它们没有匹配任何bindingKey。

其他示意图:

SpringBoot集成RabbitMQ

rabbitmq基本用法

引入相关依赖

<!-- 添加springboot对amqp的支持 -->
<!-- org.springframework.retry:spring-retry:1.2.4.RELEASE依赖有问题,所以剔除再引入 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit</artifactId>
    <version>2.1.7.RELEASE</version>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-amqp</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-amqp</artifactId>
    <version>2.1.7.RELEASE</version>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.3.3</version>
</dependency>

application.yml配置rabbitmq

server:
  port: 9999

spring:
  # rabbitmq配置
  rabbitmq:
    addresses: 192.168.198.155:5672
    virtual-host: /admin_toov5 #需要在rabbitmq管理后天中添加 /admin_toov5 目录
    username: admin
    password: admin
    listener:
      simple:
        acknowledge-mode: auto # auto 和 manual,如果manual则需要自己确认
        concurrency: 5
        max-concurrency: 10

fanout工作模式

1)RabbitFanoutConfig.java

主要是声明exchange和queue。

/**
 * fanout模式:交换机和队列直接绑定,与路由键无关
 */
@Configuration
public class RabbitFanoutConfig {

    // 邮件队列
    private String FANOUT_EMAIL_QUEUE = "fanout_eamil_queue";

    // 短信队列
    private String FANOUT_SMS_QUEUE = "fanout_sms_queue";

    // 交换机
    private String EXCHANGE_NAME = "fanoutExchange";

    /**
     * 定义队列
     * @return
     */
    //邮件队列
    @Bean
    public Queue fanoutEmailQueue() {
        return new Queue(FANOUT_EMAIL_QUEUE);
    }
    //短信队列
    @Bean
    public Queue fanoutSMSQueue() {
        return new Queue(FANOUT_SMS_QUEUE);
    }

    /**
     * 定义交换机
     * @return
     */
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange(EXCHANGE_NAME);
    }

    /**
     * 队列和交换机绑定, 参数名称和定义好的方法名称一致
     * @param fanoutEmailQueue
     * @param fanoutExchange
     * @return
     */
    @Bean
    public Binding bindingExchangeEamil(Queue fanoutEmailQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanoutEmailQueue).to(fanoutExchange);
    }

    @Bean
    public Binding bindingExchangeSMS(Queue fanoutSMSQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanoutSMSQueue).to(fanoutExchange);
    }

}

2)FanoutProducer.java 消息发送者

@Component
public class FanoutProducer {

    @Autowired
    private AmqpTemplate amqpTemplate;

    public void send(String queueName) {
        System.out.println("fanout queueName:" + queueName);
        String mString = "msg:" + new Date();
        amqpTemplate.convertAndSend(queueName, mString);  //发送消息, 没有路由键
    }
}

3)消息消费者

@Component
@RabbitListener(queues = "fanout_eamil_queue")   //监听
public class FanoutEmailConsumer {

    @RabbitHandler  //表示接收消息
    public void process(String mString) {
        System.out.println("邮件消费者获取生产者消息msg" + mString);
    }
}
@Component
@RabbitListener(queues = "fanout_sms_queue")   //监听
public class FanoutSmsConsumer {

    @RabbitHandler  //表示接收消息
    public void process(String mString) {
        System.out.println("短信消费者获取生产者消息msg" + mString);
    }
}

4)测试验证

@RestController
public class MemberProcuderController {

    @Autowired
    private FanoutProducer fanoutProducer;

    @RequestMapping("/sendFanoutMsg")
    public String sendFanoutMsg(String queueName) {
        fanoutProducer.send(queueName);
        return "success";
    }
}

启动应用,访问:

http://localhost:9999/sendFanoutMsg?queueName=fanout_eamil_queue
http://localhost:9999/sendFanoutMsg?queueName=fanout_sms_queue

Direct工作模式

1)RabbitDirectConfig.java

/**
 * direct模式:与路由键(routingKey)有关,只有交换机和队列的绑定key完全匹配才进行转发
 */
@Configuration
public class RabbitDirectConfig {

    /**
     * 交换机名称
     */
    public static final String DIRECT_EXCHANGE = "direct_exchange";

    // 1.声明direct路由模式的交换机
    @Bean
    public DirectExchange getExchange(){
        return new DirectExchange(DIRECT_EXCHANGE,true,false);
    }

    // 2.声明三个队列队列:emailQueue、smsQueue、qqQueue
    @Bean
    public Queue getEmailQueue(){
        return new Queue("email_direct_Queue",true,false,false);
    }
    @Bean
    public Queue getSMSQueue(){
        return new Queue("sms_direct_Queue",true,false,false);
    }
    @Bean
    public Queue getQqQueue(){
        return new Queue("qq_direct_Queue",true,false,false);
    }

    // 3.绑定交换机与队列的关系
    @Bean
    public Binding getEmailBinding(){
        // 注:与fanout模式不同的是,需要声明交换机与队列之间的BindingKey,如下with设置
        return BindingBuilder.bind(getEmailQueue()).to(getExchange()).with("email");
    }
    @Bean
    public Binding getSMSBinding(){
        return BindingBuilder.bind(getSMSQueue()).to(getExchange()).with("sms");
    }
    @Bean
    public Binding getQQBinding(){
        return BindingBuilder.bind(getQqQueue()).to(getExchange()).with("qq");
    }
}

2)DirectProducer.java 消息生产者

@Component
public class DirectProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(String routingKey) {
        System.out.println("direct routingKey:" + routingKey);
        String mString = "msg:" + new Date();
        //指定交换机和路由键可以知道绑定的queue, 根据第二个参数完全匹配
        rabbitTemplate.convertAndSend(RabbitDirectConfig.DIRECT_EXCHANGE, routingKey, mString);
    }
}

3)DirectConsumer.java 消息消费者

@Component
public class DirectConsumer {

    @RabbitListener(queues = "email_direct_Queue")
    public void processEmail(Message message) throws UnsupportedEncodingException {
        System.out.println("email_direct_Queue consumer = " + new String(message.getBody(), "utf-8"));
    }

    @RabbitListener(queues = "sms_direct_Queue")
    public void processSms(Message message) throws UnsupportedEncodingException {
        System.out.println("sms_direct_Queue consumer = " + new String(message.getBody(), "utf-8"));
    }

    @RabbitListener(queues = "qq_direct_Queue")
    public void processQq(Message message) throws UnsupportedEncodingException {
        System.out.println("qq_direct_Queue consumer = " + new String(message.getBody(), "utf-8"));
    }
}

4)测试验证

@RestController
public class MemberProcuderController {

    @Autowired
    private DirectProducer directProducer;

    @RequestMapping("/sendDirectMsg")
    public String sendDirectMsg(String routingKey) {
        directProducer.send(routingKey);
        return "success";
    }
}

启动项目,访问:

http://localhost:9999/sendDirectMsg?routingKey=email
http://localhost:9999/sendDirectMsg?routingKey=sms
http://localhost:9999/sendDirectMsg?routingKey=qq

Topic工作模式

1)RabbitTopicConfig.java

/**
 * topic模式:与direct模式类似,但是路由键(routingKey)匹配规则更加灵活,支持模糊匹配
 */
@Configuration
public class RabbitTopicConfig {

    /**
     * 交换机名称
     */
    public static final String TOPIC_EXCHANGE = "topic_exchange";

    @Bean(name = "topic_queue1")
    public Queue topic_queue1() {
        return new Queue("topic_queue1");
    }

    @Bean(name = "topic_queue2")
    public Queue topic_queue2() {
        return new Queue("topic_queue2");
    }

    @Bean
    public TopicExchange exchange() {
        return new TopicExchange(TOPIC_EXCHANGE);
    }

    @Bean
    Binding bindingExchangeTopicQueue1(@Qualifier("topic_queue1") Queue topic_queue1, TopicExchange exchange) {
        return BindingBuilder.bind(topic_queue1).to(exchange).with("topic.message");
    }

    @Bean
    Binding bindingExchangeTopicQueue2(@Qualifier("topic_queue2") Queue topic_queue2, TopicExchange exchange) {
        // *表示一个词,#表示零个或多个词
        return BindingBuilder.bind(topic_queue2).to(exchange).with("topic.#");
    }
}

2)TopicProducer.java 消息生产者

@Component
public class TopicProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(String routingKey) {
        System.out.println("topic routingKey:" + routingKey);
        String mString = "msg:" + new Date();
        //RabbitMQ将会根据第二个参数去寻找有没有匹配此规则的队列
        //如果有,则把消息给它,如果有不止一个,则把消息分发给匹配的队列(每个队列都有消息!)
        rabbitTemplate.convertAndSend(RabbitTopicConfig.TOPIC_EXCHANGE, routingKey, mString);
    }
}

3)TopicConsumer.java 消息消费者

@Component
public class TopicConsumer {

    @RabbitListener(queues = "topic_queue1")
    public void processEmail(Message message) throws UnsupportedEncodingException {
        System.out.println("topic_queue1 consumer = " + new String(message.getBody(), "utf-8"));
    }

    @RabbitListener(queues = "topic_queue2")
    public void processSms(Message message) throws UnsupportedEncodingException {
        System.out.println("topic_queue2 consumer = " + new String(message.getBody(), "utf-8"));
    }
}

4)测试验证

@RestController
public class MemberProcuderController {

    @Autowired
    private TopicProducer topicProducer;

    @RequestMapping("/sendTopicMsg")
    public String sendTopicMsg(String routingKey) {
        topicProducer.send(routingKey);
        return "success";
    }
}

启动项目,访问:

http://localhost:9999/sendTopicMsg?routingKey=topic.message
http://localhost:9999/sendTopicMsg?routingKey=topic.msg
http://localhost:9999/sendTopicMsg?routingKey=topic

spring.rabbitmq常见配置项

基础信息

spring.rabbitmq.host: 默认localhost
spring.rabbitmq.port: 默认5672
spring.rabbitmq.username: 用户名
spring.rabbitmq.password: 密码
spring.rabbitmq.virtual-host: 连接到代理时用的虚拟主机
spring.rabbitmq.addresses: 连接到server的地址列表(以逗号分隔),先addresses后host 
spring.rabbitmq.requested-heartbeat: 请求心跳超时时间,0为不指定,如果不指定时间单位默认为妙
spring.rabbitmq.publisher-confirms: 是否启用【发布确认】,默认false
spring.rabbitmq.publisher-returns: 是否启用【发布返回】,默认false
spring.rabbitmq.connection-timeout: 连接超时时间,单位毫秒,0表示永不超时 

SSL

spring.rabbitmq.ssl.enabled: 是否支持ssl,默认false
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.trust-store-type=JKS:Trust store 类型.
spring.rabbitmq.ssl.algorithm: ssl使用的算法,默认由rabiitClient配置
spring.rabbitmq.ssl.validate-server-certificate=true:是否启用服务端证书验证
spring.rabbitmq.ssl.verify-hostname=true 是否启用主机验证

缓存cache

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

listener

spring.rabbitmq.listener.type=simple: 容器类型.simple或direct
 
spring.rabbitmq.listener.simple.auto-startup=true: 是否启动时自动启动容器
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: 一个消费者最多可处理的nack消息数量,如果有事务的话,必须大于等于transaction数量.
spring.rabbitmq.listener.simple.transaction-size: 当ack模式为auto时,一个事务(ack间)处理的消息数量,最好是小于等于prefetch的数量.若大于prefetch, 则prefetch将增加到这个值
spring.rabbitmq.listener.simple.default-requeue-rejected: 决定被拒绝的消息是否重新入队;默认是true(与参数acknowledge-mode有关系)
spring.rabbitmq.listener.simple.missing-queues-fatal=true 若容器声明的队列在代理上不可用,是否失败; 或者运行时一个多多个队列被删除,是否停止容器
spring.rabbitmq.listener.simple.idle-event-interval: 发布空闲容器的时间间隔,单位毫秒
spring.rabbitmq.listener.simple.retry.enabled=false: 监听重试是否可用
spring.rabbitmq.listener.simple.retry.max-attempts=3: 最大重试次数
spring.rabbitmq.listener.simple.retry.max-interval=10000ms: 最大重试时间间隔
spring.rabbitmq.listener.simple.retry.initial-interval=1000ms:第一次和第二次尝试传递消息的时间间隔
spring.rabbitmq.listener.simple.retry.multiplier=1: 应用于上一重试间隔的乘数
spring.rabbitmq.listener.simple.retry.stateless=true: 重试时有状态or无状态
 
spring.rabbitmq.listener.direct.acknowledge-mode= ack模式
spring.rabbitmq.listener.direct.auto-startup=true 是否在启动时自动启动容器
spring.rabbitmq.listener.direct.consumers-per-queue= 每个队列消费者数量.
spring.rabbitmq.listener.direct.default-requeue-rejected= 默认是否将拒绝传送的消息重新入队.
spring.rabbitmq.listener.direct.idle-event-interval= 空闲容器事件发布时间间隔.
spring.rabbitmq.listener.direct.missing-queues-fatal=false若容器声明的队列在代理上不可用,是否失败.
spring.rabbitmq.listener.direct.prefetch= 每个消费者可最大处理的nack消息数量.
spring.rabbitmq.listener.direct.retry.enabled=false  是否启用发布重试机制.
spring.rabbitmq.listener.direct.retry.initial-interval=1000ms # Duration between the first and second attempt to deliver a message.
spring.rabbitmq.listener.direct.retry.max-attempts=3 # Maximum number of attempts to deliver a message.
spring.rabbitmq.listener.direct.retry.max-interval=10000ms # Maximum duration between attempts.
spring.rabbitmq.listener.direct.retry.multiplier=1 # Multiplier to apply to the previous retry interval.
spring.rabbitmq.listener.direct.retry.stateless=true # Whether retries are stateless or stateful.

template

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

参考:https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#integration-properties

AmqpTemplate/RabbitTemplate

Spring AMQP提供了一个发送和接收消息的操作模板类AmqpTemplate。 AmqpTemplate它定义包含了发送和接收消息等的一些基本的操作功能。RabbitTemplate是AmqpTemplate的一个实现。

RabbitTemplate支持消息的确认与返回,为了返回消息,RabbitTemplate 需要设置mandatory 属性为true,并且CachingConnectionFactory 的publisherReturns属性也需要设置为true。返回的消息会根据它注册的RabbitTemplate.ReturnCallbacksetReturnCallback 回调发送到给客户端,

一个RabbitTemplate仅能支持一个ReturnCallback 。

为了确认Confirms消息, CachingConnectionFactory 的publisherConfirms 属性也需要设置为true,确认的消息会根据它注册的RabbitTemplate.ConfirmCallback setConfirmCallback回调发送到给客户端。一个RabbitTemplate也仅能支持一个ConfirmCallback。

发送消息

AmqpTemplate提供了以下的几个方法来发送消息:

void send(Message message) throws AmqpException;
void send(String routingKey, Message message) throws AmqpException;
void send(String exchange, String routingKey, Message message) throws AmqpException;

发送时,需要指定Exchange和routingKey。如:

amqpTemplate.send("marketData.topic", "quotes.nasdaq.FOO",new Message("12.34".getBytes(), someProperties));

从Spring-Rabbit版本1.3开始,它提供了MessageBuilder 和 MessagePropertiesBuilder两个类,这可以方便我们以fluent流式的方式创建Message及MessageProperties对象。

Message message = MessageBuilder.withBody("foo".getBytes())
    .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN)
    .setMessageId("123")
    .setHeader("bar", "baz")
    .build();

接收消息

接收消息可以有两种方式。比较简单的一种方式是,直接去查询获取消息,即调用receive方法,如果该方法没有获得消息,receive方法不阻塞,直接返回null。另一种方式是注册一个Listener监听器,一旦有消息到时来,异步接收。

注意: 接收消息,都需要从queue中获得,接收消息时,RabbitTemplate需要指定queue,或者设置默认的queue。

API提供的直接获得消息方法,如下:

Message receive() throws AmqpException; 
Message receive(String queueName) throws AmqpException;

AmqpTemplate也提供了方法来直接接收POJOs对象(代替Message对象),同时也提供了各种的MessageConverter用来处理返回的Object对象。

Object receiveAndConvert() throws AmqpException;
Object receiveAndConvert(String queueName) throws AmqpException;

AmqpTemplate 从Spring-Rabbit 1.3版本开始也提供了 receiveAndReply 方法来异步接收、处理及回复消息

AmqpTemplate 要注意receive和reply阶段,大数时情况下,你需要提供仅仅一个ReceiveAndReplyCallback 的实现,用它来处理接收到消息和回复(对象)消息的业务逻辑。同时也要注意,ReceiveAndReplyCallback 可能返回null,在这种情况下,receiveAndReply 就相当于receive 方法。

boolean receiveAndReply(ReceiveAndReplyCallback callback) throws AmqpException;
boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback) throws AmqpException;
boolean receiveAndReply(ReceiveAndReplyCallback callback, String replyExchange, String replyRoutingKey) throws AmqpException;
boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback,String replyExchange, String replyRoutingKey) throws AmqpException;

异步接受消息

为了异步接收消息,Spring AMQP也提供了多种不同的实现方式。如:通过实现MessageListener的方式,或者通过注解@RabbitListener的方式等,来实现异步接受消息及处理

MessageListener 异步接收消息

MessageListener 定义比较简单。

public interface MessageListener {
    void onMessage(Message message);
}

如果你的程序需要依赖Channel,你需要用 ChannelAwareMessageListener这个接口。

public interface ChannelAwareMessageListener {
    void onMessage(Message message, Channel channel) throws Exception;
}

如果你想将你的程序及Message API严格分开的话,可以用MessageListenerAdapter这个适配器类

MessageListenerAdapter listener = new MessageListenerAdapter(somePojo);
listener.setDefaultListenerMethod("myMethod");

看一下Container 容器。一般地,容器是处于“主动”的责任,以使侦听器回调可以保持被动触发处理。容器是生命周期的组件,它提供了开始和停止容器的方法。在配置容器时,我们需要配置AMQP Queue和MessageListener的对应连接,需要配置ConnectionFactory 的一个引用以及Listener、以及能够从该Queue中接收消息的Queue的引用,这样的Container才能通知到对应的Listener。

SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(rabbitConnectionFactory);
container.setQueueNames("some.queue");
container.setMessageListener(new MessageListenerAdapter(somePojo));

RabbitMQ 的3.2版本开始, broker支持消费消息的priority 优先级。只需要在SimpleMessageListenerContainer 添加priority属性的设置。

利用注解@RabbitListener实现异步接收消息

从spring-rabbit 1.4版本开始,可以利用注解的@RabbitListener来异步接收消息,它是更为简便的方式。

@Component
public class MyService {

    @RabbitListener(queues = "myQueue")
    public void processOrder(String data) { ... }

}

在容器中,需要配置@EnableRabbit,来支持@RabbitListener起作用(SpringBoot不需要)。

@Configuration
@EnableRabbit
public class AppConfig { 

  @Bean
  public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory());
    factory.setConcurrentConsumers(3);
    factory.setMaxConcurrentConsumers(10);
    return factory;
  }
}

通过上面的配置,就配置好Listener和Container了,就可以异步接收消息了。

消息转换

AmqpTemplate 定义提供了各种发送和接收委拖给MessageConverter转化对象消息的方法。MessageConverter 本身比较简单,它提供了消息对象的转化,可将object转化成Message 对象,或者将Message 对象转化成Object对象。它提供了默认的SimpleMessageConverter实现,以及第三方的MessageConverter,如Jackson2JsonMessageConverter,MarshallingMessageConverter等,来处理消息与对象之间的转换。 

SimpleMessageConverter

SimpleMessageConverter是Spring AMQP中MessageConverter的一个默认实现。

public interface MessageConverter {

  Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException; 
  Object fromMessage(Message message) throws MessageConversionException; 

}

为了自定义转化对象,你也可以第三方的MessageConverter,如使用  Jackson2JsonMessageConverter 或者MarshallingMessageConverter,其中Jackson2JsonMessageConverter提供了Json对象的转化,MarshallingMessageConverter对象则提供了对象的Marshaller和 Unmarshaller转化。 

ContentTypeDelegatingMessageConverter

从Spring-Rabbit 1.4.2开始,它提供了ContentTypeDelegatingMessageConverter,它能根据不同的MessageProperties属性(contentType)决定来委托给具体的哪一个MessageConverter。

RabbitMQ消费端手动ACK确认机制

ack——acknowledge(vt. 承认;答谢;报偿;告知已收到;确认的意思),在RabbitMQ中指代的是消费者收到消息后确认的一种行为,关注点在于消费者能否实际接收到MQ发送的消息。

RabbitMQ提供了三种确认方式:

  • 自动确认acknowledge="none":当消费者接收到消息的时候,就会自动给到RabbitMQ一个回执,告诉MQ我已经收到消息了,不在乎消费者接收到消息之后业务处理的成功与否。
  • 手动确认acknowledge="manual":当消费者收到消息后,不会立刻告诉RabbitMQ已经收到消息了,而是等待业务处理成功后,通过调用代码的方式手动向MQ确认消息已经收到。当业务处理失败,就可以做一些重试机制,甚至让MQ重新向消费者发送消息都是可以的。
  • 根据异常情况确认acknowledge="auto":该方式是通过抛出异常的类型,来做响应的处理(如重发、确认等)。这种方式比较麻烦。

当消息一旦被消费者接收到,会立刻自动向MQ确认接收,并将响应的message从RabbitMQ消息缓存中移除,但是在实际的业务处理中,会出现消息收到了,但是业务处理出现异常的情况,在自动确认的模式下,该条业务处理失败的message就相当于被丢弃了。

如果设置了手动确认,则需要在业务处理完成之后,手动调用channel.basicAck(),手动的签收,如果业务处理失败,则手动调用channel.basicNack()方法拒收,并让MQ重新发送该消息。

如果不做任何关于acknowledge的配置,默认就是自动确认签收的。

1、消费端配置:开启ack

2、通过channel的basicAck方法手动签收,通过basicNack方法拒绝签收

@RabbitListener(queues = "sms_direct_Queue") //监听
public void receiveMessage(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
    try {
        System.out.println("接收的mq消息:" + message);

        // 业务处理 异常测试
        // System.out.println("业务处理"+1/0);

        // long deliveryTag 消息接收tag boolean multiple 是否批量确认
        System.out.println("deliveryTag=" + deliveryTag);

        /**
             * 无异常就确认消息
             * basicAck(long deliveryTag, boolean multiple)
             * deliveryTag:取出来当前消息在队列中的的索引;
             * multiple:为true的话就是批量确认,如果当前deliveryTag为5,那么就会确认
             * deliveryTag为5及其以下的消息;一般设置为false
             */
        if (deliveryTag == 5) {
            channel.basicAck(deliveryTag, true);
        }

    } catch (Exception e) {
        e.printStackTrace();
        /**
             * 有异常就绝收消息
             * basicNack(long deliveryTag, boolean multiple, boolean requeue)
             * requeue:true为将消息重返当前消息队列,还可以重新发送给消费者;
             *         false:将消息丢弃
             */

        // long deliveryTag, boolean multiple, boolean requeue

        try {

            channel.basicNack(deliveryTag, false, true);
            // long deliveryTag, boolean requeue
            // channel.basicReject(deliveryTag,true);

            Thread.sleep(1000);     // 这里只是便于出现死循环时查看

            /*
                 * 一般实际异常情况下的处理过程:记录出现异常的业务数据,将它单独插入到一个单独的模块,
                 * 然后尝试3次,如果还是处理失败的话,就进行人工介入处理
                 */

        } catch (Exception e1) {
            e1.printStackTrace();
        }

    }
}

 

posted @ 2022-04-24 14:33  残城碎梦  阅读(805)  评论(0编辑  收藏  举报