RabbitMQ 三种交换机、java api 操作代码


RabbitMQ 的特性

RabbitMQ使用Erlang语言编写,使用Mnesia数据库存储消息。

  1. 可靠性,RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认
  2. 灵活的路由 ,在消息进入队列之前,通过 Exchange 来路由消息的。
  3. 消息集群 ,多个RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker
  4. 高可用,队列可以再集群中的机器上进行镜像,使得部分节点出问题的情况下队列仍然可用
  5. 多种协议,支持AMQP,STOMP,MQTT
  6. 管理界面,提供了一个易用的用户界面,使得用户可以监控和管理消息,集群中的节点
  7. 插件机制

工作模型

概念解释
Broker即RabbitMQ的实体服务器。提供一种传输服务,维护一条从生产者到消费者的传输线路,保证消息数据能按照指定的方式传输。
Exchange消息交换机。指定消息按照什么规则路由到哪个队列Queue。
Queue消息队列。消息的载体,每条消息都会被投送到一个或多个队列中。
Binding绑定。作用就是将Exchange和Queue按照某种路由规则绑定起来。
Routingkey路由关键字。Exchange根据Routing Key进行消息投递。定义绑定时指定的关键字称为Binding Key。
Vhost虚拟主机。一个Broker可以有多个虚拟主机,用作不同用户的权限分离。一个虚拟主机持有一组Exchange、Queue和Binding。
Producer消息生产者。主要将消息投递到对应的Exchange上面。一般是独立的程序。
Consumer消息消费者。消息的接收者,一般是独立的程序。
ConnectionProducer 和 Consumer 与Broker之间的TCP长连接。
Channel消息通道,也称信道。在客户端的每个连接里可以建立多个Channel,每个Channel代表一个会话任务。在RabbitMQ Java Client API中,channel上定义了大量的编程接口。

三种主要的交换机

Direct Exchange 直连交换机

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

// 只有队列1能收到消息
channel.basicPublish("MY_DIRECT_EXCHANGE", "key1", null, msg.getBytes());

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());

Fanout Exchange 广播交换机

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

// 3个队列都会收到消息
channel.basicPublish("MY_FANOUT_EXCHANGE", "", null, msg.getBytes());

参数配置

TTL(Time To Live)

消息的过期时间

通过队列属性设置消息过期时间:

Map<String, Object> argss = new HashMap<String, Object>();
argss.put("x-message-ttl",6000);
channel.queueDeclare("TEST_TTL_QUEUE", false, false, false, argss);

设置单条消息的过期时间:

AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
    .deliveryMode(2) // 持久化消息
    .contentEncoding("UTF-8")
    .expiration("10000") // TTL
    .build();
channel.basicPublish("", "TEST_TTL_QUEUE", properties, msg.getBytes());

队列的过期时间

队列的过期时间决定了在没有任何消费者以后,队列可以存活多久

Map<String, Object> argss = new HashMap<String, Object>();
argss.put("x-message-ttl",6000);
channel.queueDeclare("TEST_TTL_QUEUE", false, false, false, argss);

死信队列

有三种情况消息会进入DLX(Dead Letter Exchange)死信交换机。

  1. (NACK || Reject ) && requeue == false
  2. 消息过期
  3. 队列达到最大长度(先入队的消息会被发送到DLX)

可以设置一个死信队列(Dead Letter Queue)与DLX绑定,即可以存储Dead Letter,消费者可以监听这个队列取走消息。

Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("x-dead-letter-exchange","DLX_EXCHANGE");
// 指定了这个队列的死信交换机
channel.queueDeclare("TEST_DLX_QUEUE", false, false, false, arguments);
// 声明死信交换机
channel.exchangeDeclare("DLX_EXCHANGE","topic", false, false, false, null);
// 声明死信队列
channel.queueDeclare("DLX_QUEUE", false, false, false, null);
// 绑定
channel.queueBind("DLX_QUEUE","DLX_EXCHANGE","#");

优先级队列

设置一个队列的最大优先级:

Map<String, Object> argss = new HashMap<String, Object>();
argss.put("x-max-priority",10); // 队列最大优先级
channel.queueDeclare("ORIGIN_QUEUE", false, false, false, argss);

发送消息时指定消息当前的优先级:

AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
	.priority(5) // 消息优先级
	.build();
channel.basicPublish("", "ORIGIN_QUEUE", properties, msg.getBytes());

优先级高的消息可以优先被消费,但是:只有消息堆积(消息的发送速度大于消费者的消费速度)的情况下优先级
才有意义。

延迟队列

RabbitMQ本身不支持延迟队列。可以使用TTL结合DLX的方式来实现消息的延迟投递,即把DLX跟某个队列绑定,
到了指定时间,消息过期后,就会从DLX路由到这个队列,消费者可以从这个队列取走消息。
另一种方式是使用rabbitmq-delayed-message-exchange插件。
当然,将需要发送的信息保存在数据库,使用任务调度系统扫描然后发送也是可以实现的。

服务端流控(Flow Control)

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

消费端限流

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

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

可靠性投递与生产实践

可靠性投递

首先需要明确,效率与可靠性是无法兼得的,如果要保证每一个环节都成功,势必会对消息的收发效率造成影响。
如果是一些业务实时一致性要求不是特别高的场合,可以牺牲一些可靠性来换取效率。

确保消息发送到RabbitMQ服务器

可能因为网络或者Broker的问题导致①失败,而生产者是无法知道消息是否正确发送到Broker的。
有两种解决方案,第一种是Transaction(事务)模式,第二种Confirm(确认)模式。
在通过channel.txSelect方法开启事务之后,我们便可以发布消息给RabbitMQ了,如果事务提交成功,则消息一定到达了RabbitMQ中,如果在事务提交执行之前由于RabbitMQ异常崩溃或者其他原因抛出异常,这个时候我们便可以将其捕获,进而通过执行channel.txRollback方法来实现事务回滚。使用事务机制的话会“吸干”RabbitMQ的性能,一般不建议使用。
生产者通过调用channel.confirmSelect方法(即Confirm.Select命令)将信道设置为confirm模式。一旦消息被投
递到所有匹配的队列之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确到达了目的地了。

确保消息路由到正确的队列

可能因为路由关键字错误,或者队列不存在,或者队列名称错误导致②失败。
使用mandatory参数和ReturnListener,可以实现消息无法路由的时候返回给生产者。
另一种方式就是使用备份交换机(alternate-exchange),无法路由的消息会发送到这个交换机上。

Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("alternate-exchange","ALTERNATE_EXCHANGE"); // 指定交换机的备份交换机
channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, arguments);

确保消息在队列正确地存储

可能因为系统宕机、重启、关闭等等情况导致存储在队列的消息丢失,即③出现问题。
解决方案:
1、队列持久化

// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String,
Object> arguments
channel.queueDeclare(QUEUE_NAME, true, false, false, null);

2、交换机持久化

// String exchange, boolean durable
channel.exchangeDeclare("MY_EXCHANGE","true");

3、消息持久化

AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2) // 2代表持久化,其他代表瞬态
.build();
channel.basicPublish("", QUEUE_NAME, properties, msg.getBytes());

面试题

1、消息队列的作用与使用场景?

作用:消息队列可以做消息通信、异步处理、应用解耦、流量削锋

使用场景:

  • 比如用户注册后,需要对用户发送有邮件和注册短信,或者给用户在业务表中添加一条记录进行说明
  • 比如在秒杀的场景下,由于请求量过大,可以放在消息队列中,一方面一个控制人数,两一方面可以缓解高流量的冲击

2、创建队列和交换机的方法?

3、多个消费者监听一个生产者时,消息如何分发?
4、无法被路由的消息,去了哪里?

​ 当消息无法被路由时,会返回给生产者,可以通过设置returnListener进行监听,并对消息进行后续的处理

Channel channel = connection.createChannel();
channel.addReturnListener(new ReturnListener() {
    public void handleReturn(int replyCode,
                             String replyText,
                             String exchange,
                             String routingKey,
                             AMQP.BasicProperties properties,
                             byte[] body)
        throws IOException {
        System.out.println("=========监听器收到了无法路由,被返回的消息============");
        System.out.println("replyText:"+replyText);
        System.out.println("exchange:"+exchange);
        System.out.println("routingKey:"+routingKey);
        System.out.println("message:"+new String(body));
    }
});

5、消息在什么时候会变成Dead Letter(死信)?

  • 当消费者拒绝了消息(reject或者nack)且不让重新入队
  • 当设置了私信队列,当消息过期
  • 当设置了私信队列,队列达到最大长度时,先入队的消息会进入私信队列中

6、RabbitMQ如何实现延迟队列?

  • 使用插件
  • 可以使用消息过期时间和私信队列进行

7、如何保证消息的可靠性投递?

  • 生产者可以开启事务模式和消息持久化模式,
  • 消费端可以对消息的确认可以采用手工应答和自动应答

8、如何在服务端和消费端做限流?

  • 服务端不能实现限流

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

  • 消费端可以实现限流

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

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

9、如何保证消息的顺序性?

​ 如果一个消息队列只有一个消费者的话,可以实现

10、RabbitMQ的节点类型?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值