技术最佳实践-RabbitMQ深入研究

1.应用场景

  • 异步通信
  • 服务解耦
  • 接口限流
  • 延迟处理
  • 消息分发

2.安装RabbitMq

#指定版本,该版本包含了web控制页面
docker pull rabbitmq:management

#方式一:默认guest 用户,密码也是 guest
docker run -d --hostname my-rabbit --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq:management

#方式二:设置用户名和密码
docker run -d --hostname my-rabbit --name rabbit -e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=password -p 15672:15672 -p 5672:5672 rabbitmq:management

访问ui界面

http://172.28.8.103:15672/#/queues

端口

5672: java连接端口

15672: 管理页面端口

3.RabbitMq相关词汇介绍

(1)producer:消息生产者,发送消息的一方,发送消息到RabbitMQ,消息包括消息体(即自定义的消息)和附加信息(如交换器名称、RoutingKey和一些自定义的属性);
(2)consumer:消息消费者,消费消息的一方,接收RabbitMQ的消息,它无需知道生产者是谁;
(3)broker:消息中间件的服务节点,可以认为是单个的服务器;
(4)virtualHost:虚拟主机(简称vhost),表示一批交换机、消息队列和相关对象,虚拟主机共享相同身份认证和加密环境(类似数据库中一个库的概念),默认的vhost为“/”;
(5)channel:建立在connection之上的轻量级连接,一个connection中可有多个channel(类似光缆中的光纤);
(6)routingKey:路由键,根据此键路由到指定的exchange上;
(7)exchange:交换器,生产者发送消息由它负责路由到不同的queue上,如果路由不到则返回给生产者或者直接丢弃消息;exchange包含以下四种类型:
  <1>fanout:消息发到此类型exchange上,与它绑定的queue都会收到消息;
  <2>direct:消息发到此类型exchange上,只有全匹配queue与exchange绑定上的routingKey才会接收到消息;
  <3>topic:消息发到此类型exchange上,模糊匹配queue与exchange绑定上的routingKey即可接收到消息;
  <4>headers:消息发到此类型exchange上,根据属性值匹配上的queue会接收到消息(一般不用);
(8)queue:消息队列,用于存储消息,一个queue可以对应多个消费者,但是不会出现重复发送同一消息给不同消费者的情况;
(9)binding:绑定关系,指exchange和queue的绑定关系,也会指定一个routingKey。
对应以上的名词解释,RabbitMQ的架构图如下:

在这里插入图片描述

4.Springboot整合RabbitMQ

4.1 引入依赖
	<!--rabbitmq-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
        <version>1.3.3.RELEASE</version>
    </dependency>
4.2 整合fastjson
package com.debug.middleware.server.config;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.support.converter.AbstractJsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConversionException;

import java.io.UnsupportedEncodingException;

/**
 * @author pgc
 * @date 2020/8/6 23:18
 */
@Slf4j
public class FastJsonMessageConverter extends AbstractJsonMessageConverter {
    @Override
    protected Message createMessage(Object objectToConvert, MessageProperties messageProperties) {
        String jsonString = JSON.toJSONString(objectToConvert);
        byte[] bytes = new byte[0];
        try {
            bytes = jsonString.getBytes(this.getDefaultCharset());
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        messageProperties.setContentType(objectToConvert.getClass().getName());
        // 消息持久化
        messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
        messageProperties.setReceivedDeliveryMode(MessageDeliveryMode.PERSISTENT);
        return new Message(bytes, messageProperties);
    }

    @Override
    public Object fromMessage(Message message) throws MessageConversionException {
        String contentType = message.getMessageProperties().getContentType();
        byte[] body = message.getBody();
        try {
            String jsonStr = new String(body, this.getDefaultCharset());
            return JSON.parseObject(jsonStr, Class.forName(contentType));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}

4.3 RabbitMq配置文件
package com.debug.middleware.server.config;

import com.debug.middleware.server.rabbitmq.consumer.KnowledgeManualConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

import java.util.HashMap;
import java.util.Map;

/**
 * RabbitMQ自定义注入Bean配置
 * Created by guchang.pan@hand-china.com
 */
@Configuration
public class RabbitmqConfig {

    private static final Logger log= LoggerFactory.getLogger(RabbitmqConfig.class);

    @Autowired
    private CachingConnectionFactory connectionFactory;

    @Autowired
    private SimpleRabbitListenerContainerFactoryConfigurer factoryConfigurer;

    /**
     * 单一消费者
     * @return
     */
    @Bean(name = "singleListenerContainer")
    public SimpleRabbitListenerContainerFactory listenerContainer(){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new FastJsonMessageConverter());
        factory.setConcurrentConsumers(1);
        factory.setMaxConcurrentConsumers(1);
        factory.setPrefetchCount(1);
        return factory;
    }

    /**
     * 多个消费者
     * @return
     */
    @Bean(name = "multiListenerContainer")
    public SimpleRabbitListenerContainerFactory multiListenerContainer(){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factoryConfigurer.configure(factory,connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setAcknowledgeMode(AcknowledgeMode.NONE);
        factory.setConcurrentConsumers(10);
        factory.setMaxConcurrentConsumers(15);
        factory.setPrefetchCount(10);
        return factory;
    }

    /**
     * RabbitMQ发送消息的操作组件实例
     * @return
     */
    @Bean
    public RabbitTemplate rabbitTemplate(){
        connectionFactory.setPublisherConfirms(true);
        connectionFactory.setPublisherReturns(true);
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setMessageConverter(new FastJsonMessageConverter());
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                log.info("消息发送成功:correlationData({}),ack({}),cause({})",correlationData,ack,cause);
            }
        });
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}",exchange,routingKey,replyCode,replyText,message);
            }
        });
        return rabbitTemplate;
    }

    //定义读取配置文件的环境变量实例
    @Autowired
    private Environment env;

    /**创建简单消息模型:队列、交换机和路由 **/

    //创建队列
    @Bean(name = "basicQueue")
    public Queue basicQueue(){
        return new Queue(env.getProperty("mq.basic.info.queue.name"),true);
    }

    //创建交换机:在这里以DirectExchange为例,在后面章节中我们将继续详细介绍这种消息模型
    @Bean
    public DirectExchange basicExchange(){
        return new DirectExchange(env.getProperty("mq.basic.info.exchange.name"),true,false);
    }

    //创建绑定
    @Bean
    public Binding basicBinding(){
        return BindingBuilder.bind(basicQueue()).to(basicExchange()).with(env.getProperty("mq.basic.info.routing.key.name"));
    }


    /**创建简单消息模型-对象类型:队列、交换机和路由 **/

    //创建队列
    @Bean(name = "objectQueue")
    public Queue objectQueue(){
        return new Queue(env.getProperty("mq.object.info.queue.name"),true);
    }

    //创建交换机:在这里以DirectExchange为例,在后面章节中我们将继续详细介绍这种消息模型
    @Bean
    public DirectExchange objectExchange(){
        return new DirectExchange(env.getProperty("mq.object.info.exchange.name"),true,false);
    }

    //创建绑定
    @Bean
    public Binding objectBinding(){
        return BindingBuilder.bind(objectQueue()).to(objectExchange()).with(env.getProperty("mq.object.info.routing.key.name"));
    }



    /**创建消息模型-fanoutExchange **/

    //创建队列1
    @Bean(name = "fanoutQueueOne")
    public Queue fanoutQueueOne(){
        return new Queue(env.getProperty("mq.fanout.queue.one.name"),true);
    }

    //创建队列2
    @Bean(name = "fanoutQueueTwo")
    public Queue fanoutQueueTwo(){
        return new Queue(env.getProperty("mq.fanout.queue.two.name"),true);
    }

    //创建交换机-fanoutExchange
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange(env.getProperty("mq.fanout.exchange.name"),true,false);
    }

    //创建绑定1
    @Bean
    public Binding fanoutBindingOne(){
        return BindingBuilder.bind(fanoutQueueOne()).to(fanoutExchange());
    }

    //创建绑定2
    @Bean
    public Binding fanoutBindingTwo(){
        return BindingBuilder.bind(fanoutQueueTwo()).to(fanoutExchange());
    }


    /**创建消息模型-directExchange **/

    //创建交换机-directExchange
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange(env.getProperty("mq.direct.exchange.name"),true,false);
    }

    //创建队列1
    @Bean(name = "directQueueOne")
    public Queue directQueueOne(){
        return new Queue(env.getProperty("mq.direct.queue.one.name"),true);
    }

    //创建队列2
    @Bean(name = "directQueueTwo")
    public Queue directQueueTwo(){
        return new Queue(env.getProperty("mq.direct.queue.two.name"),true);
    }

    //创建绑定1
    @Bean
    public Binding directBindingOne(){
        return BindingBuilder.bind(directQueueOne()).to(directExchange()).with(env.getProperty("mq.direct.routing.key.one.name"));
    }

    //创建绑定2
    @Bean
    public Binding directBindingTwo(){
        return BindingBuilder.bind(directQueueTwo()).to(directExchange()).with(env.getProperty("mq.direct.routing.key.two.name"));
    }


    /**创建消息模型-topicExchange **/

    //创建交换机-topicExchange
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange(env.getProperty("mq.topic.exchange.name"),true,false);
    }

    //创建队列1
    @Bean(name = "topicQueueOne")
    public Queue topicQueueOne(){
        return new Queue(env.getProperty("mq.topic.queue.one.name"),true);
    }

    //创建队列2
    @Bean(name = "topicQueueTwo")
    public Queue topicQueueTwo(){
        return new Queue(env.getProperty("mq.topic.queue.two.name"),true);
    }

    //创建绑定1
    @Bean
    public Binding topicBindingOne(){
        return BindingBuilder.bind(topicQueueOne()).to(topicExchange()).with(env.getProperty("mq.topic.routing.key.one.name"));
    }

    //创建绑定2
    @Bean
    public Binding topicBindingTwo(){
        return BindingBuilder.bind(topicQueueTwo()).to(topicExchange()).with(env.getProperty("mq.topic.routing.key.two.name"));
    }





    /**创建自动确认消费消息模型 **/

    /**
     * 单一消费者-确认模式为AUTO
     * @return
     */
    @Bean(name = "singleListenerContainerAuto")
    public SimpleRabbitListenerContainerFactory listenerContainerAuto(){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setConcurrentConsumers(1);
        factory.setMaxConcurrentConsumers(1);
        factory.setPrefetchCount(1);
        //设置确认消费模式为自动确认消费-AUTO
        factory.setAcknowledgeMode(AcknowledgeMode.AUTO);
        return factory;
    }

    //创建队列
    @Bean(name = "autoQueue")
    public Queue autoQueue(){
        return new Queue(env.getProperty("mq.auto.knowledge.queue.name"),true);
    }

    //创建交换机
    @Bean
    public DirectExchange autoExchange(){
        return new DirectExchange(env.getProperty("mq.auto.knowledge.exchange.name"),true,false);
    }

    //创建绑定
    @Bean
    public Binding autoBinding(){
        return BindingBuilder.bind(autoQueue()).to(autoExchange()).with(env.getProperty("mq.auto.knowledge.routing.key.name"));
    }


    /**
     * 单一消费者-确认模式为MANUAL
     * @return
     */

    //创建队列
    @Bean(name = "manualQueue")
    public Queue manualQueue(){
        return new Queue(env.getProperty("mq.manual.knowledge.queue.name"),true);
    }
    //创建交换机
    @Bean
    public TopicExchange manualExchange(){
        return new TopicExchange(env.getProperty("mq.manual.knowledge.exchange.name"),true,false);
    }
    //创建绑定
    @Bean
    public Binding manualBinding(){
        return BindingBuilder.bind(manualQueue()).to(manualExchange()).with(env.getProperty("mq.manual.knowledge.routing.key.name"));
    }
    //定义手动确认消费模式对应的消费者实例
    @Autowired
    private KnowledgeManualConsumer knowledgeManualConsumer;

    /**
     * 创建单一消费者-确认模式为MANUAL-并指定监听的队列和消费者
     * @param manualQueue
     * @return
     */
    @Bean(name = "simpleContainerManual")
    public SimpleMessageListenerContainer simpleContainer(@Qualifier("manualQueue") Queue manualQueue){
        SimpleMessageListenerContainer container=new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setMessageConverter(new Jackson2JsonMessageConverter());

        //TODO:并发配置
        container.setConcurrentConsumers(1);
        container.setMaxConcurrentConsumers(1);
        container.setPrefetchCount(1);

        //TODO:消息确认模式-采用人为手动确认消费机制
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        container.setQueues(manualQueue);
        container.setMessageListener(knowledgeManualConsumer);

        return container;
    }


    /**用户登录成功写日志消息模型创建**/

    //创建队列
    @Bean(name = "loginQueue")
    public Queue loginQueue(){
        return new Queue(env.getProperty("mq.login.queue.name"),true);
    }

    //创建交换机
    @Bean
    public TopicExchange loginExchange(){
        return new TopicExchange(env.getProperty("mq.login.exchange.name"),true,false);
    }

    //创建绑定
    @Bean
    public Binding loginBinding(){
        return BindingBuilder.bind(loginQueue()).to(loginExchange()).with(env.getProperty("mq.login.routing.key.name"));
    }
}

5.基于FanoutExchange的消息模型

我们通过Fanout exchange(扇型交换机)实现生产者发送一个消息,这个消息同时被传送给所有队列。

/**创建消息模型-fanoutExchange **/

    //创建队列1
    @Bean(name = "fanoutQueueOne")
    public Queue fanoutQueueOne(){
        return new Queue(env.getProperty("mq.fanout.queue.one.name"),true);
    }

    //创建队列2
    @Bean(name = "fanoutQueueTwo")
    public Queue fanoutQueueTwo(){
        return new Queue(env.getProperty("mq.fanout.queue.two.name"),true);
    }

    //创建交换机-fanoutExchange
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange(env.getProperty("mq.fanout.exchange.name"),true,false);
    }

    //创建绑定1
    @Bean
    public Binding fanoutBindingOne(){
        return BindingBuilder.bind(fanoutQueueOne()).to(fanoutExchange());
    }

    //创建绑定2
    @Bean
    public Binding fanoutBindingTwo(){
        return BindingBuilder.bind(fanoutQueueTwo()).to(fanoutExchange());
    }

配置文件

#消息模型-fanoutExchange
mq.fanout.queue.one.name=${mq.env}.middleware.mq.fanout.one.queue
mq.fanout.queue.two.name=${mq.env}.middleware.mq.fanout.two.queue
mq.fanout.exchange.name=${mq.env}.middleware.mq.fanout.exchange

测试类

    @Test
    public void test3() throws Exception{
        EventInfo info=new EventInfo(1,"增删改查模块","基于fanoutExchange的消息模型","这是基于fanoutExchange的消息模型");
        modelPublisher.sendMsgFanout(info);
    }

发送者

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private Environment env;


    /**
     * 发送消息-基于FanoutExchange消息模型
     * @param info
     */
    public void sendMsgFanout(EventInfo info){
        if (info!=null){
            try {
                rabbitTemplate.convertAndSend(
                        env.getProperty("mq.fanout.exchange.name"), 
                        null, 
                        info);
                log.info("消息模型fanoutExchange-生产者-发送消息:{} ",info);
            }catch (Exception e){
                log.error("消息模型fanoutExchange-生产者-发送消息发生异常:{} ",info,e.fillInStackTrace());
            }
        }
    }

消费者


    /**
     * 监听并消费队列中的消息-fanoutExchange-two
     */
    @RabbitListener(queues = "${mq.fanout.queue.two.name}",containerFactory = "singleListenerContainer")
    public void consumeFanoutMsgTwo(@Payload EventInfo info){
        try {
            log.info("消息模型fanoutExchange-two-消费者-监听消费到消息:{} ",info);


        }catch (Exception e){
            log.error("消息模型-消费者-发生异常:",e.fillInStackTrace());
        }
    }

6.基于DirectExchange的消息模型实战

6.1. 单个绑定

在上图中,有2个队列绑定到直连交换机上。队列Q1使用绑定值为orange,队列Q2绑定值为black,green。在这种情况下,如果生产者发送的消息的路由值为orange,则此消息会被路由到队列Q1。如果生产者发送的消息的路由值为blcak,green,则此消息会被路由到队列Q2。其它的消息会被丢弃

6.2. 多个绑定

在这里插入图片描述
我们也可以将相同的绑定值绑定到不同的队列中。如上图中,队列Q1和Q2使用的绑定值都black。如果生产者发送的消息的路由值为black,则此消息会被同时路由到队列Q1和队列Q2

//创建交换机-directExchange
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange(env.getProperty("mq.direct.exchange.name"),true,false);
    }

    //创建队列1
    @Bean(name = "directQueueOne")
    public Queue directQueueOne(){
        return new Queue(env.getProperty("mq.direct.queue.one.name"),true);
    }

    //创建队列2
    @Bean(name = "directQueueTwo")
    public Queue directQueueTwo(){
        return new Queue(env.getProperty("mq.direct.queue.two.name"),true);
    }

    //创建绑定1
    @Bean
    public Binding directBindingOne(){
        return BindingBuilder.bind(directQueueOne()).to(directExchange()).with(env.getProperty("mq.direct.routing.key.one.name"));
    }

    //创建绑定2
    @Bean
    public Binding directBindingTwo(){
        return BindingBuilder.bind(directQueueTwo()).to(directExchange()).with(env.getProperty("mq.direct.routing.key.two.name"));
    }

配置文件

#消息模型-directExchange
mq.direct.exchange.name=${mq.env}.middleware.mq.direct.exchange

mq.direct.routing.key.one.name=${mq.env}.middleware.mq.direct.routing.key.one
mq.direct.routing.key.two.name=${mq.env}.middleware.mq.direct.routing.key.two

mq.direct.queue.one.name=${mq.env}.middleware.mq.direct.one.queue
mq.direct.queue.two.name=${mq.env}.middleware.mq.direct.two.queue

测试类

    @Test
    public void test4() throws Exception{
        EventInfo info=new EventInfo(1,"增删改查模块-1","基于directExchange消息模型-1","directExchange-1");
        modelPublisher.sendMsgDirectOne(info);

        info=new EventInfo(2,"增删改查模块-2","基于directExchange消息模型-2","directExchange-2");
        modelPublisher.sendMsgDirectTwo(info);
    }

生产者

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private Environment env;
    /**
     * 发送消息-基于DirectExchange消息模型-one
     * @param info
     */
    public void sendMsgDirectOne(EventInfo info){
        if (info!=null){
            try {
                rabbitTemplate.convertAndSend(
                        env.getProperty("mq.direct.exchange.name"),
                        env.getProperty("mq.direct.routing.key.one.name"),
                        info
                );
                log.info("消息模型DirectExchange-one-生产者-发送消息:{} ",info);
            }catch (Exception e){
                log.error("消息模型DirectExchange-one-生产者-发送消息发生异常:{} ",info,e.fillInStackTrace());
            }
        }
    }

    /**
     * 发送消息-基于DirectExchange消息模型-two
     * @param info
     */
    public void sendMsgDirectTwo(EventInfo info){
        if (info!=null){
            try {
                rabbitTemplate.convertAndSend(
                        env.getProperty("mq.direct.exchange.name"),
                        env.getProperty("mq.direct.routing.key.two.name"),
                        info);

                log.info("消息模型DirectExchange-two-生产者-发送消息:{} ",info);
            }catch (Exception e){
                log.error("消息模型DirectExchange-two-生产者-发送消息发生异常:{} ",info,e.fillInStackTrace());
            }
        }
    }

消费者

   /**
     * 监听并消费队列中的消息-directExchange-one
     */
    @RabbitListener(queues = "${mq.direct.queue.one.name}",containerFactory = "singleListenerContainer")
    public void consumeDirectMsgOne(@Payload EventInfo info){
        try {
            log.info("消息模型directExchange-one-消费者-监听消费到消息:{} ",info);


        }catch (Exception e){
            log.error("消息模型directExchange-one-消费者-监听消费发生异常:",e.fillInStackTrace());
        }
    }

    /**
     * 监听并消费队列中的消息-directExchange-two
     */
    @RabbitListener(queues = "${mq.direct.queue.two.name}",containerFactory = "singleListenerContainer")
    public void consumeDirectMsgTwo(@Payload EventInfo info){
        try {
            log.info("消息模型directExchange-two-消费者-监听消费到消息:{} ",info);


        }catch (Exception e){
            log.error("消息模型directExchange-two-消费者-监听消费发生异常:",e.fillInStackTrace());
        }
    }

7.基于Topic Exchange通配符交模型

Topic Exchange交换机也叫通配符交换机,我们在发送消息到Topic Exchange的时候不能随意指定route key(应该是由一系列点号连接的字符串,一般会与binding key有关联,route key的长度一般不能超过255个字节)。同理,交换机与队列之间的binding key也应该是点号连接成的字符串,当消息发送者发送信息到Topic Exchage交换机的时候,这时候发送消息的route key会与binding key进行通配符匹配,所有匹配成功的消息都会发送到消息接受者。

Topic Exchange主要有两种通配符:# 和 *

  • *(星号):可以(只能)匹配一个单词
  • #(井号):可以匹配多个单词(或者零个)

下面我们根据一张图来理解一下Topic Exchange是怎么匹配的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sX9baE8l-1597109804899)(…/Image/20180922090459914)]

【a】一条以 ***com.register.mail***为routing key的消息将会匹配到Register Queue与SaveMail Queue两个队列上,所以消息会发送到消息接收者1和消息接收者2。routing key为“email.register.test”的消息同样会被推送到Register Queue与SaveMail Queue两个队列。

【b】如果routing key为”com.register.wsh”的话,消息只会被推送到Register Queue上;routing key为”email.com.wsh”的消息会被推送到SaveMail Queue上,routing key为"email.com.test”的消息也会被推送到SaveMail Queue上,但同一条消息只会被推送到SaveMail Queue上一次。

注意:如果在发送消息的时候没有匹配到符合条件的binding key,那么这条消息将会被废弃。如:com.register.wsh.test 消息不会被推送到Register Queue上,但是注意 email.com.wsh.test则可以推送到SaveMail Queue上。

  • 当一个队列被绑定为binding key为”#”时,它将会接收所有的消息,这类似于广播形式的交换机模式。
  • 当binding key不包含”*”和”#”时,这类似于我们上一章说的Direct Exchange直连交换机模式。

    /**创建消息模型-topicExchange **/

    //创建交换机-topicExchange
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange(env.getProperty("mq.topic.exchange.name"),true,false);
    }

    //创建队列1
    @Bean(name = "topicQueueOne")
    public Queue topicQueueOne(){
        return new Queue(env.getProperty("mq.topic.queue.one.name"),true);
    }

    //创建队列2
    @Bean(name = "topicQueueTwo")
    public Queue topicQueueTwo(){
        return new Queue(env.getProperty("mq.topic.queue.two.name"),true);
    }

    //创建绑定1
    @Bean
    public Binding topicBindingOne(){
        return BindingBuilder.bind(topicQueueOne()).to(topicExchange()).with(env.getProperty("mq.topic.routing.key.one.name"));
    }

    //创建绑定2
    @Bean
    public Binding topicBindingTwo(){
        return BindingBuilder.bind(topicQueueTwo()).to(topicExchange()).with(env.getProperty("mq.topic.routing.key.two.name"));
    }

配置文件

#消息模型-topicExchange
mq.topic.exchange.name=${mq.env}.middleware.mq.topic.exchange

mq.topic.routing.key.one.name=${mq.env}.middleware.mq.topic.routing.*.key
mq.topic.routing.key.two.name=${mq.env}.middleware.mq.topic.routing.#.key

mq.topic.queue.one.name=${mq.env}.middleware.mq.topic.one.queue
mq.topic.queue.two.name=${mq.env}.middleware.mq.topic.two.queue

测试类

    @Test
    public void test5() throws Exception{
        String msg="这是TopicExchange消息模型的消息";

        //此时相当于*,即java替代了*的位置;
        //当然由于#表示任意个单词,故而也将路由到#表示的路由和对应的队列中
        String routingKeyOne="local.middleware.mq.topic.routing.java.key";
        //此时相当于#:即 php.python 替代了#的位置
        String routingKeyTwo="local.middleware.mq.topic.routing.php.python.key";
        //此时相当于#:即0个单词
        String routingKeyThree="local.middleware.mq.topic.routing.key";

        //modelPublisher.sendMsgTopic(msg,routingKeyOne);
        //modelPublisher.sendMsgTopic(msg,routingKeyTwo);
        modelPublisher.sendMsgTopic(msg,routingKeyThree);
    }

发送消息

 /**
     * 发送消息-基于TopicExchange消息模型
     * @param msg
     */
    public void sendMsgTopic(String msg,String routingKey){
        //判断对象是否为null
        if (!Strings.isNullOrEmpty(msg) && !Strings.isNullOrEmpty(routingKey)){
            try {
                //发送消息
                rabbitTemplate.convertAndSend(
                        env.getProperty("mq.topic.exchange.name"),
                        routingKey,
                        msg
                );

                //打印日志
                log.info("消息模型TopicExchange-生产者-发送消息:{}  路由:{} ",msg,routingKey);
            }catch (Exception e){
                log.error("消息模型TopicExchange-生产者-发送消息发生异常:{} ",msg,e.fillInStackTrace());
            }
        }
    }

消费者

/**
     * 监听并消费队列中的消息-topicExchange-*通配符
     */
    @RabbitListener(queues = "${mq.topic.queue.one.name}",containerFactory = "singleListenerContainer")
    public void consumeTopicMsgOne(@Payload String message){
        try {
            log.info("消息模型topicExchange-*-消费者-监听消费到消息:{} ",message);


        }catch (Exception e){
            log.error("消息模型topicExchange-*-消费者-监听消费发生异常:",e.fillInStackTrace());
        }
    }


    /**
     * 监听并消费队列中的消息-topicExchange-#通配符
     */
    @RabbitListener(queues = "${mq.topic.queue.two.name}",containerFactory = "singleListenerContainer")
    public void consumeTopicMsgTwo(@Payload String message){
        try {
            log.info("消息模型topicExchange-#-消费者-监听消费到消息:{} ",message);


        }catch (Exception e){
            log.error("消息模型topicExchange-#-消费者-监听消费发生异常:",e.fillInStackTrace());
        }
    }

8.消息的确认消费

8.1 生产者确认消费
/**
     * RabbitMQ发送消息的操作组件实例
     * @return
     */
    @Bean
    public RabbitTemplate rabbitTemplate(){
        connectionFactory.setPublisherConfirms(true);
        connectionFactory.setPublisherReturns(true);
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setMessageConverter(new FastJsonMessageConverter());
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                log.info("消息发送成功:correlationData({}),ack({}),cause({})",correlationData,ack,cause);
            }
        });
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}",exchange,routingKey,replyCode,replyText,message);
            }
        });
        return rabbitTemplate;
    }
8.2 消费者确认消息

消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK

自动确认会在消息发送给消费者后立即确认,但存在丢失消息的可能,如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息

如果消息已经被处理,但后续代码抛出异常,使用 Spring 进行管理的话消费端业务逻辑会进行回滚,这也同样造成了实际意义的消息丢失

如果手动确认则当消费者调用 ack、nack、reject 几种方法进行确认,手动确认可以在业务失败后进行一些操作,如果消息未被 ACK 则会发送到下一个消费者

如果某个服务忘记 ACK 了,则 RabbitMQ 不会再发送数据给它,因为 RabbitMQ 认为该服务的处理能力有限

ACK 机制还可以起到限流作用,比如在接收到某条消息时休眠几秒钟

消息确认模式有:

  • AcknowledgeMode.NONE:自动确认
  • AcknowledgeMode.AUTO:根据情况确认
  • AcknowledgeMode.MANUAL:手动确认。

自动确认

/**
     * 单一消费者-确认模式为AUTO
     * @return
     */
    @Bean(name = "singleListenerContainerAuto")
    public SimpleRabbitListenerContainerFactory listenerContainerAuto(){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setConcurrentConsumers(1);
        factory.setMaxConcurrentConsumers(1);
        factory.setPrefetchCount(1);
        //设置确认消费模式为自动确认消费-AUTO
        factory.setAcknowledgeMode(AcknowledgeMode.AUTO);
        return factory;
    }

或者

spring.rabbitmq.listener.simple.acknowledge-mode=auto

手动确认

//定义手动确认消费模式对应的消费者实例
    @Autowired
    private KnowledgeManualConsumer knowledgeManualConsumer;

    /**
     * 创建单一消费者-确认模式为MANUAL-并指定监听的队列和消费者
     * @param manualQueue
     * @return
     */
    @Bean(name = "simpleContainerManual")
    public SimpleMessageListenerContainer simpleContainer(@Qualifier("manualQueue") Queue manualQueue){
        SimpleMessageListenerContainer container=new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setMessageConverter(new FastJsonMessageConverter());

        //TODO:并发配置
        container.setConcurrentConsumers(1);
        container.setMaxConcurrentConsumers(1);
        container.setPrefetchCount(1);

        //TODO:消息确认模式-采用人为手动确认消费机制
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        container.setQueues(manualQueue);
        container.setMessageListener(knowledgeManualConsumer);

        return container;
    }

生产者

 @Autowired
    private Environment env;

    @Autowired
    private RabbitTemplate rabbitTemplate;


    /**
     * 基于MANUAL机制-生产者发送消息
     * @param info
     */
    public void sendAutoMsg(KnowledgeInfo info){
        try {
            if (info!=null){
                rabbitTemplate.convertAndSend(
                        env.getProperty("mq.manual.knowledge.exchange.name"),
                        env.getProperty("mq.manual.knowledge.routing.key.name"),
                        info
                );
                log.info("基于MANUAL机制-生产者发送消息-内容为:{} ",info);
            }
        }catch (Exception e){
            log.error("基于MANUAL机制-生产者发送消息-发生异常:{} ",info,e.fillInStackTrace());
        }
    }

消费者

package com.debug.middleware.server.rabbitmq.consumer;/**
 * Created by Administrator on 2019/4/7.
 */

import com.alibaba.fastjson.JSON;
import com.debug.middleware.server.rabbitmq.entity.KnowledgeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 确认消费模式-人为手动确认消费-监听器
 * @Author:guchang.pan@hand-china.com
 **/
@Component("knowledgeManualConsumer")
public class KnowledgeManualConsumer implements ChannelAwareMessageListener{

    private static final Logger log= LoggerFactory.getLogger(KnowledgeManualConsumer.class);


    /**
     * 监听消费消息
     * @param message 消息实体
     * @param channel 通道
     * @throws Exception
     */
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        MessageProperties messageProperties=message.getMessageProperties();
        long deliveryTag=messageProperties.getDeliveryTag();

        try {
            byte[] msg=message.getBody();
            KnowledgeInfo info = JSON.parseObject(msg, Class.forName(message.getMessageProperties().getContentType()));
            log.info("确认消费模式-人为手动确认消费-监听器监听消费消息-内容为:{} ",info);

            //第一个参数为:消息的分发标识(唯一);第二个参数:是否允许批量确认消费(在这里设置为true)
            channel.basicAck(deliveryTag,true);
        }catch (Exception e){
            log.info("确认消费模式-人为手动确认消费-监听器监听消费消息-发生异常:",e.fillInStackTrace());

            //如果在处理消息的过程中发生了异常,则照样需要人为手动确认消费掉该消息
            // (否则该消息将一直留在队列中,从而将导致消息的重复消费)
            channel.basicReject(deliveryTag,false);
        }
    }
}

9.RabbitMQ持久化

9.1交换机持久化
//创建交换机:在这里以DirectExchange为例,在后面章节中我们将继续详细介绍这种消息模型
// 第二个参数设置为ture进行交换机持久化
    @Bean
    public DirectExchange basicExchange(){
        return new DirectExchange(env.getProperty("mq.basic.info.exchange.name"),true,false);
    }
9.2 队列持久化
 //创建队列
// 第二个参数设置为true进行队列持久化
    @Bean(name = "basicQueue")
    public Queue basicQueue(){
        return new Queue(env.getProperty("mq.basic.info.queue.name"),true);
    }
9.3 消息持久化
// 设置MessageDeliveryMode.PERSISTENT进行消息持久化
messageProperties.setContentType(objectToConvert.getClass().getName());
        messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
        messageProperties.setReceivedDeliveryMode(MessageDeliveryMode.PERSISTENT);
        return new Message(bytes, messageProperties);
package com.debug.middleware.server.config;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.support.converter.AbstractJsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConversionException;

import java.io.UnsupportedEncodingException;

/**
 * @author pgc
 * @date 2020/8/6 23:18
 */
@Slf4j
public class FastJsonMessageConverter extends AbstractJsonMessageConverter {
    @Override
    protected Message createMessage(Object objectToConvert, MessageProperties messageProperties) {
        String jsonString = JSON.toJSONString(objectToConvert);
        byte[] bytes = new byte[0];
        try {
            bytes = jsonString.getBytes(this.getDefaultCharset());
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        messageProperties.setContentType(objectToConvert.getClass().getName());
        messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
        messageProperties.setReceivedDeliveryMode(MessageDeliveryMode.PERSISTENT);
        return new Message(bytes, messageProperties);
    }

    @Override
    public Object fromMessage(Message message) throws MessageConversionException {
        String contentType = message.getMessageProperties().getContentType();
        byte[] body = message.getBody();
        try {
            String jsonStr = new String(body, this.getDefaultCharset());
            return JSON.parseObject(jsonStr, Class.forName(contentType));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}

10.死信队列/延迟队列

10.1 什么是死信队列

对rabbitmq来说,产生死信的来源大致有如下几种:

  • 消息被拒绝(basic.reject或basic.nack)并且requeue=false.
  • 消息TTL过期
  • 队列达到最大长度(队列满了,无法再添加数据到mq中)
10.2 死信的处理方式

死信的产生既然不可避免,那么就需要从实际的业务角度和场景出发,对这些死信进行后续的处理,常见的处理方式大致有下面几种,

  1. 丢弃,如果不是很重要,可以选择丢弃
  2. 记录死信入库,然后做后续的业务分析或处理
  3. 通过死信队列,由负责监听死信的应用程序进行处理
10.3 延迟队列的使用场景

那么什么时候需要用延时队列呢?考虑一下以下场景:

  1. 订单在十分钟之内未支付则自动取消。
  2. 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
  3. 账单在一周内未支付,则自动结算。
  4. 用户注册成功后,如果三天内没有登陆则进行短信提醒。
  5. 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
  6. 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议。

这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;发生店铺创建事件,十天后检查该店铺上新商品数,然后通知上新数为0的商户;发生账单生成事件,检查账单支付状态,然后自动结算未支付的账单;发生新用户注册事件,三天后检查新注册用户的活动数据,然后通知没有任何活动记录的用户;发生退款事件,在三天之后检查该订单是否已被处理,如仍未被处理,则发送消息给相关运营人员;发生预定会议事件,判断离会议开始是否只有十分钟了,如果是,则通知各个与会人员。

看起来似乎使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?如果数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求,如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。但对于数据量比较大,并且时效性较强的场景,如:“订单十分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。

更重要的一点是,不!优!雅!

没错,作为一名有追求的程序员,始终应该追求更优雅的架构和更优雅的代码风格,写代码要像写诗一样优美。

这时候,延时队列就可以闪亮登场了,以上场景,正是延时队列的用武之地。

10.4 如何利用RabbitMQ实现延迟队列?

想想看,延时队列,不就是想要消息延迟多久被处理吗,TTL则刚好能让消息在延迟多久之后成为死信,另一方面,成为死信的消息都会被投递到死信队列里,这样只需要消费者一直消费死信队列里的消息就万事大吉了,因为里面的消息都是希望被立即处理的消息。

从下图可以大致看出消息的流向:
在这里插入图片描述

生产者生产一条延时消息,根据需要延时时间的不同,利用不同的routingkey将消息路由到不同的延时队列,每个队列都设置了不同的TTL属性,并绑定在同一个死信交换机中,消息过期后,根据routingkey的不同,又会被路由到不同的死信队列中,消费者只需要监听对应的死信队列进行处理即可。

/**死信队列消息模型构建**/

    //创建死信队列
    @Bean
    public Queue basicDeadQueue() {
        Map<String, Object> args = new HashMap();
        args.put("x-dead-letter-exchange", env.getProperty("mq.dead.exchange.name"));
        args.put("x-dead-letter-routing-key", env.getProperty("mq.dead.routing.key.name"));

        //设定TTL,单位为ms,在这里指的是10s
        args.put("x-message-ttl", 10000);
        return new Queue(env.getProperty("mq.dead.queue.name"), true, false, false, args);
    }

    //创建“基本消息模型”的基本交换机 - 面向生产者
    @Bean
    public TopicExchange basicProducerExchange() {
        return new TopicExchange(env.getProperty("mq.producer.basic.exchange.name"), true, false);
    }

    //创建“基本消息模型”的基本绑定-基本交换机+基本路由 - 面向生产者
    @Bean
    public Binding basicProducerBinding() {
        return BindingBuilder.bind(basicDeadQueue()).to(basicProducerExchange()).with(env.getProperty("mq.producer.basic.routing.key.name"));
    }

    //创建真正队列 - 面向消费者
    @Bean
    public Queue realConsumerQueue() {
        return new Queue(env.getProperty("mq.consumer.real.queue.name"), true);
    }

    //创建死信交换机
    @Bean
    public TopicExchange basicDeadExchange() {
        return new TopicExchange(env.getProperty("mq.dead.exchange.name"), true, false);
    }

    //创建死信路由及其绑定
    @Bean
    public Binding basicDeadBinding() {
        return BindingBuilder.bind(realConsumerQueue()).to(basicDeadExchange()).with(env.getProperty("mq.dead.routing.key.name"));
    }

生产者

package com.debug.middleware.server.rabbitmq.publisher;/**
 * Created by Administrator on 2019/4/9.
 */

import com.debug.middleware.server.rabbitmq.entity.DeadInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

/**
 * 死信队列-生产者
 * @Author: guchang.pan@hand-china.com
 **/
@Component
public class DeadPublisher {
    //定义日志
    private static final Logger log= LoggerFactory.getLogger(DeadPublisher.class);
    //定义读取环境变量实例
    @Autowired
    private Environment env;
    //定义RabbitMQ操作组件
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendMsg(DeadInfo info){
        try {
            //发送对象类型的消息
            rabbitTemplate.convertAndSend(
                    env.getProperty("mq.producer.basic.exchange.name"),
                    env.getProperty("mq.producer.basic.routing.key.name"),
                    info
            );
            //打印日志
            log.info("死信队列实战-发送对象类型的消息入死信队列-内容为:{} ",info);

        }catch (Exception e){
            log.error("死信队列实战-发送对象类型的消息入死信队列-发生异常:{} ",info,e.fillInStackTrace());
        }
    }
}

消费者

package com.debug.middleware.server.rabbitmq.consumer;/**
 * Created by Administrator on 2019/4/9.
 */

import com.debug.middleware.server.rabbitmq.entity.DeadInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;

/**
 * 死信队列-真正队列消费者
 * @Author: guchang.pan@hand-china.com
 **/
@Component
public class DeadConsumer {
    //定义日志
    private static final Logger log= LoggerFactory.getLogger(DeadConsumer.class);

    /**
     * 监听真正队列-消费队列中的消息 - 面向消费者
     * @param info
     */
    @RabbitListener(queues = "${mq.consumer.real.queue.name}",containerFactory = "singleListenerContainer")
    public void consumeMsg(@Payload DeadInfo info){
        try {
            log.info("死信队列实战-监听真正队列-消费队列中的消息,监听到消息内容为:{}",info);

            //TODO:用于执行后续的相关业务逻辑

        }catch (Exception e){
            log.error("死信队列实战-监听真正队列-消费队列中的消息 - 面向消费者-发生异常:{} ",info,e.fillInStackTrace());
        }
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

潘顾昌

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

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

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

打赏作者

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

抵扣说明:

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

余额充值