RabbitMQ学习第一课 工作模型与Java编程

典型应用场景 
1、跨系统的异步通信 人民银行二代支付系统,使用重量级消息队列 IBM MQ,异步,解耦,削峰都有体现。

2、应用内的同步变成异步 秒杀:自己发送给自己

3、基于Pub/Sub模型实现的事件驱动 放款失败通知、提货通知、购买碎屏保 系统间同步数据 摒弃ELT(比如全量 同步商户数据); 摒弃API(比如定时增量获取用户、获取产品,变成增量广播)。
4、利用RabbitMQ实现事务的最终一致性 。

一、RabbitMQ简介

1.1、什么是RabbitMQ?

    RabbitMQ是一个开元的消息代理和队列服务器,用来通过普通协议在完全不同的应用之间共享数据,RabbitMQ是使用Erlang语言来编写的,并且RabbitMQ是基于 AMQP协议的。

1.2、AMQP协议 


    AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应 用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户 端/中间件同产品、不同的开发语言等条件的限制。
AMQP的实现有:RabbitMQ、OpenAMQ、Apache Qpid、Redhat Enterprise MRG、AMQP Infrastructure、 ØMQ、Zyre等。 

1.3 RabbitMQ的特性 


RabbitMQ使用Erlang语言编写,使用Mnesia数据库存储消息。
(1)可靠性(Reliability) RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。

(2)灵活的路由(Flexible Routing) 在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功 能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在 一起,也通过插件机制实现自己的 Exchange 。
(3)消息集群(Clustering) 多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。

(4)高可用(Highly Available Queues) 队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下 队列仍然可用。
(5)多种协议(Multi-protocol) RabbitMQ 支持多种消息队列协议,比如 AMQP、STOMP、MQTT 等等。
(6)多语言客户端(Many Clients) RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby、PHP、C#、 JavaScript 等等。

(7)管理界面(Management UI) RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集群 中的节点。
(8)插件机制(Plugin System) RabbitMQ提供了许多插件,以实现从多方面扩展,当然也可以编写自己的插件。 

1.4 工作模型

     RabbitMQ是AMQP协议的一个开源项目实现,遵循AMQP协议中的基本概念。其工作模型图如下:

  1)Message(消息):消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列可选属性组成,这些属性包括 routing_key(路由键)、priority(优先级)、delivery_mode(指出该消息可能需要持久化存储)等。

  2)Producer(生产者) :消息生产者。主要将消息投递到对应的Exchange上面。一般是独立的程序。

  3) Exchange(交换器): 消息交换机。指定消息按照什么规则路由到哪个队列Queue。

  4)Binging(绑定):作用就是将Exchange和Queue按照某种路由规则绑定起来。

  5)Queue 消息队列。消息的载体,每条消息都会被投送到一个或多个队列中。

  6)Connection(网络连接)Producer 和 Consumer 与Broker之间的TCP长连接。

   7)Channel(消息通道) 消息通道,也称信道。在客户端的每个连接里可以建立多个Channel,每个Channel代表一 个会话任务。在RabbitMQ Java Client API中,channel上定义了大量的编程接口。

   8)Vhost   虚拟主机。一个Broker可以有多个虚拟主机,用作不同用户的权限分离。一个虚拟主机持有 一组Exchange、Queue和Binding。

    9)Broker  即RabbitMQ的实体服务器。提供一种传输服务,维护一条从生产者到消费者的传输线路, 保证消息数据能按照指定的方式传输。

   AMQP中的消息路由过程和 JMS存在一些差别,在AMQP中增加了Exchange和Binding的角色。生产者把消息发布到 Exchange上,消息最终到达队列并被消费,二Binding决定交换器的消息应该被发送到哪个队列中。

消息发送原理

首先你必须连接到Rabbit才能发布和消费消息,那怎么连接和发送消息的呢?

你的应用程序和Rabbit Server之间会创建一个TCP连接,一旦TCP打开,并通过了认证,认证就是你试图连接Rabbit之前发送的Rabbit服务器连接信息和用户名和密码,有点像程序连接数据库,使用Java有两种连接认证的方式,后面代码会详细介绍,一旦认证通过你的应用程序和Rabbit就创建了一条AMQP信道(Channel)。

信道是创建在“真实”TCP上的虚拟连接,AMQP命令都是通过信道发送出去的,每个信道都会有一个唯一的ID,不论是发布消息,订阅队列或者介绍消息都是通过信道完成的。

为什么不通过TCP直接发送命令?

对于操作系统来说创建和销毁TCP会话是非常昂贵的开销,假设高峰期每秒有成千上万条连接,每个连接都要创建一条TCP会话,这就造成了TCP连接的巨大浪费,而且操作系统每秒能创建的TCP也是有限的,因此很快就会遇到系统瓶颈。

如果我们每个请求都使用一条TCP连接,既满足了性能的需要,又能确保每个连接的私密性,这就是引入信道概念的原因。

1.5 交换器

 1、 Direct Exchange 直连交换机 
      定义:直连类型的交换机与一个队列绑定时,需要指定一个明确的binding key。 路由规则:发送消息到直连类型的交换机时,只有routing key跟binding key完全匹配时,绑定的队列才能收到消 息。
例如:
   //  只有队列1能收到消息

    channel.basicPublish("MY_DIRECT_EXCHANGE", "key1", null, msg.getBytes()); 

2、Topic Exchange 主题交换机 
定义:主题类型的交换机与一个队列绑定时,可以指定按模式匹配的routing key。
通配符有两个,*代表匹配一个单词。#代表匹配零个或者多个单词。单词与单词之间用 . 隔开。 路由规则:发送消息到主题类型的交换机时,routing key符合binding key的模式时,绑定的队列才能收到消息。
例如:

// 只有队列1能收到消息 
channel.basicPublish("MY_TOPIC_EXCHANGE", "sh.abc", null, msg.getBytes());  
 // 队列2和队列3能收到消息 
channel.basicPublish("MY_TOPIC_EXCHANGE", "bj.book", null, msg.getBytes());  
 // 只有队列4能收到消息 
channel.basicPublish("MY_TOPIC_EXCHANGE", "abc.def.food", null, msg.getBytes()); 

3、Fanout Exchange 广播交换机 
   定义:广播类型的交换机与一个队列绑定时,不需要指定binding key。

  路由规则:当消息发送到广播类型的交换机时,不需要指定routing key,所有与之绑定的队列都能收到消息。
例如:
// 3个队列都会收到消息

channel.basicPublish("MY_FANOUT_EXCHANGE", "", null, msg.getBytes())

     

4  Headers 交换器 匹配AQMP消息的Header而不是路由键,性能差几乎不用

1.6 消息持久化

    Rabbit队列和交换器有一个不可告人的秘密,就是默认情况下重启服务器会导致消息丢失,那么怎么保证Rabbit在重启的时候不丢失呢?答案就是消息持久化。

当你把消息发送到Rabbit服务器的时候,你需要选择你是否要进行持久化,但这并不能保证Rabbit能从崩溃中恢复,想要Rabbit消息能恢复必须满足3个条件:

  1. 投递消息的时候durable设置为true,消息持久化,代码:channel.queueDeclare(x, true, false, false, null),参数2设置为true持久化;
  2. 设置投递模式deliveryMode设置为2(持久)代码:channel.basicPublish(x, x, MessageProperties.PERSISTENT_TEXT_PLAIN,x),参数3设置为存储纯文本到磁盘
  3. 消息已经到达持久化交换器上;
  4. 消息已经到达持久化的队列;

持久化工作原理

Rabbit会将你的持久化消息写入磁盘上的持久化日志文件,等消息被消费之后,Rabbit会把这条消息标识为等待垃圾回收。

持久化的缺点

消息持久化的优点显而易见,但缺点也很明显,那就是性能,因为要写入硬盘要比写入内存性能较低很多,从而降低了服务器的吞吐量,尽管使用SSD硬盘可以使事情得到缓解,但他仍然吸干了Rabbit的性能,当消息成千上万条要写入磁盘的时候,性能是很低的。

所以使用者要根据自己的情况,选择适合自己的方式。

1.7 虚拟主机

每个Rabbit都能创建很多vhost,我们称之为虚拟主机,每个虚拟主机其实都是mini版的RabbitMQ,拥有自己的队列,交换器和绑定,拥有自己的权限机制。

vhost特性

  1. RabbitMQ默认的vhost是“/”开箱即用;

  2. 多个vhost是隔离的,多个vhost无法通讯,并且不用担心命名冲突(队列和交换器和绑定),实现了多层分离;

  3. 创建用户的时候必须指定vhost;

vhost操作

可以通过rabbitmqctl工具命令创建:

rabbitmqctl add_vhost[vhost_name]

删除vhost:

rabbitmqctl delete_vhost[vhost_name]

查看所有的vhost:

rabbitmqctl list_vhosts

二 Java编程

   maven工程引入:

<dependency>
  <groupId>com.rabbitmq</groupId>
  <artifactId>amqp-client</artifactId>
  <version>5.2.0</version>
</dependency>

    生产者代码:

public class MyProducer {

    //队列名称
    private final static String QUEUE_NAME = "ORIGIN_QUEUE";

    public static void main(String[] args) {
        //1  c创建连接g工厂
        ConnectionFactory factory = new ConnectionFactory();
       //2 连接ip
        factory.setHost("127.0.0.1");
        factory.setPort(5672);
       // 用户        
        factory.setUsername("guest");
        factory.setPassword("guest");
        // 虚拟机        
        factory.setVirtualHost("/");
        try {
            // 建立连接    
            Connection conn = factory.newConnection();
           //建立消息通道
            Channel channel = conn.createChannel();
            //声明队列
            // String queue  队列名称
            // boolean durable,  是否是否持久化
            // boolean exclusive,是否排他性队列。排他性队列只能在声明它的Connection中使用,连接断开时自动删除
            // boolean autoDelete,是否自动删除。如果为true,至少有一个消费者连接到这个队列,
                 // 之后所有与这个队列连接 的消费者都断开时,队列会自动删除。
           // Map<String, Object> arguments:队列的其他属性,
            // 例如x-message-ttl、x-expires、x-max-length、x-maxlength-bytes、
                   // x-dead-letter-exchange、x-dead-letter-routing-key、x-max-priority。
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);
            //消息
            String msg = "Hello world, Rabbit MQ";
            // 发送消息(发送到默认交换机AMQP Default,Direct)        
            // 如果有一个队列名称跟Routing Key相等,那么消息会路由到这个队列      
            // String exchange, String routingKey, BasicProperties props, byte[] body
            channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
            channel.close();
            conn.close();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }

    }

}

消费者代码:

public class MyConsumer {
     private final static String QUEUE_NAME = "ORIGIN_QUEUE";

    public static void main(String[] args) {
        //1  c创建连接g工厂
        ConnectionFactory factory = new ConnectionFactory();
        //2 连接ip
        factory.setHost("127.0.0.1");
        factory.setPort(5672);
        // 用户        
        factory.setUsername("guest");
        factory.setPassword("guest");
        // 虚拟机        
        factory.setVirtualHost("/");

        try {
            //创建连接
            Connection connection = factory.newConnection();
            //创建通道
            Channel channel =connection.createChannel();
            //声明队列
            // 声明队列【参数说明:参数一:队列名称,参数二:是否持久化;
            // 参数三:是否独占模式;参数四:消费者断开连接时是否删除队列;参数五:消息其他参数】
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            System.out.println(" Waiting for message....");
           //创建消费者
            Consumer consumer = new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope,
                                 AMQP.BasicProperties properties, byte[] body) throws IOException {
                    String routingKey = envelope.getRoutingKey(); // 队列名称
                    String contentType = properties.getContentType(); // 内容类型
                    String content = new String(body, "utf-8"); // 消息正文
                    System.out.println("消息正文:" + content);
                    // 手动确认消息【参数说明:参数一:该消息的index;
                    // 参数二:是否批量应答,true批量确认小于index的消息】
                 //   channel.basicAck(envelope.getDeliveryTag(), false);
                }
            };
            //1、队列名称 2 是否自动应答,3 消费者信息
           channel.basicConsume(QUEUE_NAME,true,consumer);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }

    }
}

三 进阶知识

1、TTL(Time To Live)

 rabbitmq 分为 队列过期 和消息过期,其中队列过期 其内的消息也会过期。

  队列过期配置

  //设置队列过期时间
             Map<String, Object> arguments = new HashMap<>();
            arguments.put("x-message-ttl",6000);
            // Map<String, Object> arguments:队列的其他属性,
           //例如x-message-ttl、x-expires、x-max-length、x-maxlength-bytes、
            // x-dead-letter-exchange、x-dead-letter-routing-key、x-max-priority。
            channel.queueDeclare(QUEUE_NAME,false,false,false,arguments);

消费过期 只针对消息

   AMQP.BasicProperties basicProperties = new AMQP.BasicProperties()
                    .builder().deliveryMode(2)// 持久化消息
                        .contentEncoding("UTF-8")
                         .expiration("5000")// TTL
                           .build();

            channel.basicPublish("",QUEUE_NAME,basicProperties,msg.getBytes());

2、死信队列 


  有三种情况消息会进入DLX(Dead Letter Exchange)死信交换机。
       1、(NACK || Reject ) && requeue == false 消费端处理失败,且不用重发。

       2、消息过期

     3、队列达到最大长度(先入队的消息会被发送到DLX)


3、服务端流控(Flow Control) 


      RabbitMQ 会在启动时检测机器的物理内存数值。默认当 MQ 占用 40% 以上内存时,MQ 会主动抛出一个内存警 告并阻塞所有连接(Connections)。可以通过修改 rabbitmq.config 文件来调整内存阈值,默认值是 0.4,如下 所示:   [{rabbit, [{vm_memory_high_watermark, 0.4}]}]. 默认情况,如果剩余磁盘空间在 1GB 以下,RabbitMQ 主动阻塞所有的生产者。这个阈值也是可调的。
注意队列长度只在消息堆积的情况下有意义,而且会删除先入队的消息,不能实现服务端限流。


  4、消费端限流 


             在AutoACK为false的情况下,如果一定数目的消息(通过基于consumer或者channel设置Qos的值)未被确认 前,不进行消费新的消息。

channel.basicQos(2); // 如果超过2条消息没有发送ACK,当前消费者不再接受队列消息 channel.basicConsume(QUEUE_NAME, false, consumer);
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值