一 消息发布时的权衡
失败确认
在生产者通过channel的basicPublish方法发布消息时,通常有几个参数需要设置,为此我们有必要了解清楚这些参数代表的具体含义及其作用,查看Channel接口,会发现存在3个重载的basicPublish方法
void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException; void basicPublish(String exchange, String routingKey, boolean mandatory, BasicProperties props, byte[] body) throws IOException; void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body) throws IOException;
当mandatory标志位设置为true时,如果exchange根据自身类型和消息routingKey无法找到一个合适的queue存储消息,那么broker会调用basic.return方法将消息返还给生产者;当mandatory设置为false时,出现上述情况broker会直接将消息丢弃;通俗的讲,mandatory标志告诉broker代理服务器至少将消息route到一个队列中,否则就将消息return给发送者;
可以这样认为,开启mandatory是开启故障检测模式。
注意:它只会让RabbitMQ向你通知失败,而不会通知成功。如果消息正确路由到队列,则发布者不会受到任何通知。带来的问题是无法确保发布消息一定是成功的,因为通知失败的消息可能会丢失。
channel.addConfirmListener则用来监听RabbitMQ发回的失败信息。
package cn.enjoyedu.mandatory; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; /** *类说明: */ public class ProducerMandatory { //交换器名字 public final static String EXCHANGE_NAME = "mandatory_test"; public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { /** * 创建连接连接到RabbitMQ */ ConnectionFactory factory = new ConnectionFactory(); // 设置MabbitMQ所在主机ip或者主机名 factory.setHost("127.0.0.1"); // 创建一个连接 Connection connection = factory.newConnection(); // 创建一个信道 Channel channel = connection.createChannel(); // 指定转发 channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); //失败消息通知 channel.addReturnListener(new ReturnListener() { public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException { String message = new String(body); System.out.println("返回的replyText :"+replyText); System.out.println("返回的exchange :"+exchange); System.out.println("返回的routingKey :"+routingKey); System.out.println("返回的message :"+message); } }); String[] severities={"error","info","warning"}; for(int i=0;i<3;i++){ String severity = severities[i%3]; // 发送的消息 String message = "Hello World_"+(i+1) +("_"+System.currentTimeMillis()); //向MQ发送消息 mandatory=true channel.basicPublish(EXCHANGE_NAME,severity,true, null,message.getBytes()); System.out.println("----------------------------------"); System.out.println(" Sent Message: [" + severity +"]:'" + message + "'"); Thread.sleep(200); } // 关闭频道和连接 channel.close(); connection.close(); } }
监听器的小甜点
在信道关闭和连接关闭时,还有两个监听器可以使用
//连接关闭时执行 connection.addShutdownListener(new ShutdownListener() { public void shutdownCompleted(ShutdownSignalException cause) { System.out.println(cause.getMessage()); } }); //信道关闭时执行 channel.addShutdownListener(new ShutdownListener() { public void shutdownCompleted(ShutdownSignalException cause) { System.out.println(cause.getMessage()); } });
事务
事务的实现主要是对信道(Channel)的设置,主要的方法有三个:
- channel.txSelect()声明启动事务模式;
- channel.txComment()提交事务;
- channel.txRollback()回滚事务;
在发送消息之前,需要声明channel为事务模式,提交或者回滚事务即可。
开启事务后,客户端和RabbitMQ之间的通讯交互流程:
- · 客户端发送给服务器Tx.Select(开启事务模式)
- · 服务器端返回Tx.Select-Ok(开启事务模式ok)
- · 推送消息
- · 客户端发送给事务提交Tx.Commit
- · 服务器端返回Tx.Commit-Ok
以上就完成了事务的交互流程,如果其中任意一个环节出现问题,就会抛出IoException移除,这样用户就可以拦截异常进行事务回滚,或决定要不要重复消息。
那么,既然已经有事务了,为何还要使用发送方确认模式呢,原因是因为事务的性能是非常差的。根据相关资料,事务会降低2~10倍的性能。
发送方确认模式
基于事务的性能问题,RabbitMQ团队为我们拿出了更好的方案,即采用发送方确认模式,该模式比事务更轻量,性能影响几乎可以忽略不计。
原理:生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),由这个id在生产者和RabbitMQ之间进行消息的确认。
不可路由的消息,当交换器发现,消息不能路由到任何队列,会进行确认操作,表示收到了消息。如果发送方设置了mandatory模式,则会先调用addReturnListener监听器。
可路由的消息,要等到消息被投递到所有匹配的队列之后,broker会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号。
confirm模式最大的好处在于他可以是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息决定下一步的处理。
Confirm的三种实现方式:
方式一:channel.waitForConfirms()普通发送方确认模式;消息到达交换器,就会返回true。
方式二:channel.waitForConfirmsOrDie()批量确认模式;使用同步方式等所有的消息发送之后才会执行后面代码,只要有一个消息未到达交换器就会抛出IOException异常。
方式三:channel.addConfirmListener()异步监听发送方确认模式;
如何使用,参见代码no-spring模块包cn.enjoyedu. producerconfirm中。
备用交换器
在第一次声明交换器时被指定,用来提供一种预先存在的交换器,如果主交换器无法路由消息,那么消息将被路由到这个新的备用交换器。
如果发布消息时同时设置了mandatory会发生什么?如果主交换器无法路由消息,RabbitMQ并不会通知发布者,因为,向备用交换器发送消息,表示消息已经被路由了。注意,新的备用交换器就是普通的交换器,没有任何特殊的地方。
使用备用交换器,向往常一样,声明Queue和备用交换器,把Queue绑定到备用交换器上。然后在声明主交换器时,通过交换器的参数,alternate-exchange,,将备用交换器设置给主交换器。
建议备用交换器设置为faout类型,Queue绑定时的路由键设置为“#”
package cn.enjoyedu.backupexchange; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeoutException; /** *类说明:存放到延迟队列的元素,对业务数据进行了包装 */ public class BackupExProducer { public final static String EXCHANGE_NAME = "main-exchange"; public final static String BAK_EXCHANGE_NAME = "ae"; public static void main(String[] args) throws IOException, TimeoutException { /** * 创建连接连接到RabbitMQ */ ConnectionFactory factory = new ConnectionFactory(); factory.setHost("127.0.0.1"); // 创建一个连接 Connection connection = factory.newConnection(); // 创建一个信道 Channel channel = connection.createChannel(); // 声明备用交换器 Map<String,Object> argsMap = new HashMap<String,Object>(); argsMap.put("alternate-exchange",BAK_EXCHANGE_NAME); //主交换器 channel.exchangeDeclare(EXCHANGE_NAME,"direct", false,false,argsMap); //备用交换器 channel.exchangeDeclare(BAK_EXCHANGE_NAME,BuiltinExchangeType.FANOUT, true,false,null); //所有日志严重性级别 String[] severities={"error","info","warning"}; for(int i=0;i<3;i++){ //每一次发送一条不同严重性的日志 String severity = severities[i%3]; // 发送的消息 String message = "Hello World_"+(i+1); //参数1:exchange name //参数2:routing key channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes()); System.out.println(" [x] Sent '" + severity +"':'" + message + "'"); } // 关闭频道和连接 channel.close(); connection.close(); } }
二 消息的消费
消息的获得方式
拉取Get
属于一种轮询模型,发送一次get请求,获得一个消息。如果此时RabbitMQ中没有消息,会获得一个表示空的回复。总的来说,这种方式性能比较差,很明显,每获得一条消息,都要和RabbitMQ进行网络通信发出请求。而且对RabbitMQ来说,RabbitMQ无法进行任何优化,因为它永远不知道应用程序何时会发出请求。具体使用,参见代码no-spring模块包cn.enjoyedu.GetMessage中。对我们实现者来说,要在一个循环里,不断去服务器get消息。
推送Consume
属于一种推送模型。注册一个消费者后,RabbitMQ会在消息可用时,自动将消息进行推送给消费者。这种模式我们已经使用过很多次了,
消息的应答
前面说过,消费者收到的每一条消息都必须进行确认。消息确认后,RabbitMQ才会从队列删除这条消息,RabbitMQ不会为未确认的消息设置超时时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开。这么设计的原因是RabbitMQ允许消费者消费一条消息的时间可以很久很久。
自动确认
消费者在声明队列时,可以指定autoAck参数,当autoAck=true时,一旦消费者接收到了消息,就视为自动确认了消息。如果消费者在处理消息的过程中,出了错,就没有什么办法重新处理这条消息,所以我们很多时候,需要在消息处理成功后,再确认消息,这就需要手动确认。
自行手动确认
当autoAck=false时,RabbitMQ会等待消费者显式发回ack信号后才从内存(和磁盘,如果是持久化消息的话)中移去消息。否则,RabbitMQ会在队列中消息被消费后立即删除它。
采用消息确认机制后,只要令autoAck=false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会一直持有消息直到消费者显式调用basicAck为止。
当autoAck=false时,对于RabbitMQ服务器端而言,队列中的消息分成了两部分:一部分是等待投递给消费者的消息;一部分是已经投递给消费者,但是还没有收到消费者ack信号的消息。如果服务器端一直没有收到消费者的ack信号,并且消费此消息的消费者已经断开连接,则服务器端会安排该消息重新进入队列,等待投递给下一个消费者(也可能还是原来的那个消费者)。
如何使用,参见代码no-spring模块包cn.enjoyedu. ackfalse中。
通过运行程序,启动两个消费者A、B,都可以收到消息,但是其中有一个消费者A不会对消息进行确认,当把这个消费者A关闭后,消费者B又会收到本来发送给消费者A的消息。所以我们一般使用手动确认的方法是,将消息的处理放在try/catch语句块中,成功处理了,就给RabbitMQ一个确认应答,如果处理异常了,就在catch中,进行消息的拒绝,如何拒绝,参考《消息的拒绝》章节。
QoS预取模式
在确认消息被接收之前,消费者可以预先要求接收一定数量的消息,在处理完一定数量的消息后,批量进行确认。如果消费者应用程序在确认消息之前崩溃,则所有未确认的消息将被重新发送给其他消费者。所以这里存在着一定程度上的可靠性风险。
这种机制一方面可以实现限速(将消息暂存到RabbitMQ内存中)的作用,一方面可以保证消息确认质量(比如确认了但是处理有异常的情况)。
注意:消费确认模式必须是非自动ACK机制(这个是使用baseQos的前提条件,否则会Qos不生效),然后设置basicQos的值;另外,还可以基于consume和channel的粒度进行设置(global)。
具体使用,参见代码no-spring模块包cn.enjoyedu. qos中。我们可以进行批量确认,也可以进行单条确认。
basicQos方法参数详细解释:
prefetchSize:最多传输的内容的大小的限制,0为不限制,但据说prefetchSize参数,rabbitmq没有实现。
prefetchCount:会告诉RabbitMQ不要同时给一个消费者推送多于N个消息,即一旦有N个消息还没有ack,则该consumer将block掉,直到有消息ack
global:truefalse 是否将上面设置应用于channel,简单点说,就是上面限制是channel级别的还是consumer级别。
如果同时设置channel和消费者,会怎么样?AMQP规范没有解释如果使用不同的全局值多次调用basic.qos会发生什么。 RabbitMQ将此解释为意味着两个预取限制应该彼此独立地强制执行; 消费者只有在未达到未确认消息限制时才会收到新消息。
channel.basicQos(10, false); // Per consumer limit
channel.basicQos(15, true); // Per channel limit
channel.basicConsume("my-queue1", false, consumer1);
channel.basicConsume("my-queue2", false, consumer2);
也就是说,整个通道加起来最多允许15条未确认的消息,每个消费者则最多有10条消息。
消费者中的事务
使用方法和生产者一致
假设消费者模式中使用了事务,并且在消息确认之后进行了事务回滚,会是什么样的结果?
结果分为两种情况:
- autoAck=false手动应对的时候是支持事务的,也就是说即使你已经手动确认了消息已经收到了,但RabbitMQ对消息的确认会等事务的返回结果,再做最终决定是确认消息还是重新放回队列,如果你手动确认之后,又回滚了事务,那么以事务回滚为准,此条消息会重新放回队列;
- autoAck=true如果自动确认为true的情况是不支持事务的,也就是说你即使在收到消息之后在回滚事务也是于事无补的,队列已经把消息移除了。
可靠性和性能的权衡
消息的拒绝
Reject和Nack
消息确认可以让RabbitMQ知道消费者已经接受并处理完消息。但是如果消息本身或者消息的处理过程出现问题怎么办?需要一种机制,通知RabbitMQ,这个消息,我无法处理,请让别的消费者处理。这里就有两种机制,Reject和Nack。
Reject在拒绝消息时,可以使用requeue标识,告诉RabbitMQ是否需要重新发送给别的消费者。不重新发送,一般这个消息就会被RabbitMQ丢弃。Reject一次只能拒绝一条消息。
channel.basicReject(envelope.getDeliveryTag(),false);
Nack则可以一次性拒绝多个消息。这是RabbitMQ对AMQP规范的一个扩展。
// 此处false 代表非批量, true代表是否重写投递(reQueue)
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,true);
死信交换器DLX
RabbitMQ对AMQP规范的一个扩展。被投递消息被拒绝后的一个可选行为,往往用在对问题消息的诊断上。
消息变成死信一般是以下几种情况:
- · 消息被拒绝,并且设置 requeue 参数为 false
- · 消息过期
- · 队列达到最大长度
死信交换器仍然只是一个普通的交换器,创建时并没有特别要求和操作。在创建队列的时候,声明该交换器将用作保存被拒绝的消息即可,相关的参数是x-dead-letter-exchange。
消息生产者 生产者产生了3条消息,分别是error,info,warn,
package cn.enjoyedu.dlx; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.io.IOException; import java.util.concurrent.TimeoutException; /** *@author Mark老师 享学课堂 https://enjoy.ke.qq.com *往期视频咨询芊芊老师 QQ:2130753077 VIP课程咨询 依娜老师 QQ:2470523467 *类说明: */ public class DlxProducer { public final static String EXCHANGE_NAME = "dlx_make"; public static void main(String[] args) throws IOException, TimeoutException { /** * 创建连接连接到MabbitMQ */ ConnectionFactory factory = new ConnectionFactory(); // 设置MabbitMQ所在主机ip或者主机名 factory.setHost("127.0.0.1"); // 创建一个连接 Connection connection = factory.newConnection(); // 创建一个信道 Channel channel = connection.createChannel(); // 指定转发 channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC); /*日志消息级别,作为路由键使用*/ String[] serverities = {"error","info","warning"}; for(int i=0;i<3;i++){ String severity = serverities[i%3]; String msg = "Hellol,RabbitMq"+(i+1); /*发布消息,需要参数:交换器,路由键,其中以日志消息级别为路由键*/ channel.basicPublish(EXCHANGE_NAME,severity,null, msg.getBytes()); System.out.println("Sent "+severity+":"+msg); } // 关闭频道和连接 channel.close(); connection.close(); } }
WillMakeWarnDlxConsumer
package cn.enjoyedu.dlx; import com.rabbitmq.client.*; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeoutException; /** *类说明:普通的消费者,但是自己无法消费的消息,通过路由键将投入死信队列 */ public class WillMakeWarnDlxConsumer { public static void main(String[] argv) throws IOException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("127.0.0.1"); // 打开连接和创建频道,与发送端一样 Connection connection = factory.newConnection(); final Channel channel = connection.createChannel(); channel.exchangeDeclare(DlxProducer.EXCHANGE_NAME, BuiltinExchangeType.TOPIC); /*声明一个队列,并绑定死信交换器*/ String queueName = "dlx_warn_make"; Map<String, Object> arguments = new HashMap<String, Object>(); //死信交换器 arguments.put("x-dead-letter-exchange", DlxProcessWarnConsumer.DLX_EXCHANGE_NAME); //死信路由键,会替换消息原来的路由键 arguments.put("x-dead-letter-routing-key", DlxProcessWarnConsumer.DLX_ROUTE_KEY); channel.queueDeclare(queueName,false,false, false,arguments); /*绑定,将队列和交换器通过路由键进行绑定*/ channel.queueBind(queueName, DlxProducer.EXCHANGE_NAME,"#"); System.out.println("waiting for message........"); /*声明了一个消费者*/ final Consumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String message = new String(body, "UTF-8"); if(envelope.getRoutingKey().equals("error")){ System.out.println("Received[" +envelope.getRoutingKey() +"]"+message); channel.basicAck(envelope.getDeliveryTag(), false); }else{ System.out.println("Will reject[" +envelope.getRoutingKey() +"]"+message); channel.basicReject(envelope.getDeliveryTag(), false); } } }; /*消费者正式开始在指定队列上消费消息*/ channel.basicConsume(queueName,false,consumer); } }
WillMakeDlxConsumer
package cn.enjoyedu.dlx; import com.rabbitmq.client.*; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeoutException; /** *类说明:普通的消费者,但是自己无法消费的消息,将投入死信队列 */ public class WillMakeDlxConsumer { public static void main(String[] argv) throws IOException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("127.0.0.1"); // 打开连接和创建频道,与发送端一样 Connection connection = factory.newConnection(); final Channel channel = connection.createChannel(); channel.exchangeDeclare(DlxProducer.EXCHANGE_NAME, BuiltinExchangeType.TOPIC); /*声明一个队列,并绑定死信交换器*/ String queueName = "dlx_make"; Map<String,Object> args = new HashMap<String,Object>(); args.put("x-dead-letter-exchange", DlxProcessConsumer.DLX_EXCHANGE_NAME); channel.queueDeclare(queueName,false,true, false, args); /*绑定,将队列和交换器通过路由键进行绑定*/ channel.queueBind(queueName, DlxProducer.EXCHANGE_NAME,"#"); System.out.println("waiting for message........"); /*声明了一个消费者*/ final Consumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String message = new String(body, "UTF-8"); if(envelope.getRoutingKey().equals("error")){ System.out.println("Received[" +envelope.getRoutingKey() +"]"+message); channel.basicAck(envelope.getDeliveryTag(), false); }else{ System.out.println("Will reject[" +envelope.getRoutingKey() +"]"+message); channel.basicReject(envelope.getDeliveryTag(), false); } } }; /*消费者正式开始在指定队列上消费消息*/ channel.basicConsume(queueName,false,consumer); } }
两个消费者WillMakeDlxConsumer和WillMakeWarnDlxConsumer都拒绝了两条消息,而投送到死信队列后,可以发现根据投送死信时的路由键,
不同的消费者有些可以接受到消息,有些则不行
和备用交换器的区别
1、备用交换器是主交换器无法路由消息,那么消息将被路由到这个新的备用交换器,而死信交换器则是接收过期或者被拒绝的消息。
2、备用交换器是在声明主交换器时发生联系,而死信交换器则声明队列时发生联系。
自动删除队列
自动删除队列和普通队列在使用上没有什么区别,唯一的区别是,当消费者断开连接时,队列将会被删除。自动删除队列允许的消费者没有限制,也就是说当这个队列上最后一个消费者断开连接才会执行删除。
自动删除队列只需要在声明队列时,设置属性auto-delete标识为true即可。系统声明的随机队列,缺省就是自动删除的。
单消费者队列
普通队列允许的消费者没有限制,多个消费者绑定到多个队列时,RabbitMQ会采用轮询进行投递。如果需要消费者独占队列,在队列创建的时候,设定属性exclusive为true。
自动过期队列
指队列在超过一定时间没使用,会被从RabbitMQ中被删除。什么是没使用?
一定时间内没有Get操作发生
没有Consumer连接在队列上
特别的:就算一直有消息进入队列,也不算队列在被使用。
通过声明队列时,设定x-expires参数即可,单位毫秒。
永久队列
队列的持久性
持久化队列和非持久化队列的区别是,持久化队列会被保存在磁盘中,固定并持久的存储,当Rabbit服务重启后,该队列会保持原来的状态在RabbitMQ中被管理,而非持久化队列不会被保存在磁盘中,Rabbit服务重启后队列就会消失。
非持久化比持久化的优势就是,由于非持久化不需要保存在磁盘中,所以使用速度就比持久化队列快。即是非持久化的性能要高于持久化。而持久化的优点就是会一直存在,不会随服务的重启或服务器的宕机而消失。
在声明队列时,将属性durable设置为“false”,则该队列为非持久化队列,设置成“true”时,该队列就为持久化队列
队列级别消息过期
就是为每个队列设置消息的超时时间。只要给队列设置x-message-ttl 参数,就设定了该队列所有消息的存活时间,时间单位是毫秒。如果声明队列时指定了死信交换器,则过期消息会成为死信消息。
队列保留参数列表
消息的属性
在发送消息时,我们还可以对消息的属性做更细微的控制,比如构建Request-Response模式
package cn.enjoyedu.setmsg; import com.rabbitmq.client.*; import java.io.IOException; import java.util.UUID; import java.util.concurrent.TimeoutException; /** *类说明:消息的属性的控制 消息发送者 */ public class ReplyToProducer { public final static String EXCHANGE_NAME = "replyto"; public static void main(String[] args) throws IOException, TimeoutException { /* 创建连接,连接到RabbitMQ*/ ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost("127.0.0.1"); Connection connection = connectionFactory.newConnection(); /*创建信道*/ Channel channel = connection.createChannel(); /*创建持久化交换器*/ channel.exchangeDeclare(EXCHANGE_NAME,"direct",false); //响应QueueName ,消费者将会把要返回的信息发送到该Queue String responseQueue = channel.queueDeclare().getQueue(); //消息的唯一id String msgId = UUID.randomUUID().toString(); AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder() .replyTo(responseQueue) .messageId(msgId) .build(); /*声明了一个消费者*/ final Consumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String message = new String(body, "UTF-8"); System.out.println("Received["+envelope.getRoutingKey() +"]"+message); } }; /*消费者正式开始在指定队列上消费消息*/ channel.basicConsume(responseQueue,true,consumer); String msg = "Hellol,RabbitMq"; channel.basicPublish(EXCHANGE_NAME,"error", properties, msg.getBytes()); System.out.println("Sent error:"+msg); } }
消费者
package cn.enjoyedu.setmsg; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; /** *类说明:消息的属性的控制 */ public class ReplyToConsumer { public static void main(String[] argv) throws IOException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("127.0.0.1"); // 打开连接和创建频道,与发送端一样 Connection connection = factory.newConnection(); final Channel channel = connection.createChannel(); /*创建交换器*/ channel.exchangeDeclare(ReplyToProducer.EXCHANGE_NAME, "direct",false); /*声明一个队列*/ String queueName = "replyto"; channel.queueDeclare(queueName,false,false, false,null); /*绑定,将队列和交换器通过路由键进行绑定*/ String routekey = "error";/*表示只关注error级别的日志消息*/ channel.queueBind(queueName,ReplyToProducer.EXCHANGE_NAME,routekey); System.out.println("waiting for message........"); /*声明了一个消费者*/ final Consumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String message = new String(body, "UTF-8"); System.out.println("Received["+envelope.getRoutingKey() +"]"+message); AMQP.BasicProperties respProp = new AMQP.BasicProperties.Builder() .replyTo(properties.getReplyTo()) .correlationId(properties.getMessageId()) .build(); //应答 channel.basicPublish("", respProp.getReplyTo() , respProp , ("Hi,"+message).getBytes("UTF-8")); } }; /*消费者正式开始在指定队列上消费消息*/ channel.basicConsume(queueName,true,consumer); } }
甚至可以消息超时时间和死信交换器配合使用,实现延时队列功能
消息存活时间
当队列消息的TTL(过期时间) 和消息TTL都被设置,时间短的TTL设置生效。如果将一个过期消息发送给RabbitMQ,该消息不会路由到任何队列,而是直接丢弃。
为消息设置TTL有一个问题:RabbitMQ只对处于队头的消息判断是否过期(即不会扫描队列),所以,很可能队列中已存在死消息,但是队列并不知情。这会影响队列统计数据的正确性,妨碍队列及时释放资源。
消息的持久化
默认情况下,队列和交换器在服务器重启后都会消失,消息当然也是。将队列和交换器的durable属性设为true,缺省为false,但是消息要持久化还不够,还需要将消息在发布前,将投递模式设置为2(MessageProperties.PERSISTENT_TEXT_PLAIN)。消息要持久化,必须要有持久化的队列、交换器。
package cn.enjoyedu.msgdurable; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.MessageProperties; import java.io.IOException; import java.util.concurrent.TimeoutException; /** *类说明:消息的持久化生产者 */ public class MsgAttrProducer { public final static String EXCHANGE_NAME = "msg_durable"; public static void main(String[] args) throws IOException, TimeoutException { /* 创建连接,连接到RabbitMQ*/ ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost("127.0.0.1"); Connection connection = connectionFactory.newConnection(); /*创建信道*/ Channel channel = connection.createChannel(); /*创建持久化交换器*/ channel.exchangeDeclare(EXCHANGE_NAME,"direct",true); /*日志消息级别,作为路由键使用*/ String[] serverities = {"error","info","warning"}; for(int i=0;i<3;i++){ String severity = serverities[i%3]; String msg = "Hellol,RabbitMq"+(i+1); /*发布持久化消息*/ channel.basicPublish(EXCHANGE_NAME,severity, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes()); } channel.close(); connection.close(); } }
消费者
package cn.enjoyedu.msgdurable; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; /** *类说明:消息持久化的消费者 */ public class MsgAttrConsumer { public static void main(String[] argv) throws IOException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("127.0.0.1"); // 打开连接和创建频道,与发送端一样 Connection connection = factory.newConnection(); final Channel channel = connection.createChannel(); /*创建持久化交换器*/ channel.exchangeDeclare(MsgAttrProducer.EXCHANGE_NAME, "direct",true); /*声明一个持久化队列*/ String queueName = "msgdurable"; channel.queueDeclare(queueName,true,false, false,null); /*绑定,将队列和交换器通过路由键进行绑定*/ String routekey = "error";/*表示只关注error级别的日志消息*/ channel.queueBind(queueName,MsgAttrProducer.EXCHANGE_NAME,routekey); System.out.println("waiting for message........"); /*声明了一个消费者*/ final Consumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String message = new String(body, "UTF-8"); System.out.println("Received["+envelope.getRoutingKey() +"]"+message); } }; /*消费者正式开始在指定队列上消费消息*/ channel.basicConsume(queueName,true,consumer); } }