RabbitMQ核心原理和知识点

从协议出发

 

rabbitmq客户端和服务端网络通信基于AMQP(高级消息队列协议)标准协议,AMQP协议底层通过TCP可靠传输协议进行通信,基于标准协议的好处主要体现在”开发语言无关性”,任何高级开发语言客户端只要遵循协议就能和服务端进行通信并对接服务端开放的API能力,从协议架构上分析,通过协议给rabbitmq带来更大的灵活性和扩展性,同时为rabbitmq的社区发展和软件应用在消息组件的架构选型上拥有更多的优势。

架构模型图

下面是rabbitmq的一个架构图:

里面涉及多个实体模块,下面分别讲解每个实体的概念和作用:

 

发送者和接收者

发送者和接收者属于使用rabbitmq组件的客户应用系统,理论上不属于rabbitmq架构实体范围内,但是作为rabbitmq价值存在核心客户方,这里作为一个整体列出来。只要往rabbitmq发送消息的(basic.publish)应用都属于发送者,相反,只要往rabbitmq接受或拉取消息的(basic.consume或basic.get)应用都属于接收者,任何一个应用即可能是发送者也可能是接收者。

TCP连接(Connection)

客户端和rabbitmq服务端最底层都是基于TCP可靠传输协议进行数据交互,一般来说客户端应用只启动一条TCP连接作为基础连接。

AMQP Channel

AMQP channel是rabbitmq协议定义的轻量级逻辑连接(信道),为了节约资源,AMQP协议允许一个客户端应用只需要创建一条TCP连接,然后在TCP连接基础上创建多条channel,他们共享一条TCP连接,发送消息和接受消息都是通过channel逻辑连接来完成,当我们完成消息发送或接受时可以关闭channel连接,而不是关闭TCP连接。

Broker Server

一个rabbitmq服务节点就是一个Broker。

鉴权(auth)

任何一个发送者或接收者(为了方便后续统一称为客户端应用)如果在创建TCP连接时会先发送一个心跳给服务端,检查版本、然后将username、password信息包装后一起发送给rabbitmq服务端进行AMQP协议的3次握手(StartOK、Tune),握手的同时会进行账号密码校验,通过后最后校验虚拟机权限等流程,鉴权的作用是通过定义每个账号的虚拟机、交换器、队列等读写权限来保证rabbitmq服务的安全性。

用户(user)

rabbitmq服务端创建的账号和密码信息,账号密码作用是保证rabbitmq服务的安全性以及权限控制。

虚拟机(virtual_host)

rabbitmq broker服务支持多租户,一个虚拟机就是一个租户,虚拟机的作用的隔离不同租户的交换机、队列、用户等资源,它们之间互相不影响。

队列(queue)

队列是接受交换机转发过来的消息并有序入队的一个数据结构,必须先声明(queueDeclare)队列,然后才能使用它,声明队列将导致它创建(如果不存在),如果队列已经存在并且其属性与声明中的相同,则该声明无效,当现有队列属性与声明中的属性不同时,将引发代码为406(PRECONDITION_FAILED)异常,下面是java客户端创建队列的示例代码:

//arg1:队列名称,arg2:是否持久性队列,arg3:是否排他,arg4:是否自动删除,arg5:队列属性参数
channel.queueDeclare("hello", false, false, false, null);

然后可以通过rabbitmq服务端rabbitmqctl命令查看队列信息:

rabbitmqctl -p vhost list_queues

 

绑定(bind)

队列可以通过某个绑定键(band key)绑定到某个交换机上,这种行为我们称为绑定,可以通过客户端代码进行队列和交换机的绑定,java客户端示例代码如下:

channel.queueBind("queue_name", "exchange_name", "bind_key");

然后可以通过rabbitmq服务端rabbitmqctl命令查看绑定信息:

 

rabbitmqctl -p vhost list_bindings

交换机(exchange)

前面已经提过交换机,它的作用是路由转发消息给绑定在当前交换机上的队列上,可以通过客户的创建交换机,java客户端创建交换机的示例代码如下:

//arg1:交换机名称 arg2:交换机类型
channel.exchangeDeclare("exchange_name", "direct");

然后可以通过rabbitmq服务端rabbitmqctl命令查看交换机信息:

rabbitmqctl list_exchanges

当发送者发送一条消息到rabbitmq服务时需要携带路由键(router key)和交换机名称(如果为空则表示默认的交换机 amq.direct)信息,rabbitmq服务根据交换机名称找到对应虚拟机下的交换机,然后根据交换机不同类型来确定不同路由逻辑找到对应绑定在当前交换机下的所有队列,示例代码如下:

channel.basicPublish("exchange_name","router_key" , null, message.getBytes());

交互流程图如下:

队列需要先通过某个绑定键(bind key)绑定到交换机上,然后交换机根据交换机自身的类型来决定如何匹配(例如是完全匹配还是正则匹配或是无需匹配等)路由键(router key)和绑定键(bind key)来转发给绑定的队列上。

交换机类型

交换机类型目前有4种,分别如下:

 

交换机类型

对应rabbitmq默认创建的名称

Direct exchange(直接/默认交换)

(Empty string) and amq.direct

Fanout exchange(扇出交换)

amq.fanout

Topic exchange(主题交换)

amq.topic

Headers exchange(标头交换)

amq.match (and amq.headers in RabbitMQ)

  •  Direct exchange(直接/默认交换):直接交换也是默认的交换机类型,直接交换机背后的路由算法很简单,即:将消息转发给bind key和router key完全匹配一致的所有队列上,如果某条消息没有找到匹配的队列,则该消息会被丢弃(mandatory=false)或返回给发送者(mandatory=true),示例图如下:

上图表示队列Q1通过绑定key为orange绑定到交换机X,队列Q2通过绑定key为black和green绑定到交换机X,当发送者应用P发送一条消息到交换机X后,如果消息携带路由key为black或green时则消息会进入Q2队列,如果携带路由key为grange时则消息会进入Q1队列。除了一个队列可以绑定多个bind key外(例如Q2的black、green),还可以多个队列绑定相同的key,如下图:

 

队列Q1和队列Q2都通过绑定键black到交换机X,当发送者应用P发送一条消息到交换机X后,如果消息携带路由key为black时则消息会同时转发到Q1和Q2队列(类似广播),如果没有找到匹配的队列时,则该消息会被丢弃(mandatory=false)或返回给发送者(mandatory=true)。

  • Fanout exchange(扇出交换):扇出交换类似发布/订阅的模式,当消息发送到扇出类型的交换机上时,该交换机会把消息转发给所有绑定在该交换机上的队列上,也就是说这种类型的交换机不关心绑定键和路由键,如下图:

 

  • Topic exchange(主题交换):主题类型的交换机允许使用多个条件进行绑定键和路由键的匹配,多个条件值必须是单词,且通过点(.)连接起来,例如如下的绑定键示例:

 

  1. 绑定键为:info.error 表示将完整匹配info或error或info.error路由键。

  2. 绑定键为:zhangsan.lisi 表示将完整匹配zhangsan或lisi或zhangsan.lisi路由键。

除了完整匹配外主题类型交换机还支持简单的正则匹配,目前只支持两个正则符号:

 

  1. *:表示一个单词

  2. #:表示一个或多个单词

 

正则绑定键示例如下:

 

  1. 绑定键为:* 表示可以匹配任何单词路由键

  2. 绑定键为:# 表示可以匹配任何单词或多个单词组成的路由键

  3. 绑定键为:info.* 表示可以匹配info.error或info.aaa或info.abcdef等路由键

  4. 绑定键为:info.*.* 表示可以匹配info.aa.bb或info.aaa.eee或info.fff.ccc等路由键

  5. 绑定键为:info.# 表示可以匹配info.a或info.a.b.c.e等路由键

 

如下图:

队列Q1绑定的路由键为*.orange.*,队列Q2绑定的路由键为*.*.rabbit和lazy.#,当消息路由键为以下情况时,转发情况如下:

 

  1. 当消息路由键为:a.orange.rabbit时,消息会转发给Q1和Q2队列。

  2. 当消息路由键为:a.orange.b时,消息会转发给Q1队列。

  3. 当消息路由键为:lazy.a.b.c时,消息会转发给Q2队列。

  4. 当消息路由键为:lazy.a.rabbit时,消息会转发给Q2队列一次,虽然两个绑定键都匹配。

  5. 当消息路由键为:a.b.c时,消息会被丢弃,因为找不到匹配的队列。

  • Headers exchange(标头交换): 标头交换忽略路由键属性,相反,用于路由的属性取自消息的headers属性,如果header的键值对的值等于绑定时指定的一个或多个(由x-match模式指定是否完全匹配还是匹配任意一个)键值对。路由消息时可以携带x-match属性,当“ x-match”参数设置为“ any”时,仅匹配一个键值对就可以了,或者,将“ x-match”设置为“ all”要求所有键值对必须匹配,最后请注意,以字符串x-开头的标头键是不合法的,将不会起作用。

默认交换机

在安装启动rabbitmq服务后,由rabbitmq服务自动创建一个名称为“/”的虚拟机,然后为每个交换类型创建一个以amq.开头默认交换机(例如amq.direct),这些默认交换机默认归属于“/”虚拟机,其中没有名称的交换机为 amq.direct,任何创建后没有明确声明绑定交换机的队列默认通过队列名字作为绑定键绑定到默认的 amq.direct交换机上,这就是为什么客户端在发送消息时携带交换机名称为空字符串,且路由键为队列名称也能够发送消息的原因。示例代码如下: 

// arg1:交换机名称为空,arg2:路由键为队列名称
channel.basicPublish("", "hello_queue", null, message.getBytes());


交换机属性

下面罗列交换机一些核心属性,了解这些属性对使用rabbitmq提供更多的准备:

 

  • 交换类型:定义交换机路由算法,具体前面已经介绍过。

  • 名称(name):定义交换机的名称,名称为空的为默认的交换机amq.direct,默认交换机在安装启动rabbitmq服务后由服务端自动创建。

  • 持久性(durable):非持久类型的交换机在broker服务重新启动后将被删除,标记为持久性类型对的交换机重新启动后仍然存在。

  • 自动删除(auth-delete):当取消最后一个队列的绑定时,将删除该交换机。

  • 参数(args):可选,由插件或其它broker功能使用。

 

队列属性

下面罗列队列的属性:

  • 名称(name):队列名称,应用程序可以选择队列名称,也可以要求broker为其创建名称,队列名称最多可以包含255个字节的UTF-8字符,当传递一个空字符串作为队列名称参数时,broker服务将为其创建一个随机唯一的名称,并将生成的名称随响应一起返回给客户端,另外队列名称不能以“ amq”开头,这个开头的为broker服务内部使用。

  • 持久(durable): 队列在broker服务启动后是否仍然存在。

  • 独占(exclusive):仅由一个连接使用,并且该连接关闭时队列将被删除。

  • 自动删除(auto-delete):当最后一个消费者取消订阅时,该队列将被删除。

  • 参数(args):可选,由插件或其它broker功能使用。

 

消费消息模式

消费者可以通过两种方式消费某个队列的消息:

 

  • 推:推的方式应用程序必须订阅某个队列,这样当某个队列由消息时会按消息序号有序地、轮询地方式推送给每一个订阅者,订阅代码如下:

channel.basicConsume(.....)
  • 拉:当某个应用程序需要某个队列的消息时,可以在需要的时间点调用下面拉接口进行获取消息:

channel.basicGet(.....)


消费者负载均衡

当多个应用程序实例订阅了同一个的队列后,当队列有消息时,默认按照轮询的方式将消息有序的轮询给下一个消费者,假如有A、B两个应用消费者同时订阅了队列Q,当队列有10条消息时,轮询结果可能是A应用收到5条(1、3、5、7、9),B应用收到5条(2、4、6、8、10),这种类似Nginx的负载均衡,把多个消息负载到多个下游应用上。默认轮询的负载均衡可能存在问题,比如前面A应用的5条消息可能需要很长的时间去处理,而B应用的5条消息恰好只需要很短时间,这样就会导致A应用忙死,而B应用闲得慌,这种情况可以通过在消费应用订阅前配置Qos=1的方式来解决,java客户端示例代码如下:

一次仅接受一条未经确认的消息


channel.basicQos(1);


...省略回调代码


boolean autoAck = false;


channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });

basicQos(1)这行代码的作用是告诉队列当我还没处理完消息时(还没回复消息确认ack),不要给我下发新的消息,而是将其分派给尚不繁忙的下一个消费者,这样就可以避免某个消费者很忙时,队列还继续轮询方式下发消息。

消费者消息确认

可能会出现这样的问题:当某条消息M轮询或正常下发到A应用时,A应用还没处理完就因为发版被重启了导致这条消息M没有得到真正的处理,而rabbitmq一旦向消费者发送了一条消息,便立即将其标记为删除,这种情况我们将丢失这条M消息,消息确认机制可以解决这个问题,首先消费者在订阅队列时需要将autoAck设置为false(关闭自动确认)在消息正确被处理后再手动确认,这样rabbitmq服务如果发现某个订阅的消费者为需要手动确认的情况下,就不会将消息发送给消费者后自动标记消息为删除而是等到消费者回复确认后才标记为删除,如果下发后消费者一直没有确认(其channel已关闭,连接已关闭或TCP连接丢失,通过心跳机制可以识别消费者挂掉了),那么这条消息将会重新排队(消息重新排队后,将尽可能放置在其队列中的原始位置),重新排队会导致下次被再次消费处理,这里我们又必须思考另外一个问题就是重新处理相同消息的情况,比如某消费者已经成功处理某条消息M,刚好处理完成准备回复ack时系统故障,此时这条消息实际已经处理,但是却被认为没有收到ack而再次发送到下一个消费者再次被消费,这时我们的应用代码必须考虑幂等性的设计,重新发送时将具有特殊的布尔属性redeliver, 由RabbitMQ 设置为true。对于第一次发送给消费者,它将设置为false,我们可以充分借助这个属性进行幂等性处理。

下面是手动确认java客户端示例代码:

//消费消息回调函数


DeliverCallback deliverCallback = (consumerTag, delivery) -> {


  String message = new String(delivery.getBody(), "UTF-8");


  System.out.println(" [x] Received '" + message + "'");


  try {


    //处理消息


    doWork(message);


  } finally {


System.out.println(" [x] Done");


// 手动确认,消息被标记为删除


    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);


  }


};


//关闭自动确认


boolean autoAck = false;


channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });

消费者确认是可靠处理消息核心步骤之一,另外手动确认看起来很完美,但是如果由于程序问题导致大量消息没有被确认,这样对rabbitmq的内存挑战很大,这种情况需要通过下面命令进行排查:

rabbitmqctl list_queues queue_name messages_ready messages_unacknowledged

 

消息持久性

前面我们知道如何保证当某个消费者应用程序宕机不会丢失消息,但是如果rabbitmq服务被重启,rabbitmq还未被消费的消息仍然会丢失,甚至连队列也会被清除(durable=false),因为默认消息是保存在内存中的,为了保证消息持久性我们需要做下面两件事:

 

  • 把对于的队列配置为持久性:java客户端示例代码如下:

//配置队列为持久性


boolean durable = true;


channel.queueDeclare("hello", durable, false, false, null);
  • 发送者发送消息时,把消息配置为持久性:java客户端示例代码如下:

import com.rabbitmq.client.MessageProperties;


channel.basicPublish("", "task_queue",


            //配置为持久性


            MessageProperties.PERSISTENT_TEXT_PLAIN,


            message.getBytes());

这样消息基本具备持久性的能力,但是也可能存在小概率的丢失消息,因为rabbitmq不会对每条消息都指向fsyc(2)落盘操作,任何一条消息都有可能在很短时间内是保存在缓存中的,由后台线程定时(几百毫秒)批量刷新到磁盘,但是这对于大多数消息来说安全性已经绰绰有余,如果要确保很强的消息持久性,那么需要通过下面的发送者确认机制来真正确保消息成功在rabbitmq服务落盘。

关于消息持久性有几点需要牢记:

l 队列持久性不代表消息被持久性;

l 消息持久性必须保证队列是持久性的;

发送者消息确认

在channel上启动发送者确认是实现可靠消息发送的核心步骤之一,下面是启动chanel发送者确认代码示例:

Channel channel = connection.createChannel();


channel.confirmSelect();

请注意:必须在希望启动发送者确认的每个channel上调用此方法且仅应启用一次,而不是对每次发送消息都掉一次启用方法。如果在channel启动发送者确认后,rabbitmq服务将以异步回调的方式通知发送端确认结果,但是客户端这边可以是同步等待和处理结果也可以是异步等待和处理结果,看业务场景,但是如果是需要高并发的场景,必须通过异步等待和处理结果才是正确的。下面演示发送者客户端同步等待结果示例代码:

byte[] body = ...;


BasicProperties properties = ...;


channel.basicPublish(exchange, queue, properties, body);


try {


// uses a 5 second timeout


channel.waitForConfirmsOrDie(5000);


}catch(Exception e){


 //处理异常


}

如果未在超时时间内(5s)确认该消息或该消息没有被确认(这意味着rabbitmq服务出于某种原因无法处理该消息),则该方法将引发异常。异常的处理由发送者自己定义,通常包括记录错误消息和/或重试发送消息。

 

下面是异步处理确认的示例代码:

//确认回调
ConfirmCallback ackCallback = new ConfirmCallback() {
    public void handle(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("消息被成功确认");
    }
};
//否认回调,可以认为是丢失的消息
ConfirmCallback nackCallback = new ConfirmCallback() {
    public void handle(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("消息被否认");
    }
};
channel.addConfirmListener(ackCallback, nackCallback);

异步回调有个细节需要发送者客户端应用妥善处理,就是如何知道回调确认的消息是前面发送的哪一条,换句话说就是发送的消息和确认的消息如何关联起来,这里需要客户端增加一些开发量,大概思路如下:

首先在发布消息时需要通过下面的代码获取消息序列号/编号:

int sequenceNumber = channel.getNextPublishSeqNo());

然后这个序列号需要客户端应用自己保存跟业务数据的关联关系,在上面确认的回调接口handle方法的第一个参数long deliveryTag就是消息的序列号。但是我们还需要关注第二个参数boolean multiple,这个参数如果为false,那么就是rabbitmq服务端只确认了一条消息,如果为true,则表示rabbitmq服务端确认了多条消息,多条消息逻辑为: 序列号小于或等于deliveryTag的所有消息都被确认了,如果你是把消息存入本地DB做映射的话,回调处理伪代码可能像下面这样:

ConfirmCallback ackCallback = new ConfirmCallback() {
    public void handle(long deliveryTag, boolean multiple) throws IOException {
        String querySql = "";
        if (multiple){
            querySql = "select * from msg_map_table t where t.msg_no <= :deliveryTag";
        }else {
            querySql = "select * from msg_map_table t where t.msg_no = :deliveryTag";
        }
        List<MsgEntity> list = sqlSession.excetion(querySql, deliveryTag);
        for (int i = 0; i < list.size(); i++) {
            MsgEntity msgEntity = list.get(i);
            //doHandler...
        }
        String delSql = "";
        if (multiple){
            delSql = "delete from msg_map_table t where t.msg_no <= :deliveryTag";
        }else {
            delSql = "delete from msg_map_table t where t.msg_no = :deliveryTag";
        }
        sqlSession.excetion(delSql, deliveryTag);
    }
};

最后注意:对于rabbitmq服务没有确认的消息也会调用回调(nackCallback),如果需要重新发送不建议直接在回调方法区处理,推荐的方式是保存到另外的内存队列或DB中由单独的线程去轮询重新发送。

 

拒绝处理

前面我们讲解了消费者手动确认消息和发送者回调确认消息功能,接下来我们思考这样的场景,某个消费者应用A对某条消息由于数据格式错误导致处理失败,这种情况下可以发送一个拒绝信息给rabbitmq服务,拒绝时可以告诉rabbitmq服务删除该消息,如果该队列设置了死信队列则被删除的消息会被路由到死信队列,然后可以单独消费者消费这个死信队列,分析拒绝原因,也可以告诉rabbitmq服务继续重新排序这条消息,示例代码如下:

channel.basicConsume(queueName, autoAck, "a-consumer-tag",


     new DefaultConsumer(channel) {


         @Override


         public void handleDelivery(String consumerTag,


                                    Envelope envelope,


                                    AMQP.BasicProperties properties,


                                    byte[] body)


             throws IOException
{


             long deliveryTag = envelope.getDeliveryTag();


             //拒绝,并希望rabbitmq服务删除消息


             channel.basicReject(deliveryTag, true);


         }


     });


 


channel.basicConsume(queueName, autoAck, "a-consumer-tag",


     new DefaultConsumer(channel) {


         @Override


         public void handleDelivery(String consumerTag,


                                    Envelope envelope,


                                    AMQP.BasicProperties properties,


                                    byte[] body)


             throws IOException
{


             long deliveryTag = envelope.getDeliveryTag();


             //拒绝,并希望该消息重新排队


channel.basicReject(deliveryTag,true);


         }


     });

事务

这里只做一个简单介绍,建议避免使用事务,而改用发送者确认的方式来替代事务,事务事阻塞的,且会要求rabbitmq服务多次来回网络发送消息进行交互,很吃rabbitmq服务的性能。示例代码如下:

//开始事务
channel.txSelect();
try {
    //发送消息
    channel.basicPublish("hello_exchange", "hello_queue", null, message.getBytes());
    //提交事务
    channel.txCommit();
}catch (Exception ex){
    ex.printStackTrace();
    //回滚事务
    channel.txRollback();
}

队列长度限制

可以通过设置一定的消息数量或字节数的方式控制队列的长度,或者两种方式同时设置。可以通过客户端使用队列的参数来定义,或者在rabbitmq通过policy来控制,如果客户端和rabbitmq服务同时设置了长度的情况下将应用两个值的最小值。如果队列被设置了最大的长度后,消息达到了最大长度时rabbitmq服务默认是丢弃(或路由到死信队列,如果有设置的话)最早的消息,也就是队列头开始丢弃消息,当然你也可以配置为丢弃最新的消息,具体配置方式如下:

1、通过rabbitmq服务的rabbitmqctl命令配置方式如下:

rabbitmqctl set_policy my-pol "^one-meg$" \


  '{"max-length-bytes":1048576}' \


  --apply-to queues

上面命令表示创建一个名称为my-pol的策略,它定义了one-meg队列的最大字节为1048576,大约1M,当超过1M时,会丢失最早的消息。

 

2、通过rabbitmq服务的rabbitmqctl命令自定义丢失最新的消息,

rabbitmqctl set_policy my-pol "^two-messages$" \


  '{"max-length":2,"overflow":"reject-publish"}' \


  --apply-to queues

上面命令表示创建一个名称为my-pol的策略,它定义了two-meg队列的最多接收2条消息当超过2条消息时,会丢失拒绝新的发送者发送的消息,如果发送者监听了确认回调,那么此时rabbitmq服务会回调一个拒绝的通知给到发送者。

 

3、通过客户端创建队列的方式定义长度代码示例如下:

Map<String, Object> args = new HashMap<String, Object>();


args.put("x-max-length", 10);


channel.queueDeclare("myqueue", false, false, false, args);

表示该队列最多接收10条消息。

注意,队列长度设置虽然可以控制资源(内存/磁盘)的使用,但是不是所以场景的消息都可以随便设置长度的,因为会导致消息丢失,一般适用对于一些无关紧要的类似系统日志消息。


消息生存时间(TTL)

类似redis,我们可以通过设置队列或单条消息的过期时间ttl来控制消息的过期时间,如果设置某个队列TTL超时时间后,只要是发送到该队列的消息的有效期都按队列设置有效期的为准,当队列和某条消息同时被设置了ttl时,以两者最小值为准。凡是超过设置TTL的消息,rabbitmq服务将不会推给消费者或消费者拉取时也不会返回给消费者,一般超过TTL的消息将会在一段时间后被删除或重新路由到死信队列(如果有设置的话)。和长度限制一样,可以通过rabbitmqctl命令或者客户端创建队列时通过可选参数进行设置,示例如下:

 

1、通过rabbitmqctl命令设置如下:

rabbitmqctl set_policy TTL ".*" '{"message-ttl":60000}' --apply-to queues

表示将所有的队列设置TTL有效期为60s,一般来说单位是ms,60000ms = 60s;ttl的值必须大于等于0,如果为0表示到达队列时立即过期。

 

2、通过客户端创建队列时进行设置示例如下:

 

Map<String, Object> args = new HashMap<String, Object>();


args.put("x-message-ttl", 60000);


channel.queueDeclare("myqueue", false, false, false, args);

3、在发送消息时设置某条消息的ttl:

byte[] messageBodyBytes = "Hello, world!".getBytes();


AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()


                                   .expiration("60000")


                                   .build();


channel.basicPublish("my-exchange", "routing-key", properties, messageBodyBytes);


队列TTL

队列本身也支持设置ttl,当队列不再使用时,例如最后订阅者取消订阅时,该队列经ttl的空闲时间后自动删除,这个一般可以和自动删除属性一起使用。

1、rabbitmqctl命令方式为所有队列(.*)设置ttl如下:

rabbitmqctl set_policy expiry ".*" '{"expires":1800000}' --apply-to queues

2、客户端方式为myqueue队列设置ttl如下:

Map<String, Object> args = new HashMap<String, Object>();


args.put("x-expires", 1800000);


channel.queueDeclare("myqueue", false, false, false, args);

均表示队列在没有任何消费者订阅时30分钟后过期。

死信交换(DLX)

死信交换器或死信队列,当发生以下情况时,消息将被路由到死信交换器:

1、消息被消费者拒绝(basic.reject/basic.nack)并且将requeue参数设置为false。

2、消息由于ttl而到期。

3、由于队列长度限制到时消息被删除。

 

下面是通过rabbitmqct命令创建死信交换:

rabbitmqctl set_policy DLX "hello" '{"dead-letter-exchange":"my-dlx", "dead-letter-routing-key":"router_key"}' --apply-to queues

表示将hello队列通过router_key路由键绑定到名称为my-dlx的死信交换器中,my-dlx可以像创建其它普通类型交换机一样创建它,如果没有指定路由键则以hello队形正常的路由键相同,比如默认的以队列名称。

 

下面是通过客户端创建死信交换:

channel.exchangeDeclare("exchange.name", "direct");


Map<String, Object> args = new HashMap<String, Object>();


args.put("x-dead-letter-exchange", "exchange.name");


channel.queueDeclare("myqueue", false, false, false, args);

上面名称为exchange.name的direct类型的交换器被myqueue队列通过参数x-dead-letter-exchang绑定为死信交换,这样当myqueue队列消息ttl过期或消费者拒绝或超过长度设置将删除消息时,这些将要被删除消息将会被路由到名称为exchange.name的交换器中,路由键默认为消息到达myqueue时的路由键,也可以通过下面参数指定一个路由键。

args.put("x-dead-letter-routing-key", "some-routing-key");

持续关注,后续将继续讲解RabbitMQ集群的搭建、AMQP协议源码分析等文章!

---------------------- 正文结束 ------------------------

长按扫码关注微信公众号

Java软件编程之家

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值