rabbit消费堆积、超时、通道关闭问题记录-消息未应答后重入队列
背景
报错信息:
ERROR -Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - unknown delivery tag 32, class-id=60, method-id=120)
多次失败后超时通道关闭开始报超时错误找不到通道
ERROR -Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - delivery acknowledgement on channel 109 timed out. Timeout value used: 1800000 ms. This timeout value can be configured, see consumers doc guide to learn more, class-id=0, method-id=0)
Restarting Consumer@36b3911e: tags=[[amq.ctag-3MjQzJb_8aTfIuk12kOzqg]], channel=Cached Rabbit Channel: AMQChannel(amqp://localhost:5672,109), conn: Proxy@70807224 Shared Rabbit Connection: SimpleConnection@23cd4ff2 [delegate=amqp://locahost:5672/, localPort= 44358], acknowledgeMode=MANUAL local queue size=49 INFO -Received a frame on an unknown channel, ignoring it
问题分析:由于设置的手动应答模式,mq消费逻辑代码不是异步的没能及时手动应答,导致一直重入队列重复消费,其原因则是多个消费者同时消费,例如消费者1把a消息消费了但是还没手动应答比如超过30分钟了重入队列,这时候消费者2接收到了又开始消费同样没手动应答,循环一段时间后mq自动关闭了通道然后开始提示超时找不到通道。
解决:这里的业务逻辑是只要发送成功进入具体消费逻辑默认会执行成功,把代码调整为异步执行,及时进行手动应答。再次重启服务器即可把队列中失败重入的消息重新消费,这里如果不希望再消费只需要把队列删除重建即可。
本地使用Thread.sleep()调试即可触发这种情况
public class SleepUtils { public static void sleep (int second) { try { Thread.sleep(1000 * second); } catch (InterruptedException e) { e.printStackTrace(); } } }
总结
rabbitmq一旦向消费者传递一条西溪,该消息就会标记为删除,这种情况下消费者挂掉则正在处理的消息就会丢失,为了保证消息发送过程不会丢失,rabbitmq引入了应答机制,即在消费者接受并处理了该条消息后高速mq它已经把该条消息处理了,可以删除了。
自动应答
消息发送后立即被认为已经传送成功,这种模式需要在吞吐量和数据传输安全方面做权衡,这种模式下万一消费者的连接或信道关闭,消息就丢失了,这种模式没有数量限制,消息太多来不及消费也肯能出现消息堆积导致内存耗尽,最终被操作系统杀死。所以这种模式只能在消费者可以高效率、高速率的处理消息的前提下使用。
手动应答
- channel.basicAck()(用于肯定确认,即向RabbitMQ表示该消息已经发送并处理成功了,可以将其丢弃)
- channel.basicNack()(用于否定确认,即不处理该信息直接丢弃)
- channel.basicReject()(用于否定确认,即不处理该信息直接丢弃,比basicNack方法少一个Multiple参数)
channel.basicNack(deliveryTag,true)
第一个参数 deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel
第二个参数就是Multiple参数:为了减少网络流量,手动确认可以被批处理
- (1)true表示批量应答channel上未应答的消息,比如channel上有传送tag为5,6,7,8的消息,当前tag是8,那么此时5-8还未应答的消息就会被确认收到消息应答,但如果处理6或7消息失败了,5也会被应答,导致5消息丢失,所以一般情况下multiple为false。
- (2)false表示只会应答tag=8的消息,5,6,7这三个消息依然不会被确认收到消息应答
默认情况下消费者是自动ack的,所以项目中修改确认模式,如果某些逻辑需要则在消费端指定即可
配置端:
spring: rabbitmq: listener: simple: acknowledge-mode: manual #手动确认
#concurrency: 1 #消费端的监听个数(及@RabbitListener开启几个线程去处理数据) 可以在具体业务消费上开启进行限流和并发 @RabbitListener(queues = {RabbitMQConfig.TEST_QUEUE}, concurrency = "5-8"})
#max-concurrency: 10 # 消费端的监听最大个数
#prefetch: 10
#connection-timout: 15000 #超时时间
代码层:
@Bean(name="containerFactory")
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); //开启手动 ack
return factory;
}
消息重入队列
如果消费者由于某些原因失去连接,导致消费者未成功发送ACK确认应答,RabbitMQ将会对未完全处理完的消息重新入队,如果其他消费者可以处理,则该消息将被分配到另一个消费者,从而保证消息未丢失。
确认消息(局部方法处理)
@RabbitListener(queues = MQUtil.TEST_QUEUE, containerFactory = "containerFactory") public void consume1(Channel channel, Message message) { try { //消费逻辑 //手动ack应答 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (Exception e) { e.printStackTrace(); try { channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false); } catch (IOException e1) { e1.printStackTrace(); } logger.error("消费失败:id:{}, message:{}", message.getMessageProperties().getDeliveryTag(), message); } }
手动否认、拒绝消息
//消费者获取消息时检查到头部包含 error 则 nack 消息 @RabbitHandler public void processMessage2(String message, Channel channel,@Headers Map<String,Object> map) { System.out.println(message); if (map.get("error")!= null){ System.out.println("错误的消息"); try { channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true); //否认消息 return; } catch (IOException e) { e.printStackTrace(); } } try { channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //确认消息 } catch (IOException e) { e.printStackTrace(); } }
//拒绝该消息,消息会被丢弃,不会重回队列
channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //拒绝消息
确认消息(全局处理消息)
自动确认涉及到一个问题就是如果在处理消息的时候抛出异常,消息处理失败,但是因为自动确认而导致 Rabbit 将该消息删除了,造成消息丢失
@Bean public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){ SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.setQueueNames("consumer_queue"); // 监听的队列 container.setAcknowledgeMode(AcknowledgeMode.NONE); // NONE 代表自动确认 container.setMessageListener((MessageListener) message -> { //消息监听处理 System.out.println("====接收到消息====="); System.out.println(new String(message.getBody())); //相当于自己的一些消费逻辑抛错误 throw new NullPointerException("consumer fail"); }); return container; } //手动确认消息 @Bean public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){ SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.setQueueNames("consumer_queue"); // 监听的队列 container.setAcknowledgeMode(AcknowledgeMode.MANUAL); // 手动确认 container.setMessageListener((ChannelAwareMessageListener) (message, channel) -> { //消息处理 System.out.println("====接收到消息====="); System.out.println(new String(message.getBody())); if(message.getMessageProperties().getHeaders().get("error") == null){ channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); System.out.println("消息已经确认"); }else { //channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false); channel.basicReject(message.getMessageProperties().getDeliveryTag(),false); System.out.println("消息拒绝"); } }); return container; }
AcknowledgeMode 除了 MANUAL 之外还有 AUTO ,它会根据方法的执行情况来决定是否确认还是拒绝(是否重新入queue)
- 如果消息成功被消费(成功的意思是在消费的过程中没有抛出异常),则自动确认
- 当抛出 AmqpRejectAndDontRequeueException 异常的时候,则消息会被拒绝,且 requeue = false(不重新入队列)
- 当抛出 ImmediateAcknowledgeAmqpException 异常,则消费者会被确认
- 其他的异常,则消息会被拒绝,且 requeue = true(如果此时只有一个消费者监听该队列,则有发生死循环的风险,多消费端也会造成资源的极大浪费,这个在开发过程中一定要避免的)。可以通过 setDefaultRequeueRejected(默认是true)去设置
@Bean public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){ SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.setQueueNames("consumer_queue"); // 监听的队列 container.setAcknowledgeMode(AcknowledgeMode.AUTO); // 根据情况确认消息 container.setMessageListener((MessageListener) (message) -> { System.out.println("====接收到消息====="); System.out.println(new String(message.getBody())); //抛出NullPointerException异常则重新入队列 //throw new NullPointerException("消息消费失败"); //当抛出的异常是AmqpRejectAndDontRequeueException异常的时候,则消息会被拒绝,且requeue=false //throw new AmqpRejectAndDontRequeueException("消息消费失败"); //当抛出ImmediateAcknowledgeAmqpException异常,则消费者会被确认 throw new ImmediateAcknowledgeAmqpException("消息消费失败"); }); return container; }
如何保证消息可靠
持久化:exchange要持久化、queue要持久化、message要持久化
消息确认:启动消费返回(@ReturnList注解,生产者就可以知道哪些消息没有发出去)、生产者和Server(broker)之间的消息确认、消费者和Server(broker)之间的消息确认
超时问题分析:reply-text=PRECONDITION_FAILED - delivery acknowledgement on channel 109 timed out. Timeout value used: 1800000 ms
如果 RabbitMQ 在设定的超时时间内未接收到消费者的确认,它会认为这个消息可能没有被成功处理,因此会关闭对应的通道并报告这个错误。
这个超时时间可以在 RabbitMQ 的配置中进行调整。默认情况下,超时时间是 1800000 毫秒,即 30 分钟
解决方案
以下是一些可能的解决方案:
- 增加超时时间:可以考虑增加 RabbitMQ 的超时时间。这可以通过修改 RabbitMQ 的配置来实现,具体的步骤和配置项可能依赖于 RabbitMQ 版本和具体的使用场景。
- 优化消息处理:如果消费者在处理消息时耗时过长,你可能需要优化消息处理逻辑,使其能在更短的时间内完成任务并发送确认。
- 使用消息拆分:如果消息包含多个独立的任务,可以考虑将其拆分为多个消息,每个消息对应一个任务。这样,每个任务可以单独被确认,也不会阻塞其他任务的处理和确认。
- 使用异步确认:在某些情况下,也可以考虑使用异步确认。这样消费者可以立即接收下一个消息而不需要等待当前消息的确认,就是收到消息就确认,而不是等待执行完成。但是请注意,这可能会增加消息处理的复杂性和难度。

浙公网安备 33010602011771号