RabbitMQ高级特性
前言:此篇文章包括消息可靠性投递、Consumer ACK、 消费端限流、TTL、死信队列、延迟队列的内容,使用SpringBoot进行代码编写,多种配置方式切换,但是需要看懂还是有不小难度的,我也是不久前归纳整理出来的,如有问题请及时提出。
声明:本文主要使用配置类进行配置,但是代码中也是用了配置文件(注释掉了).
此外对队列交换机的持久化是本文的基础,当然SpringBoot中RabbitMQ默认为他们设置了持久化创建
gitee源码地址:https://gitee.com/XuLiZhao/rabbit-mq-demo.git
消息可靠投递
为保证信息的可靠性,消息的发送者希望避免任何消息丢失或投递失败的场景。RabbitMQ为我们提供了两种方式:confirm确认模式和return回退模式
确认模式
确认模式有三种:
- 自动确认:acknowledge=“none”
- 手动确认:acknowledge=“manual”
- 根据异常情况确认:acknowledge=“auto”(这种模式我还没有用过)
消费者配置(开启确认模式)
@Bean public CachingConnectionFactory connectionFactory(){ CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); connectionFactory.setHost("192.168.187.131"); connectionFactory.setPort(5672); connectionFactory.setUsername("xihai"); connectionFactory.setPassword("123456"); connectionFactory.setVirtualHost("/"); connectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED);//确认模式开启 connectionFactory.setPublisherReturns(true); return connectionFactory; }
生产者
@Test public void testConfirm(){ //设置回调 rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { System.out.println("confirm方法被执行了"); if(ack){ System.out.println("接收消息成功"); }else{ System.out.println("接收消息失败"); } } }); //发送信息 rabbitTemplate.convertAndSend("test.direct","confirm","消息发送了,请确认收到..."); }
运行结果
可以看到交换机接收消息后返回了如下信息,你可以更改交换机exchange模拟未收到消息的情况。
在生产者容器中我定义了如下队列和交换机
@Bean public Queue reliableQueue(){ return new Queue("queue.reliable"); } @Bean public DirectExchange reliableDirectExchange(){ return new DirectExchange("exchange.reliable"); } @Bean public Binding bindingReliable(Queue reliableQueue, DirectExchange reliableDirectExchange){ return BindingBuilder.bind(reliableQueue).to(reliableDirectExchange).with("confirm"); }
回退模式
消费者配置(开启回退模式):判断消息是否从交换机发送到队列中
# yaml文件配置,配置类文件中也有,见源码 spring: rabbitmq: host: 192.168.187.131 # rabbitMQ的ip地址 port: 5672 # 端口 username: xihai password: 123456 virtual-host: / publisher-confirm-type: correlated 开启回退模式 publisher-returns: true template: mandatory: true
消费者
@Test public void testReturn(){ //设置交换机处理消息失败的模式,可以在配置文件中进行配置 rabbitTemplate.setMandatory(true); rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() { @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { System.out.println("回退模式执行了"); System.out.println(message); System.out.println(replyCode); System.out.println(replyText); System.out.println(exchange); System.out.println(routingKey); } }); //发送消息,将路由的key值写错 rabbitTemplate.convertAndSend("exchange.reliable","confirm","消息发送了,请确认收到..."); }
结果显示
这里直接将routingKey写错,数据不能到达队列,默认会被丢弃
事务机制
事务机制和确认机制是互斥的,不能共存,而且事务机制比较消耗性能,慎用。
使用之前一定要将之前设置的确认模式关闭,不然会报错
//事务型消息的发送 @Test public void testTranslation(){ Connection connection = connectionFactory.createConnection(); //创建一个通道并且设置其支持事务,当参数为true时自动提交,当参数为false时手动提交 Channel channel = connection.createChannel(false); try { //创建队列和交换机并绑定 channel.queueDeclare("queue.translation", true, false, false, null); channel.exchangeDeclare("exchange.translation", BuiltinExchangeType.DIRECT,true); channel.queueBind("queue.translation","exchange.translation","translation"); channel.txSelect();//开启事务 //发送消息 channel.basicPublish("exchange.translation","translation",null,"事务消息发送1".getBytes()); int a = 10/0; channel.basicPublish("exchange.translation","translation",null,"事务消息发送2".getBytes()); channel.txCommit();//提交事务 } catch (IOException e) { e.printStackTrace(); try { channel.txRollback();//回滚事务 } catch (IOException ioException) { ioException.printStackTrace(); } }finally { try { channel.close(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }
Consumer ACK
ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。
在实际业务中,下次可能发生丢失,而业务已经处理好了,这就有可能发生错误,所以使用ack机制就可以在业务调用成功后手动签收,一旦出现异常,拒绝签收并提示消息重新发送。
@Bean public SimpleMessageListenerContainer simpleMessageListenerContainer(){ SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory()); container.setConnectionFactory(connectionFactory()); //配置监听队列,可以设置多个,为了不与注解类监听器冲突我只设置这一个 //container.setQueueNames("simple.queue","ttl.queue","queue.reliable"); container.setQueueNames("ttl.queue"); //设置多个并发消费者的消费 //container.setConcurrentConsumers(4); //设置允许消费者的最大数量 container.setMaxConcurrentConsumers(10); //设置消息监听,使用MessageListener的子接口ChannelAwareMessageListener,以便使用channel的方法 container.setMessageListener(new ChannelAwareMessageListener() { @Override public void onMessage(Message message, Channel channel) throws Exception { Thread.sleep(1000); //方便消息拉取时更容易看到效果 long deliveryTag = message.getMessageProperties().getDeliveryTag(); try { //1.接收转换消息 System.out.println(new String(message.getBody())); //2.处理业务逻辑 System.out.println("处理业务逻辑..."); //int a = 2/0; //出现错误,手动签收不执行 //3.手动签收 channel.basicAck(deliveryTag,true); } catch (Exception e) { //4.拒绝签收。第三个参数表示的值是是否重回队列,当设置为true时,broker会重新发送消息给消费端 channel.basicNack(deliveryTag,true,true); //对单条消息的处理,basicNack方法比此条方法多一个参数,也就是中间那个布尔值,表示是否处理多条数据 //channel.basicReject(deliveryTag,true); } } }); //设置手动签收 container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //限流机制,我上面设置了最大四个消费者,所以这边处理的消息会有四条 container.setPrefetchCount(1); // 默认采用下面的这种转换器 // container.setMessageConverter(new SimpleMessageConverter()); //使用JSON进行序列化和反序列化 return container; }
我这边使用了监听器容器的方式最信息进行ack处理,你也可以单独设置监听器并制定需要监听的队列。做整件事的基础是你已经在SpringBoot与RabbitMQ的连接工厂中开启了确认模式,并且在消息监听勇气中设置确认模式为手动签收,而不是自动签收。
需要注意的是这边如果要打印数据Message,那么你会看到一段你看不懂的字符。所以要设置序列化。关于序列化我在前一篇文章中讲解过,这里提供另一种方法。为监听器工厂设置。
@Bean public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(CachingConnectionFactory connectionFactory){ SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); //设置JSON序列化与反序列化 factory.setMessageConverter(new Jackson2JsonMessageConverter()); return factory; }
消费端限流
队列的方式存储请求,通过队列的先进后出机制,控制消费端每次获取的最大消息数量实现限流处理
代码在前面已经给出了,就在那个ACK里面,你需要配置的就是这一段
container.setPrefetchCount(1);
,另外,当时我配置类每次并发有4个消费者,所以当时取得是四个值。为了使得实验效果更明显,这边使用了自定义异常并设置为手动确认模式。你可以进行模拟。生产端代码:/** * Description: * 发送消息 * **/ @Test public void testSend(){ for (int i = 0; i < 10; i++) { rabbitTemplate.convertAndSend("exchange.reliable","confirm","消息发送了,请确认收到..." + i); } }
我这边将手动确认代码注释掉了,这样就每个消费者就只会接收你预收的最多消息,在消息没有进行确认时不会接收更多消息。而且我这边将最大接收四条消息的配置打开了,所以会有四条消息出现。运行结果如图:
TTL
TTL表示存活/过期时间,当消息到达过期时间后会被清除,可以对单条消息设置过期时间,也可以为队列设置过期时间。
/** * Description: * TTL:过期时间 * 1.队列的统一过期 * 给队列设置属性:queue.addArgument("x-message-ttl",10000); * 也可以在构造函数中指定 * 2.消息的单独过期 * 给消息设置单独的过期时间 * * 需要注意:1.消息过期与队列过期同时存在时,以短时间为准 * 2.队列过期后,队列中所有消息都将清除 * 3.消息过期后,只有这条消息到达顶端才会进行判断(是否移除) **/ @Test public void testTTL(){ //创建队列 Queue queue = new Queue("ttl.queue", true); //设置队列的属性,当前设置消息为10秒后过期 queue.addArgument("x-message-ttl",50000); rabbitAdmin.declareQueue(queue); //创建交换机并绑定 TopicExchange topicExchange = new TopicExchange("ttl.topic"); rabbitAdmin.declareExchange(topicExchange); rabbitAdmin.declareBinding(new Binding("ttl.queue", Binding.DestinationType.QUEUE,"ttl.topic","ttl.#",null)); //创建公共对象 MessagePostProcessor messagePostProcessor = new MessagePostProcessor() { /*MessagePostProcessor为发送时使用到的消息处理器,即消息后处理对象 用来设置一些消息的参数信息 */ @Override public Message postProcessMessage(Message message) throws AmqpException { //1.设置message的信息 message.getMessageProperties().setExpiration("5000");//设置消息过期时间 //2.返回该消息 return message; } }; //队列的统一过期发送消息设置 for (int i = 0; i < 10; i++) { rabbitTemplate.convertAndSend("ttl.topic","ttl.hello","第【" + i + "】条ttl消息发送了"); } //消息单独过期发送消息设置 rabbitTemplate.convertAndSend("ttl.topic", "ttl.hello", "ttl消息发送了",messagePostProcessor); //过期与不过期消息掺杂进队列,查看顶端为未过期消息时,队列中的过期消息是否被删除 for (int i = 0; i < 10; i++) { if (i == 5){ //消息单独过期,为队列中部 rabbitTemplate.convertAndSend("ttl.topic","ttl.hello","第【" + i + "】条消息为ttl类型消息发送了",messagePostProcessor); }else { //消息不过期 rabbitTemplate.convertAndSend("ttl.topic","ttl.hello","第【" + i + "】条消息发送了"); } } }
我这边使用了一种新的方式创建队列和交换机,即RabbitAdmin。在前一篇中我讲解了三种种创建队列和交换机的方式,两种是通过Spring容器创建,包括@Bean注解和@RabbitListener注解,第三种是连接工厂的Channel创建,不理解的可以去看看。这边又是一种新的方式,需要在容器中注入以下配置。
/** * Description: * RabbitAdmin可以帮助我们声明交换机,队列,绑定,并可以完成一般性的工作。 * 现在spring容器默认自动加载这个类 * **/ @Bean public RabbitAdmin rabbitAdmin(){ RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory()); rabbitAdmin.setAutoStartup(true); return rabbitAdmin; }
另外,关于MessagePostProcessor为消息类配置的一些属性我列举给大家
/** * Description: * 补充: * 在Queue中Argument参数中可以添加的map参数有如下一些,你可以进入控制台,新建队列时点击Arguments下提供的各种属性查看 * (1)x-message-ttl:消息的过期时间,单位:毫秒; * (2)x-expires:队列过期时间,队列在多长时间未被访问将被删除,单位:毫秒; * (3)x-max-length:队列最大长度,超过该最大值,则将从队列头部开始删除消息; * (4)x-max-length-bytes:队列消息内容占用最大空间,受限于内存大小,超过该阈值则从队列头部开始删除消息; * (5)x-overflow:设置队列溢出行为。这决定了当达到队列的最大长度时消息会发生什么。有效值是drop-head、reject-publish或reject-publish-dlx。仲裁队列类型仅支持drop-head; * (6)x-dead-letter-exchange:死信交换器名称,过期或被删除(因队列长度超长或因空间超出阈值)的消息可指定发送到该交换器中; * (7)x-dead-letter-routing-key:死信消息路由键,在消息发送到死信交换器时会使用该路由键,如果不设置,则使用消息的原来的路由键值 * (8)x-single-active-consumer:表示队列是否是单一活动消费者,true时,注册的消费组内只有一个消费者消费消息,其他被忽略,false时消息循环分发给所有消费者(默认false) * (9)x-max-priority:队列要支持的最大优先级数;如果未设置,队列将不支持消息优先级; * (10)x-queue-mode(Lazy mode):将队列设置为延迟模式,在磁盘上保留尽可能多的消息,以减少RAM的使用;如果未设置,队列将保留内存缓存以尽可能快地传递消息; * (11)x-queue-master-locator:在集群模式下设置镜像队列的主节点信息。 **/
过期时间的配置:设置队列过期就是在队列创建时添加指定属性。为消息设置过期就是为消息设置
MessagePostProcessor
对象,他是一个消息的处理器,具体配置过程如上。这里效果不好演示,你运行后过你定义时间后,队列中的信息自动消失。当然由于队列是持久化的,队列并不会被删除,并且也可以向它里面发送消息。需要注意,如果你在先前队列还没有过期的时候,再次向队列中发送消息,队列中最多只有你设置的每次30条消息,关于这个同一队列为啥连续发送两次数据,消息数目不是简单相加,而是两次消息互不影响的原理我目前也不懂。
队列中数据情况如图
关于队列中有消息过期而队列未过期,且过期消息不是处于首部的情况,判断过期消息是否被删除
通过数据我们可以看到,10秒后消息过期,而我的消费者是在30秒后才启动开启消费消息的,看第一张的消息数据图,我们可以看到一条消息由于过期,所以没有ack确认。所以准备中的数据从之前的10条变成了9条。通过控制台的打印也可以知道,第五条过期消息没有被接收,但是在队列中即使过了20秒,过期消息也没有从队列中删除,而是当这条过期消息要被处理时才去删除的。
死信队列
用来实现消息在未被正常消费的情况下,对这些消息进行处理。当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。
死信队列的创建和配置
//正常交换机 @Bean public Queue simpleQueue(){ Queue queue = new Queue("simple.queue"); //设置死信交换机名称 queue.addArgument("x-dead-letter-exchange","exchange.dlx"); //设置死信交换机的routingKey,#可以替换成任何单词 queue.addArgument("x-dead-letter-routing-key","dlx.xihai"); //设置队列过期时间ttl queue.addArgument("x-message-ttl",10000); //设置队列的长队限制,当长度超出10条后都会进入死信队列中 queue.addArgument("x-max-length",10); return queue; } @Bean public TopicExchange topicExchange(){ return new TopicExchange("test.exchange"); } @Bean public Binding binding(Queue simpleQueue, TopicExchange topicExchange){ // return BindingBuilder.bind(simpleQueue).to(directExchange).with("confirm"); return BindingBuilder.bind(simpleQueue).to(topicExchange).with("test.#"); } //死信交换机 @Bean public Queue deadLetterQueue(){ return new Queue("queue.dlx"); } @Bean public TopicExchange deadLetterExchange(){ return new TopicExchange("exchange.dlx"); } @Bean public Binding bindingDeadLetter(Queue deadLetterQueue, TopicExchange deadLetterExchange){ return BindingBuilder.bind(deadLetterQueue).to(deadLetterExchange).with("dlx.#"); }
由上图可以看出,死信交换机其实就是一个正常交换机,只不过是由于在正常交换机所绑定的队列上使用死信交换机的属性绑定了它,他就成了死信交换机,这两个属性为死信交换机的名称和routingKey。有点绕,自己分句捋一捋。这里其实有很多注释,但是博客上就不显示了,有兴趣可以看看我的源码,源码会在文章首部贴出。
@Test public void testDeadLetter(){ //1.测试过期时间,死信消息 rabbitTemplate.convertAndSend("test.exchange","test.xihai","我是一条消息,我所在的queue过期后会进死信队列吗"); //2.测试长度限制后,消息超长度后死信 for (int i = 0; i < 20; i++) { rabbitTemplate.convertAndSend("test.exchange","test.xihai","我是第【" + i +"】条消息,我所在的queue过期后会进死信队列吗"); } //3.消息拒收,在消费者MyRabbitListener配置中进行了配置 rabbitTemplate.convertAndSend("test.exchange","test.xihai","我是一条消息,我所在的queue过期后会进死信队列吗"); }
这里分别模拟了三种称为死信的情况:队列消息过期、消费者拒收、队列长度限制。
对于消费端,自定了一个类,将它注入spring容器中,并通过@RabbitListener注解的方式实现了消息监听。
@Component public class MyRabbitListener { /** * Description: * springboot使用@RabbitListener注解如何修改消息确认模式 * 注解及参数的解释: * 1.@RabbitHandler:指定对消息的处理方法 * 2.@RabbitListener:指定需要处理的队列 * 3.@Payload:获得消息中的body,也就是消息主体(解码后的信息) * 4.@Headers:使用@Header接口获取messageProperties中的DELIVERY_TAG属性。 * 5.Channel:接收信息所使用的的信道 * * 注意:你还需要配置消息格式转换器,不然这个方法不会起作用。 * 原因是这个方法所处理的消息时String类型, * 而容器中默认使用的是SimpleMessageConverter,在发送消息时会将对象序列化成字节数组 * 若要反序列化,需要自己定义MessageConverter * 在SimpleMessageListenerContainer容器中进行设置 **/ @RabbitHandler @RabbitListener(queues = "simple.queue") public void DlxListener(@Payload String message, @Header (AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel ) throws IOException { try { //接收消息 System.out.println(message); //处理业务逻辑 System.out.println("RabbitListener中处理业务逻辑"); //int a = 1/0; //由于配置类中设置手动签收,所以这里可以进行手动ack channel.basicAck(deliveryTag,true); }catch (Exception e){ System.out.println("出现异常,拒绝接收"); //需要配置不重回队列才能路由到死信队列中(拒收后进入死信队列) channel.basicNack(deliveryTag,true,false); } }
处理结果就是原本在正常队列中的消息会进入到死信队列中,而正常消息会被干掉,我就不演示了,感兴趣你可以自己测试一下。
需要注意的是:你发送消息是发送给正常交换机,routingKey也是正常交换机的routingKey。只不过是正常队列中设置死信交换机的routingKey。
延迟队列
消息进入丢列或并不会立即被消费,只有到达指定时间后,才会被消费。使用场景就是30分钟后的订单支付了,当然还有其他的。
可以使用 TTL+死信队列实现该效果。
@Test public void testOrderQueue() throws InterruptedException { //1.正常交换机和队列 Queue queue0 = new Queue("order.queue", true); //3.绑定,设置正常队列过期时间为30分钟(我这里只是模拟,就只设置10秒) queue0.addArgument("x-dead-letter-exchange","order.exchange.dlx"); queue0.addArgument("x-dead-letter-routing-key","dlx.order.cancel"); queue0.addArgument("x-message-ttl",10000); rabbitAdmin.declareQueue(queue0); TopicExchange topicExchange0 = new TopicExchange("order.exchange"); rabbitAdmin.declareExchange(topicExchange0); rabbitAdmin.declareBinding(new Binding("order.queue", Binding.DestinationType.QUEUE,"order.exchange","order.#",null)); //2.定义死信交换机和队列 Queue queue1 = new Queue("order.queue.dlx", true); rabbitAdmin.declareQueue(queue1); TopicExchange topicExchange1 = new TopicExchange("order.exchange.dlx"); rabbitAdmin.declareExchange(topicExchange1); rabbitAdmin.declareBinding(new Binding("order.queue.dlx", Binding.DestinationType.QUEUE,"order.exchange.dlx","dlx.order.#",null)); //模拟发送订单消息 rabbitTemplate.convertAndSend("order.exchange","order.msg","订单信息:id=1,name=猛男汐海一枚,time=2021.12.14-18:36:24"); //打印倒计时十秒,消费者与死信配置在同一个类中 for (int i = 0; i < 10; i++){ System.out.println(i + ".."); Thread.sleep(1000); } }
消费端监听配置
/** * Description: * 延迟队列效果实现,监听的是死信队列!!! **/ @RabbitHandler @RabbitListener(queues = "order.queue.dlx") public void OrderListener(@Payload String message, @Header (AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel ) throws IOException { try { //接收消息 System.out.println(message); //处理业务逻辑 System.out.println("RabbitListener中处理业务逻辑"); System.out.println("根据订单ID查询状态...(调用数据库)"); System.out.println("判断订单是否支付成功"); System.out.println("取消订单,回滚库存"); //手动签收 channel.basicAck(deliveryTag,true); }catch (Exception e){ System.out.println("出现异常,拒绝接收"); //设置不重回队列,销毁订单 channel.basicNack(deliveryTag,true,false); } }
结合以上两个特性就可以实现,效果是对数据库进行操作只需使用死信队列中的数据,避免了频繁调用数据库。
本文到此结束