与RabbitMQ的第一次亲密接触

一、前言

首先说一下RabbitMQ,为什么叫RabbitMQ呢?
嘿嘿嘿,我想大部分人没想过这个问题,Rabbit兔子,作者是想表示可以兔子繁殖起来异常的疯狂,就像分布式系统一样。说到兔子疯狂繁殖,让我想到了高中时代做过的很多数学题和生物题。
说完命名问题,那为何要有RabbitMQ呢?他用来解决什么问题?适用于何种场景?如何使用?
这就是本文要讨论的问题,但由于作者也并未深入的了解,很多地方都是浅尝辄止,或只是说明部分内容。第一次亲密接触嘛,哪有第一次接触就深入的,是吧。

二、为何我们要选用RabbitMQ

1、Rabbit的作用

(1)解耦

消息中间件在处理过程中加入了一个隐含的、基于数据的接口层,两边的处理过程都需要实现这个接口,这允许你独立的扩展或修改两边的处理过程,只需要保证遵守同样的接口约束即可。

(2)扩展性

扩展性是基于解耦的特性的,因为发消息和收消息双发都是各自处理实现,所有有了很大的扩展性

(3)削峰

可以支撑突发访问压力,不会因为突发的超负荷请求而完全崩溃

(4)可恢复性

当部分组件失效时,不会影响整个系统。消息中间件降低了进程之间的耦合度,一个处理消息的进程挂掉,加入消息中间件的消息,依旧可以在系统恢复后进行处理

这里闲话一句,前几天刚读完《淘宝技术这十年》,里面讲到Notify组件的诞生过程,中间有一个很大的版本改造,就是加了数据库,消息队列的所有消息都会存储到数据库,避免由于宕机或其他原因,造成服务器关闭,而导致数据丢失的问题。

(5)顺序保证

大多数情况下,数据处理的顺序非常重要,大部分消息中间件支持一定程度的顺序性

(6)缓冲

由于不同的元素,其处理时间不同。写入消息中间件的处理会尽可能快速,该层有助于控制和优化数据经流系统的速度

(7)异步通信

很多时候,应用不会立即的处理消息。消息中间件提供异步处理机机制,允许应用把一些消息放入消息中间件,不会立即处理它

2、Rabbit的特性

(1)可靠性
(2)灵活的路由

在消息进入队列之前,通过交换器来路由消息,针对复杂的理由功能,可以将多个交换器绑定在一起,也可以通过插件机制实现自己的交换器

(3)扩展性

多个RabbitMQ节点可以组成一个集群,也可以根据实际业务动态的扩展集群中的节点

(4)高可用性
(5)多种协议
(6)多种客户端
(7)管理界面
(8)插件机制

3、遵循了AMQP协议

AMQP的模型架构和RabbitMQ的模型架构师一样的,生产者将消息发送给交换器,交换器和队列绑定,当生产者发送消息是,所写的的RoutingKey与绑定的BindingKey相匹配是,消息即被存入相对于的队列,消费者可以定于相应的队列来获取消息。
AMQP协议本身包括三层:

  • Modle Layer :协议最高层,主要定义了一些供客户端调用的命令,客户端可以利用这些命令实现自己的业务逻辑,例如,客户端可以使用Queue.Declare命令声明一个队列,或者使用Consume订阅消费一个队列中的消息。
  • Session Layer:位于中间层,主要负责将客户端的命令发送 给服务器,再将服务端的应答返回给客户端,主要为客户端与服务器之间的通信提供可靠的同步机制和错误处理。
  • Transport Layer: 位于最底层,主要传输二进制数据流,提供帧处理、信道复用、错误检测和数据表示等。
    AMQP说到底还是一个通信协议,通信协议都会涉及到报文交互,从low-level举例来说,AMQP本身是应用层的协议,其填充与TCP协议层的数据部分。而从high-level来说,AMQP是通过协议命令进行交互的。AMQP协议可以看做一系列结构化命令的协议,这里的命令协议代表一种操作,类似于HTTP中的方法(GET/POST/PUT/DELETE等)

四、RabbitMQ基础功能实现

1、RabbitConsumer 消费者

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-05-10 14:05
 */
public class RabbitConsumer {
	//定义队列名称
    private static final String QUEUE_NAME="queue_demo";
    //定义IP地址
    private static final String IP_ADDRESS="192.168.1.15";
    //定义端口号,此端口号是Rabbit默认端口号
    private static final int PORT=5672;

    public static void main(String [] args) throws IOException, TimeoutException, InterruptedException {
        Address [] addresses=new Address[]
                {new Address(IP_ADDRESS,PORT)};
        ConnectionFactory factory=new ConnectionFactory();
        factory.setUsername("root");
        factory.setPassword("root");
        //创建连接
        Connection connection=factory.newConnection(addresses);
        //创建通道,通道不同于一般网络连接,它是双向传输的。
        final Channel channel=connection.createChannel();
        Consumer consumer=new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("recv msg: "+new String(body));
                try {
                    TimeUnit.SECONDS.sleep(1);
                }catch (Exception e){
                    e.printStackTrace();
                }
                channel.basicAck(envelope.getDeliveryTag(),false);
           }
        };

        channel.basicConsume(QUEUE_NAME,consumer);
        TimeUnit.SECONDS.sleep(60);
        channel.close();
        connection.close();
    }
}

2、RabbitProducer 生产者

public class RabbitProducer {
    private static final String EXCHANGE_NAME="exchange_demo";
    private static final String ROUTING_KEY="routingkey_demo";
    private static final String QUEUE_NAME="queue_demo";
    private static final String IP_ADDRESS="192.168.1.15";
    private static final int PORT=5672;

    public static void main(String [] args) throws Exception{
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost(IP_ADDRESS);
        factory.setPort(PORT);
        factory.setUsername("root");
        factory.setPassword("root");
         //创建连接
        Connection connection=factory.newConnection();
          //创建通道,通道不同于一般网络连接,它是双向传输的。
        Channel channel=connection.createChannel();
        //创建一个Direct类型,持久化、非自动删除的交换器
        channel.exchangeDeclare(EXCHANGE_NAME,"direct",true,false,null);
        //创建一个持久化、非排他,非自动删除的交换器
        channel.queueDeclare(QUEUE_NAME,true,false,false,null);
        //绑定交换器和队列,通过路由键绑定【值得注意的是这里是通过路由键绑定】
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,ROUTING_KEY);
        String msg="Hello World";
        channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,
                MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());
        //close
        channel.close();
        connection.close();
    }

}

3、运行流程

1、服务器建立一个连接(生产者启动)。然后在这个连接之上创建一个信道、之后创建一个交换器和一个队列。并通过路由键绑定,发送消息,关闭当前资源
2、创建连接,注意创建时必须要指定连接地址。不同于生产者的PORT是绑定在factory上的,消费者的端口是,是在连接创建是绑定。
3、消费者创建信道。设置客户端最多接受未被ack的消息的个数。
4、注入监听,等待服务端响应。

五、RabbitMQ在Spring boot中的使用

1、添加依赖

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

2、设置连接属性

spring.rabbitmq.username=root
spring.rabbitmq.password=root
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
#投递消息间隔时间
spring.kafka.consumer.heartbeat-interval=2000
#投递消息最大次数
spring.rabbitmq.template.retry.max-attempts=2
#当为false时,如果没有收到消费者的ack,会无线投递,设置为true
#最多只会投递三次
spring.rabbitmq.listener.simple.retry.enabled=true

3、声明队列

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-05-17 11:05
 */
@Configuration
public class QueueConfiguration {
    public static final String FIRST_QUEUE="com.mq.first";
    public static final String TWO_QUEUE="com.mq.two";
    public static final String THREE_QUEUE="com.mq.three";
    public static final String REQUEST_QUEUE="REQUEST_QUEUE";

    @Bean(name = FIRST_QUEUE)
    public Queue getFirstQueue(){
        System.out.println("create Queue 1");
        return new Queue(FIRST_QUEUE);
    }

    @Bean(name = TWO_QUEUE)
    public Queue getTowQueue(){
        System.out.println("create Queue 2");

        return new Queue(TWO_QUEUE);
    }

    @Bean(name = THREE_QUEUE)
    public Queue getThreeQueue(){
        System.out.println("create Queue 3");
        return new Queue(THREE_QUEUE);
    }
}

2、交换器


/**
 * fanout路由策略(广播机制)的交换机注入、Queue与Exchange的绑定注入
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-05-17 11:11
 */
@Configuration
public class TopicExchangeAndBindingConfiguration {
    public static final String TOPIC_EXCHANGE="Topic_EXCHANGE";

    @Bean(name = TOPIC_EXCHANGE)
    TopicExchange  getFanoputExchange(){
        System.out.println("create Exchange");
        return new TopicExchange(TOPIC_EXCHANGE);
    }

    /**
     * 将myFirstQueue对应的Queue绑定到此topicExchange,并指定路由键为"routingKey.#"
     * 即:此Exchange中,路由键以"routingKey."开头的Queue将被匹配到
     * @param queue
     * @param myFanoutExchange
     * @return
     */
    @Bean
    Binding bindingQueueFirst(@Qualifier(QueueConfiguration.FIRST_QUEUE) Queue queue,@Qualifier(TOPIC_EXCHANGE) TopicExchange myFanoutExchange){
        System.out.println("bindingQueueFirst");
        return BindingBuilder.bind(queue).to(myFanoutExchange).with("com.#");
    }

    /**
     *将myTwoQueue对应的Queue绑定到此topicExchange,并指定路由键为"#.topic"
     *即:此Exchange中,路由键以".topic"结尾的Queue将被匹配到
     * @param queue
     * @param myFanoutExchange
     * @return
     */
    @Bean
    Binding bindingQueueTwo(@Qualifier(QueueConfiguration.TWO_QUEUE) Queue queue,@Qualifier(TOPIC_EXCHANGE) TopicExchange myFanoutExchange){
        System.out.println("bindingQueueTwo");
        return BindingBuilder.bind(queue).to(myFanoutExchange).with("#.two");
    }
    /**
     * 将myThreeQueue对应的Queue绑定到此topicExchange,并指定路由键为"#"
     * 即:此topicExchange中,任何Queue都将被匹配到
     */
    @Bean
    Binding bindingQueueThree(@Qualifier(QueueConfiguration.THREE_QUEUE) Queue queue,@Qualifier(TOPIC_EXCHANGE) TopicExchange myFanoutExchange){
        System.out.println("bindingQueueThree");
        return BindingBuilder.bind(queue).to(myFanoutExchange).with("#");
    }
}

3、监听消息


/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-05-17 11:18
 */
@Component
public class DemoMessageListener {

    @RabbitListener(queues = QueueConfiguration.FIRST_QUEUE)
    public void firstConsumer(String msg){
        System.out.println("I am first queue :"+ msg);
    }

    @RabbitListener(queues = QueueConfiguration.TWO_QUEUE)
    public void twoConsumer(String msg){
        System.out.println("I am two queue :"+ msg);
    }

    @RabbitListener(queues = QueueConfiguration.THREE_QUEUE)
    public void threeConsumer(String msg){
        System.out.println("I am three queue :"+ msg);
    }

4、发送消息

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-05-17 11:22
 */
@SpringBootTest(classes = StartApplication.class)
@RunWith(SpringRunner.class)
public class TestMQ {
    @Autowired
    private AmqpTemplate amqpTemplate;

    @Test
    public void sendMsg(){
        amqpTemplate.convertAndSend(TopicExchangeAndBindingConfiguration.TOPIC_EXCHANGE,QueueConfiguration.FIRST_QUEUE,"第一");
        amqpTemplate.convertAndSend(TopicExchangeAndBindingConfiguration.TOPIC_EXCHANGE,QueueConfiguration.TWO_QUEUE,"第二");
        amqpTemplate.convertAndSend(TopicExchangeAndBindingConfiguration.TOPIC_EXCHANGE,QueueConfiguration.THREE_QUEUE,"第三");
    }
}

5、请求流程

执行测试用例的代码。
发送消息,到指定交换器,再通过RoutingKey匹配BindingKey,如果匹配的上,就发送消息。监听可以监听到消息的发送。

6、RoutingKey与BindingKey

RoutingKey是路由key,BindingKey是交换器与队列绑定的key
请求发送消息后,会根据RoutingKey在交换器上寻找,交换器与队列连接的BindingKey,如果能匹配上,就将消息发送到队列。
消费者再到队列中取值。

六、RabbitMQ中的基础组件

1、生产者

说明

生产者是投递消息的一方

消息体

业务逻辑数据,如JSON串

标签

用来表述这条消息,比如一个交换器名称与一个路由键
生产者吧消息传给RabbitMQ,RabbitMQ之后会根据标签把消息发送给感兴趣的消费者

2、队列

说明

在消息路由的过程中,消息的标签会被丢弃,存入队列的只有消息的消息体

作用

是RabbitMQ的内部对象,用以存储消息

扩展

Kafka是将消息存储是topic上

多个消费者可以订阅同一个消息队列,这时队列的消息会会被轮询给多个消费者进行处理。而不是每个消费者都收到所有的消息并处理

3、消费者

说明

消费者hi接收消息的一方
消费者连接到RabbitMQ服务器,并订阅到队列上

4、Broker

说明

消息中间件的服务街店
对于RabbitMQ 来说,一个RabbitMQ Broker可以简单的看做一个RabbitMQ服务节点,或者说是RabbitMQ的服务实例。

5、交换器

说明

生产者将消息发送到交换器上,交换器将消息路由到一个或多个队列上,如果路由不到,或许会返回给生产者,或许直接丢弃

路由类型–RoutingKey路由键

生产者将消息发给交换器时,一般会指定一个RoutingKey,用来指定这个消息的路由规则。而这个RoutingKey则需要交换器类型和绑定键BindingKey联合才能最终生效
生产者在发送消息给交换器时可以通过RoutingKey指定消息的流向

路由类型–BindingKey绑定

将交换器与队列关联起来。生成一个BingdingKey

交换器类型
  • fanout
    他会把所有发送到该交换器的消息,路由到所有与该交换器绑定的队列中
  • direct
    会吧消息路由到BindingKey和RoutingKey完全匹配的队列中
  • topic
    会把消息路由到BindingKey和RoutingKey完全匹配的队列中
    RoutingKey为被点号‘.’分割的字符串
    com.rabbitmq.client、com.hidden.client
    BindingKey同上
    可以用*、#进行匹配
    匹配全部

一次请求的流程

  • 生产者处理业务数据
  • 序列化业务数据
  • 指定交换器以及路由Key,即为消息添加了Label(标签)
  • 发送消息到Broker中
  • RabbitMQ Broker处理消息
  • 消费者订阅并接受消息
  • 消费者获取到消息
  • 反序列化消息
  • 业务处理数据

七、总结

Rabbit作为消息中间件
在这里插入图片描述
在实际应用中的流程图大致如上:
前置场景是,监听中心已启动,监听中心已经创建了交换器,消息队列,以及绑定了交换器和消息队列。声明了监听,即当队列中有符合BindingKey的消息时会进入监听处理业务。
1、业务处理的服务A,要处理一个业务,但是业务耗时过长或由于其他原因,决定使用消息。
2、指定发送的路由键,交换器,以及发送的消息内容
3、监听中心调用其他微服务处理业务或直接处理业务

八、参考

参考书籍《RabbitMQ实战指南》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章