【RabbitMQ】@RabbitListener的使用及造成报错死循环的分析

问题复现:

docker 下清空对应的队列信息

exec rabbitmq /bin/bash #rabbitmq是容器名,也可以指定为Id
rabbitmqctl purge_queue queue.order # queue.order是队列名

RabbitMQ控制台新建消息,等待消费者消费
http://localhost:15672
在这里插入图片描述

前置知识:

一、 @RabbitListener的使用

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

1.1 作用在方法上

指定队列名,多个方法使用@RabbitListener,可以绑定多个消费实现

  • 指定队列的消费方法
    普通用法
    @RabbitListener(queues = "queue.order")
    public void handleMsg(String msg) {
    }
    

1.2 作用在类上

指定队列名,在上配置@RabbitListener 指定队列名 。
@RabbitHandler绑定在类中的多个方法上,视为队列名的多个消费实现

  • 需要配合 @RabbitHandler

    @RabbitListener(queues = "queue.order")
    public class PayOrderUpdateListener {
    	//监听队列的消费方法之一
    	@RabbitHandler
    	public void handlerMsg(String msg) {
    	}
    }
    
  • 注解可选参数: @RabbitListener(isDefault = true),这个参数值得注意

    @RabbitHandler(queues = "queue.order", isDefault = true)
    public void handleMsg(String msg) {
    }
    

二、 消息的封装类Message ,及解析规则

@RabbitHandler(queues = "queue.order")
public void handleMsg(Message msg) {
}
  • 关键属性content_type, 用于约束body内容
  1. http://localhost:15672 进入RabbitMQ的控制台
  2. 设置消息的content_type
    在这里插入图片描述
  3. content_type 的常见取值
content_typehandler的可接收形参
text/plainString
application/json/
application/x-java-serialized-object实体类
application/octet-streambyte[]
未指定byte[]
未指定org.springframework.amqp.core.Message

未指定 content_type 的时候,可以用byte[]接收,
值得一提的是 Message 可用于接收所有可能的序列化场景,都不会报错。

异常捕获

先分析错误,再分析死循环

【异常1】:No method found for class

在这里插入图片描述
意思是消费者已经准备消费,队列里存在消息,但是找不到指定的消费实现方法来处理。

异常1问题:为什么会找不到消费实现?

  1. @RabbitListener@RabbitHandler 配置出错
    很大原因是取决于content_type 的配置和 方法的形参
    如果通过客户端放入队列中有个content_type为空的的消息,@RabbitListener只有形参为String 的Handler,是无法对应上消费实现的。
  2. @RabbitHandler 没有使用可选参数isDefault
    消费者找不到任何一个消费实现,就回去找isDefault = truehandler,类似一个兜底策略。

异常1问题:处理思路

  1. 使用Message 作为方法形参
  2. 尽量将@RabbitListener 放在类上, 使用@RabbitHandler(isDefault = true) 做兜底策略

异常1分析 :死循环分析

这是一种应用级别的死循环,消息找不到消费实现,一直重试直到找到消费实现。这种死循环原因是配置失误,要在源头避免,测试阶段就要消灭。【找到消除这种死循环的方法再来填坑】。另外一种必须处理的死循环是已经找到消费实现,但是在消费的过程中造成死循环,见异常2:


【异常2】:消费过程中抛出未捕获Exception

通常是业务逻辑导致的异常NullPointerException,无脑的做法是try-catch,处理不当依旧会造成死循环。

异常2问题:try-catch后仍然会死循环

详细分析
这里简要概括下:RabbitMQ 默认的异常策略是不断重试除非抛出了fatal类型的异常,这种异常类型如下

异常类
MessageConversionException
MessageConversionException
MethodArgumentNotValidException
MethodArgumentTypeMismatchException
NoSuchMethodException
ClassCastException

类似于NullPointerException,是不再抛弃重试的范围的,也就是不主动捕获并处理,RabbitMQ会一直尝试消费该消息,导致死循环,从而使大量CPU资源被占用。

异常2处理: 终止死循环

最简单的办法就是在catch语句块中抛出fatal 类型的异常,如

    @RabbitHandler
    public void handlerMsg(String msg) {
        try { // try 住语句块 抛出致命异常
            //1.获取字符串转成map
            Map<String, String> map = JSON.parseObject(msg, Map.class);
            String out_trade_no = map.get("out_trade_no");
            if (map != null && map.get("return_code").equalsIgnoreCase("SUCCESS")) {
            }
        } catch (Exception e) {
            e.printStackTrace();
            // todo  解决消息队列重复试错的bug 抛出一个致命异常就会抛弃消费这个消息
            throw new MessageConversionException("消息消费失败,移出消息队列,不再试错");
        }
    }

更健壮的方法:使用异常处理器 + 死信队列
参考网上的资料,比较全面

解决方案

  1. 测试阶段暴力抛出fatal异常,终止死循环
  2. 线上不暴力抛出fatal异常,先设置重试次数,详细参数
  3. 超过重试次数,把消息加入死信队列,或者持久化到db,从消息队列暂时移除
  4. 根据失败的消息做预警,微信通知API 或者其他方式触发人工干预

参考

RabbitMQ的术语与参数配置
@RabbitListener 详解
@RabbitListener 详解, @Payload 和 @headers
MessageConvert 序列化相关
Spring Boot2.0 下 RabbitMQ的异常处理

<think>好的,我现在需要帮助用户理解他们提供的@RabbitListener注解的代码示例。用户之前已经询问过Message类的作用,现在他们展示了一个使用@RabbitListener的方法,其中参数包括String dataString、Message message和Channel channel。我需要逐步分析这个代码的结构和各个部分的作用。 首先,我应该回忆一下Spring AMQP中的@RabbitListener的基本用法。@RabbitListener通常用于声明监听某个队列的方法,当消息到达队列时,该方法会被调用。用户提供的代码中,@RabbitListener的bindings属性使用了@QueueBinding,这通常用于动态声明队列、交换机和绑定,可能在应用启动时自动创建这些资源。 接下来,分解@QueueBinding的各个部分:value指定队列,exchange指定交换机,key是路由键。这里用户的代码使用了ClientExchange类中的常量,说明用户可能有集中管理这些名称的地方,这是好的做法,便于维护。 然后看方法参数:String dataString、Message message、Channel channel。这里需要解释为什么可以同时有消息体和原始Message对象。可能涉及到消息转换器,比如默认的SimpleMessageConverter会自动将消息体转换为String,而用户也可以选择同时获取原始Message对象以访问头部或其他属性。此外,Channel参数允许手动确认消息或其他操作,比如拒绝消息、重新入队等。 需要指出的是,方法参数顺序不重要,Spring会根据类型自动注入。但需要说明各个参数的类型和用途,比如dataString是反序列化的消息体,message是原始消息,channel提供AMQP Channel的操作能力。 同时,应该提到消息确认机制。如果使用手动确认(AcknowledgeMode.MANUAL),用户需要调用channel.basicAck等方法来确认消息处理,否则可能导致消息堆积。但用户代码中没有处理确认的部分,可能需要提醒他们根据需求添加。 另外,使用@QueueBinding会在RabbitMQ服务器上声明这些资源,如果它们不存在的话。需要确认用户的交换机类型是否正确,比如这里用的是DIRECT_EXCHANGE,所以是直接交换类型,根据路由键精确匹配路由消息。 可能还需要提到潜在的问题,比如如果多个消费者使用相同的队列名称,可能会导致竞争消费;或者如果队列是持久化的,需要考虑服务器重启后的行为。此外,如果消息转换失败,如何处理异常,是否需要添加错误处理逻辑。 最后,总结这个配置的作用:监听指定的队列,将消息体转换为String,同时保留原始Message和Channel,以便进行更灵活的处理。同时,提供代码示例说明如何确认消息或处理异常,帮助用户更好地应用这个模式。</think>以下是您提供的 `@RabbitListener` 注解代码的逐步解析及其在 Spring AMQP 中的作用说明: --- ### 1. **注解配置解析** #### (1) `@RabbitListener` 的作用 - **监听消息队列**:标记一个方法为 RabbitMQ 消费者,当指定队列有消息到达时触发方法调用。 - **动态声明资源**:结合 `@QueueBinding` 可在 RabbitMQ 服务器上**自动创建队列、交换机及绑定关系**(若不存在)。 #### (2) `@QueueBinding` 的参数分解 ```java bindings = @QueueBinding( value = @Queue(value = ClientExchange.SERVER_QUEUE1), // 定义队列 exchange = @Exchange(value = ClientExchange.DIRECT_EXCHANGE), // 定义交换机 key = {ClientExchange.ROUTING_KEY} // 绑定路由键 ) ``` | 配置项 | 说明 | |----------------------|----------------------------------------------------------------------| | `value = @Queue` | 定义监听的队列名(`ClientExchange.SERVER_QUEUE1`为队列名称常量) | | `exchange = @Exchange` | 指定交换机(`ClientExchange.DIRECT_EXCHANGE`为直连交换机名称常量) | | `key` | 绑定路由键(消息通过此路由键从交换机路由到队列) | --- ### 2. **方法参数详解** ```java public void processMessage(String dataString, Message message, Channel channel) { // 参数说明: // - dataString: 反序列化后的消息体(由MessageConverter自动转换) // - message: 原始AMQP消息对象(包含Body和Properties) // - channel: RabbitMQ Channel对象(用于手动ACK/NACK等操作) } ``` #### (1) **参数注入规则** - **参数顺序无关**:Spring 根据参数类型自动匹配注入。 - **支持多类型参数**:可混合使用反序列化对象、原始`Message`和`Channel`。 #### (2) 参数类型说明 | 参数类型 | 作用 | |-------------------|----------------------------------------------------------------------| | `String dataString` | 消息体自动转换后的结果(依赖配置的`MessageConverter`,如JSON转换器) | | `Message message` | 原始消息对象(可访问消息头、属性等元信息) | | `Channel channel` | AMQP通道对象(用于手动确认消息、拒绝消息等操作) | --- ### 3. **典型使用场景** #### (1) **同时获取消息体和原始消息** ```java public void processMessage(String dataString, Message message) { // 业务数据 System.out.println("Payload: " + dataString); // 元数据(如headers) Map<String, Object> headers = message.getMessageProperties().getHeaders(); System.out.println("TraceID: " + headers.get("traceId")); } ``` #### (2) **手动消息确认(ACK/NACK)** ```java public void processMessage(String dataString, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { try { // 处理业务逻辑 processData(dataString); // 手动ACK确认 channel.basicAck(deliveryTag, false); // 第二个参数表示是否批量确认 } catch (Exception e) { // 手动NACK拒绝(并重新入队) channel.basicNack(deliveryTag, false, true); } } ``` --- ### 4. **关键配置依赖** #### (1) **消息转换器 (`MessageConverter`)** - 需确保Spring容器中配置了匹配的转换器(如JSON转换器): ```java @Bean public MessageConverter jacksonConverter() { return new Jackson2JsonMessageConverter(); } ``` #### (2) **确认模式 (`AcknowledgeMode`)** - 若需手动ACK,需在配置中设为`MANUAL`: ```yaml spring: rabbitmq: listener: simple: acknowledge-mode: manual ``` --- ### 5. **注意事项** 1. **队列/交换机声明**: - 使用`@QueueBinding`时,确保应用有 RabbitMQ 的管理权限(自动声明资源需要权限)。 - 若队列已存在,需保证声明的属性(如`durable`、`autoDelete`)与现有队列一致,否则会报错。 2. **线程安全**: - `Channel` 对象非线程安全,避免在多线程环境下共享。 3. **性能优化**: - 频繁操作`Channel`可能影响吞吐量,优先使用自动确认模式(`AUTO`)除非需要精确控制。 --- ### 6. **扩展:绑定多个路由键** 若需监听多个路由键,可直接扩展`key`数组: ```java @QueueBinding( value = @Queue(value = "myQueue"), exchange = @Exchange(value = "myExchange", type = ExchangeTypes.TOPIC), key = {"routing.key1", "routing.key2.*"} ) ``` --- 如果需要更具体的异常处理示例或配置细节,可以进一步补充问题!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值