rabbitmq的内容

SpringBoot与RabbitMQ整合,发送和接收消息实战(代码可运行)
硅谷笔记
rabbitmq

messageproperty
mq 幂等性

RabbitMQ的Message详解

推拉模式

rabbitmq 整合springboot

千峰一哥 、消息汇总

RabbitMQ面试考点 可靠消息&延迟队列

RabbitMQ中动态创建队列和监听

缓存架构之史上讲的最明白的 RabbitMQ 可靠消息传输实战演练


黑马-rabbitmq 高级
千峰-java 挺全的-包括rabbitmq

千峰-高频面试题|RabbitMQ如何防止消息的重复消费?-代码
rabbitmq-不错-人丑多读书

  • 可靠传输示例、五种工作模式、消费者流量控制、死信备份交换机
  • 消息持久化、消息确认之confirm模式、提升消息可靠传输方法总结
  • 模拟未支付订单取消、消息幂等全局唯一id、交换机模式总结

谷粒商城-mq
谷粒-mq实战

SpringBoot整合RabbitMq(消息可靠投递


3

0. 消费者重试机制和失败策略。

RabbitMQ中重试机制的坑

0.1 mq 顺序消费

Rabbitmq消息的有序性、消息不丢失、不被重复消费

3

1.rabbitmq 持久化

1.1 队列持久化

@Bean(name = "basicQueue")
    public Queue basicQueue() {
        return new Queue(env.getProperty("mq.basic.info.queue.name"), true);
    }

1.2 消息持久化

 * 使用convertAndSend方式发送消息,消息默认就是持久化的,下面是源码:
 * new MessageProperties() --> DEFAULT_DELIVERY_MODE = MessageDeliveryMode.PERSISTENT --> deliveryMode = 2;

@Service
public class MessageSender {
    private final RabbitTemplate rabbitTemplate;
	public void sendMessage(String message) {
	    rabbitTemplate.setMandatory(true);
	    rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
	        if (!ack) {
	            // 处理消息发送失败的情况
	        }
	    });
		// 构建消息体
	    Message msg = MessageBuilder.withBody(message.getBytes())
	            .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN)
	            .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
	            .build();
		// 发送MQ消息
	    rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE_NAME, RabbitConfig.ROUTING_KEY, msg);
	}
}

在这里,我们使用 rabbitTemplate 来发送消息,并启用了 mandatory 选项,这将确保消息被成功发送到 RabbitMQ。

我们还设置了 confirmCallback 回调函数,用于处理消息发送失败的情况。如果消息无法被正确发送,我们可以在这个回调函数中执行处理逻辑。

2. 发布确认

生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理。

confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息。

3

3. exchange 类型

直接(direct), 主题(topic) ,标题(headers) , 扇出(fanout)

3.1 无名交换机

在本教程的前面部分我们对exchange一无所知,但仍然能够将消息发送到队列。之前能实现的原因是因为我们使用的是默认交换,我们通过空字符串(“”)进行标识。
2
第一个参数是交换机的名称。空字符串表示默认或无名称交换机:消息能路由发送到队列中其实是由routingKey(bindingkey)绑定key指定的,如果它存在的话

3.2 临时队列

之前的章节我们使用的是具有特定名称的队列(还记得hello和ack_queue吗?)。队列的名称我们来说至关重要-我们需要指定我们的消费者去消费哪个队列的消息。
每当我们连接到Rabbit时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列名称那就更好了。其次一旦我们断开了消费者的连接,队列将被自动删除。
创建临时队列的方式如下:
String queueName = channel.queueDeclare().getQueue();
创建出来之后长成这样:

3

3.3 bindings

什么是bingding呢,·binding其实是exchange和queue之间的桥梁·,它告诉我们exchange和那个队列进行了绑定关系。比如说下面这张图告诉我们的就是X与Q1和Q2进行了绑定
3

3.4 交换机类型

3.4.1 fanout

Fanout这种类型非常简单。正如从名称中猜到的那样,它是将接收到的所有消息广播到它知道的所有队列中。系统中默认有些exchange类型
3
3

3.4.2 direct exchange

我们再次来回顾一下什么是bindings,绑定是交换机和队列之间的桥梁关系。也可以这么理解:队列只对它绑定的交换机的消息感兴趣。绑定用参数:routingKey来表示也可称该参数为binding key,创建绑定我们用代码:channel.queueBind(queueName, EXCHANGE_NAME, “routingKey”);绑定之后的意义由其交换类型决定。
一个队列可以绑定多个bindingkey
3
在上面这张图中,我们可以看到X绑定了两个队列,绑定类型是direct。
队列Q1绑定键为orange,
队列Q2绑定键有两个:一个绑定键为black,另一个绑定键为green.

3

3.4.3 topics

3
发送到类型是topic交换机的消息的routing_key不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词,比如说:“stock.usd.nyse”, “nyse.vmw”, “quick.rabbit”.这种类型的。当然这个单词列表最多不能超过255个字节。
在这个规则列表中,其中有两个替换符是大家需要注意的
·*(星号)可以代替一个单词
#(井号)可以替代零个或多个单词

下图绑定关系如下
Q1–>绑定的是
中间带orange带3个单词的字符串(.orange.)
Q2–>绑定的是
最后一个单词是rabbit的3个单词(..rabbit)
第一个单词是lazy的多个单词(lazy.#)

3
上图是一个队列绑定关系图,我们来看看他们之间数据接收情况是怎么样的
quick.orange.rabbit 被队列Q1Q2接收到
lazy.orange.elephant 被队列Q1Q2接收到
quick.orange.fox 被队列Q1接收到
lazy.brown.fox 被队列Q2接收到
lazy.pink.rabbit 虽然满足两个绑定但只被队列Q2接收一次
quick.brown.fox 不匹配任何绑定不会被任何队列接收到会被丢弃
quick.orange.male.rabbit 是四个单词不匹配任何绑定会被丢弃
lazy.orange.male.rabbit 是四个单词但匹配Q2

当队列绑定关系是下列这种情况时需要引起注意

  • 当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像fanout了
  • 如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是direct

4. 死信队列

先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer将消息投递到broker或者直接到queue里了,consumer从queue取出消息进行消费,但某些时候由于特定的原因导致queue中的某些消息无法被消费, 这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。

应用场景:为了保证订单业务的消息数据不丢失,需要使用到RabbitMQ的死信队列机制,当消息消费发生异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时间未支付时自动失效

// 声明普通的 simple.queue队列,并且为其指定死信交换机:dl.direct
@Bean
public Queue simpleQueue2(){
    return QueueBuilder.durable("simple.queue") // 指定队列名称,并持久化
        .deadLetterExchange("dl.direct") // 指定死信交换机
        .build();
}
// 声明死信交换机 dl.direct
@Bean
public DirectExchange dlExchange(){
    return new DirectExchange("dl.direct", true, false);
}
// 声明存储死信的队列 dl.queue
@Bean
public Queue dlQueue(){
    return new Queue("dl.queue", true);
}
// 将死信队列 与 死信交换机绑定
@Bean
public Binding dlBinding(){
    return BindingBuilder.bind(dlQueue()).to(dlExchange()).with("simple");
}

4.1 什么情况导致消息死信

  • 消息TTL过期
  • 队列达到最大长度(队列满了,无法再添加数据到mq中)
  • 消息被拒绝(basic.reject或basic.nack)并且requeue=false.
    3
    3

3

4.1.1 消息ttl

3

3
3

4.1.2 超出消息队列长度

正常队列最大6条消息。
3
3

4.1.3 拒绝消息 且不重新入队

if(message.equals("info5")){
                System.out.println("Consumer01接收到消息" + message + "并拒绝签收该消息");
                //requeue设置为false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中
                channel.basicReject(delivery.getEnvelope().getDeliveryTag(), 
                false);
            }

5.延迟队列

5.1 rabbitmq 的ttl

单位是毫秒。换句话说,如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的TTL和消息的TTL,那么较小的那个值将会被使用,有两种方式设置TTL。

5.1.1 消息ttl

@Test
public void testTTLMsg() {
    // 创建消息
    Message message = MessageBuilder
        .withBody("hello, ttl message".getBytes(StandardCharsets.UTF_8))
        .setExpiration("5000")
        .build();
    // 消息ID,需要封装到CorrelationData中
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    // 发送消息
    rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData);
    log.debug("发送消息成功");
}

5.1.2 队列ttl

@Bean
public Queue ttlQueue(){
    return QueueBuilder.durable("ttl.queue") // 指定队列名称,并持久化
        .ttl(10000) // 设置队列的超时时间,10秒
        .deadLetterExchange("dl.ttl.direct") // 指定死信交换机
        .build();
}

5.1.3 两者区别

如果设置了队列的TTL属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中),而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间;另外,还需要注意的一点是,如果不设置TTL,表示消息永远不会过期,如果将TTL设置为0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃。

5.2 延时队列优化

3

不过,如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有10S和40S两个时间选项,如果需要一个小时后处理,那么就需要增加TTL为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?

在这里新增了一个队列QC,绑定关系如下,该队列不设置TTL时间
3
看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置TTL的方式,消息可能并不会按时“死亡“,·因为RabbitMQ只会检查第一个消息是否过期如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。

5.2 rabbitmq 延时插件

2.3.2.DelayExchange原理
DelayExchange需要将一个交换机声明为delayed类型。当我们发送消息到delayExchange时,流程如下:

3

  • 接收消息
  • 判断消息是否具备x-delay属性
  • 如果有x-delay属性,说明是延迟消息,持久化到硬盘,读取x-delay值,作为延迟时间
  • 返回routing not found结果给消息发送者
  • x-delay时间到期后,重新投递消息到指定队列

5.2.1 使用delayExchange

插件的使用也非常简单:声明一个交换机,交换机的类型可以是任意类型,只需要设定delayed属性为true即可,然后声明队列与其绑定即可。

1)声明DelayExchange交换机
3

也可以基于@Bean的方式:
2

2)发送消息
发送消息时,一定要携带x-delay属性,指定延迟的时间:.

2
// 也可以使用MessagePostProcessor

/**
                 * public void setDelay(Integer delay) {
                 * 		if (delay == null || delay < 0) {
                 * 			this.headers.remove(X_DELAY);
                 *                }
                 * 		else {
                 * 			this.headers.put(X_DELAY, delay);
                 *        }* 	}
                 */
                MessagePostProcessor po=message1->{
                    message1.getMessageProperties()
                            .setDelay(1000);
                    return message1;
                    
                };

2
那这里为什么会报错呢,这是因为delay的交换机是将消息持有了5秒后发送的,这里其实不是报错,而是消息暂存了5秒,过了5秒才发送到队列,那我们能不让它们报错吗,当然可以,我们继续修改。
根据receivedDelay是否有值判断是否重发,有就不重发
修改publisher中的CommonConfig.java
2

6. 发布确认高级

3

spring:
  rabbitmq:
    host: 
    port: 5672
    virtual-host: /
    #开启发送端确认
    publisher-confirm-type: correlated
    #开启发送消息抵达队列的确认
    publisher-returns: true
    #只要抵达队列,以异步发送优先回调returnConfirm
    template:
      mandatory: true


  • publish-confirm-type:开启publisher-confirm,这里支持两种类型:
    • simple:同步等待confirm结果,直到超时
    • correlated⭐:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
  • publish-returns:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallback
    - template.mandatory:定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息

6.1 发布确认

消息到不了交换机
3

spring:
  rabbitmq:
    host: 
    port: 5672
    virtual-host: /
    #开启发送端确认
    publisher-confirm-type: correlated

  • NONE
    禁用发布确认模式,是默认值
  • CORRELATED
    发布消息成功到交换器后会触发回调方法
  • SIMPLE
    经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法.
    其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或
    waitForConfirmsOrDie方法 等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,
    ·要注意的点是 waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker

发布确认代码

每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目加载时配置:

  // 生产者发送消息后,如果发送成功,则打印“消息发送成功”的日志信息
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
        /**
             *
             * @param correlationData 当前消息唯一关联数据
             * @param b 是否成功收到
             * @param cause 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                String id=correlationData!=null?correlationData.getId():"";
                if(ack){
                    log.info("交换机已经收到id为:{}的消息",id);
                }else{
                    log.info("交换机还未收到id为:{}消息,由于原因:{}",id,cause);
                }
            }
        });

6.2 回退消息

消息到了broker,但routingkey 不对,到不了队列。
8.2.1.Mandatory参数
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。·那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置mandatory参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。

  /**
             *
             *  returnedMessage 该类有以下变量
             *  Message message; 投递失败的消息详细信息
             *  replyCode;       回复的状态吗
             *  replyText;       回复的文本内容
             *  exchange;        当时这个消息发给那个交换机
             *  routingKey;      当时这个消息用那个路由键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, 
            String replyText, String exchange, String routingKey) {
                log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}"
                ,exchange,routingKey,replyCode,replyText,message);
            }
        });

总体代码


    @PostConstruct
    public void initRabbitTemplate() {
        //设置一个确认回调
      /**
             * @param correlationData 当前消息唯一关联数据
             * @param ack 是否成功收到
             * @param cause 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                String id=correlationData!=null?correlationData.getId():"";
                if(ack){
                    log.info("交换机已经收到id为:{}的消息",id);
                }else{
                    log.info("交换机还未收到id为:{}消息,由于原因:{}",id,cause);
                }
            }
        });
  // 生产者发送消息后,若发送失败,则输出“消息发送失败”的日志信息
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             *
             *  returnedMessage 该类有以下变量
             *  Message message; 投递失败的消息详细信息
             *  replyCode;       回复的状态吗
             *  replyText;       回复的文本内容
             *  exchange;        当时这个消息发给那个交换机
             *  routingKey;      当时这个消息用那个路由键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}",exchange,routingKey,replyCode,replyText,message);
            }
        });
    }

6.3 备份交换机

有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?

前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。·在RabbitMQ中,有一种备份交换机的机制存在,可以很好的应对这个问题

什么是备份交换机呢?

备份交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。

3

 public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
    public static final String BACKUP_QUEUE_NAME = "backup.queue";
    public static final String WARNING_QUEUE_NAME = "warning.queue";
//声明确认Exchange交换机的备份交换机
// 创建正常交换机时 设置备份交换机名称即可。
    @Bean("confirmExchange")
    public DirectExchange confirmExchange(){
        ExchangeBuilder exchangeBuilder =
                ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
                        .durable(true)
                        //设置该交换机的备份交换机
                        .withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);
        return (DirectExchange)exchangeBuilder.build();
    }
//声明备份Exchange
    @Bean("backupExchange")
    public FanoutExchange backupExchange(){
        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }

7. 消费者端确认

spring:
  rabbitmq:
    host: 
    port: 5672
    virtual-host: /
    #开启发送端确认
    publisher-confirm-type: correlated
    #开启发送消息抵达队列的确认
    publisher-returns: true
    #只要抵达队列,以异步发送优先回调returnConfirm
    template:
      mandatory: true
    #默认客户端接收到消息自动ack, acknowledge修改为manual 开启手动确认
    listener:
      simple:
        acknowledge-mode: manual

3

   /**
     * queues 需要监听的队列
     * <p>
     * message: 原生消息详细信息。头+体
     * T<发送的消息类型></>  Order order
     * channel:当前传输数据的通道
     */
    @RabbitHandler
    public void recieveMessage(Message message, Order order,
                               Channel channel) throws IOException {
        //deliveryTag channel内按顺序自增
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        log.info("接收队列订单消息{}", order);
        if (ThreadLocalRandom.current().nextInt(100) % 2 == 0) {
            /**签收消息
             * long deliveryTag, boolean multiple
             * multiple 是否批量签收模式 false代表我只签收我当前这条消息
             *  签收需要给broker回复ack,所以有可能网络中断 需要捕获异常或者抛出异常
             */
            channel.basicAck(deliveryTag, false);
            log.info("消息签收成功");
        } else {
            /**
             * long deliveryTag, boolean multiple, boolean requeue
             * multiple 是否批量拒绝签收
             * requeue true 发回服务器,重新入队  false 丢弃
             */
            channel.basicNack(deliveryTag, false, false);
            log.info("消息签收失败丢弃消息");
        }
    }

import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
 
import java.io.IOException;
 
/**
 * @Description: 消费者2
 * @author: weishihuai
 * @Date: 2019/6/27 10:42
 */
@Component
public class Consumer02 {
    private static final Logger logger = LoggerFactory.getLogger(Consumer02.class);
 
    @RabbitListener(queues = "message_confirm_queue")
    public void receiveMessage02(String msg, Channel channel, Message message) throws IOException {
        try {
            logger.info("【Consumer02成功接收到消息】>>> {}", msg);
            // 确认收到消息,只确认当前消费者的一个消息收到
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
        //、消费方的Message对象有个getRedelivered()方法返回Boolean,为TRUE就表示重复发送过来的。
            if (message.getMessageProperties().getRedelivered()) {
                logger.info("【Consumer02】消息已经回滚过,拒绝接收消息 : {}", msg);
                // 拒绝消息,并且不再重新进入队列
                //public void basicReject(long deliveryTag, boolean requeue)
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
            } else {
                logger.info("【Consumer02】消息即将返回队列重新处理 :{}", msg);
                //设置消息重新回到队列处理
                // requeue表示是否重新回到队列,true重新入队
                //public void basicNack(long deliveryTag, boolean multiple, boolean requeue)
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            }
            e.printStackTrace();
        }
    }
 
}

CorrelationData 和 rabbit自己的DeliveryTag的区别是什么呢?

CorrelationData 是实现接口 ConfirmCallback ,重写其confirm()方法的参数之一,表示对象内部只有一个 id 属性,用来表示当前消息的唯一性;而DeliveryTag 可以做此消息处理通道的名字,回传告诉 rabbitmq 这个消息处理成功并清除此消息,每次接收消息+1。

消费者 获取correlationDataid
RabbitMQ 消费者如何获取生产者设置的correlationId + 得到CorrelationId为空的解决方案 + 源码解析

所以道理也差不多,全局设置就完事了。

这种方法唯一的好处,就是它的参数直接就是CorrelationData,不用转换

rabbitTemplate.setCorrelationDataPostProcessor(new CorrelationDataPostProcessor() {
         @Override
         public CorrelationData postProcess(Message message, CorrelationData correlationData) {
             MessageProperties messageProperties = message.getMessageProperties();

             messageProperties.setCorrelationId(correlationData.getId());
             //这里可以处理correlationData,该data会在confirm时接收
             correlationData.setId("经过处理的Id");
             return correlationData;
         }
});

消息重试机制


spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 开启消费者失败重试
          initial-interval: 1000 # 初始的失败等待时长为1秒
          #如果 multiper 不是1 ,那时间就会累加
          # 2时  1S  2S  4S  8S
          multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          max-attempts: 4 # 最大重试次数
          stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false

开启本地重试时,消息处理过程中抛出异常,不会requeue到队列,而是在消费者本地重试
重试达到最大次数后,Spring会返回ack,消息会被丢弃

@RabbitListener和@RabbitHandler注解

监听消息:使用@RabbitListener和@RabbitHandler,主启动类必须有@EnableRabbit。

@RabbitListener: 类+方法上(监听哪些队列即可)
@RabbitHandler: 标在方法上(重载区分不同的消息)

@RabbitListener注解@RabbitHandler都可以接受消息队列中的消息,并进行处理。

@RabbitListener注解:
使用@RabbitListener时主启动类必须有@EnableRabbit,其可以标记方法或类上进行使用

自定义方法的参数可以为以下类型:

1、Message message:原生消息详细信息。头 + 体

2、T <发送的消息的类型> 可以是我们自定义的对象

3、Channel channel :当前传输数据的信道。

@RabbitListener(queues = {"hello.queue"})
public String receiveMessage(Message message, OrderEntity content) {
    //消息体信息
    byte[] body = message.getBody();
    // 消息头信息
    MessageProperties messageProperties = message.getMessageProperties();
    log.info("收到的消息: {}", content);
    return "ok";
}

同时要注意:Queue可以由很多方法来监听,只要收到消息,队列就删除消息,并且只能有一个方法收到消息。并且一个方法接收消息是一个线性的操作,只有处理完一个消息之后才能接收下条消息

@RabbitHandler注解:

@RabbitHandler标在方法上用于·接受不同类型的消息对象

@RabbitHandler标记的方法结合@RabbitListener,@RabbitHandler使用可以变得更加灵活:采用在类上加 @RabbitListener 注解,标识监听哪些消息队列。在方法上添加@RabbitHandler注解,重载区分不同的消息。

比如说,当两个方法对一个消息队列进行监听时,用于监听的两个方法用于接收消息内容的参数不同,根据消息的内容可以自动的确定使用那个方法。

@RestController
public class RabbitController {
    @Autowired
    RabbitTemplate rabbitTemplate;
    
    @GetMapping("/sendMq")
    public String sendMq(@RequestParam(value = "num",defaultValue = "10") Integer num){
        for (int i = 0; i < num; i++){
            //向一个队列中发送两种不同类型的消息
            if (i%2==0){
                OrderReturnApplyEntity orderReturnApplyEntity = new OrderReturnApplyEntity();
                orderReturnApplyEntity.setId(1L);
                orderReturnApplyEntity.setCreateTime(new Date());
                orderReturnApplyEntity.setReturnName("哈哈哈");
                //配置MyRabbitConfig,让发送的对象类型的消息,可以是一个json
                rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderReturnApplyEntity, new 				 CorrelationData(UUID.randomUUID().toString()));
            }else {
                OrderEntity entity = new OrderEntity();
                entity.setOrderSn(UUID.randomUUID().toString());
                rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",entity, new 								CorrelationData(UUID.randomUUID().toString()));
            }
        }
        return "OK";
    }
}
@RabbitListener(queues = {"hello-java-queue"})//queues:声明需要监听的所有队列
@Service("orderItemService")
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {
    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<OrderItemEntity> page = this.page(
                new Query<OrderItemEntity>().getPage(params),
                new QueryWrapper<OrderItemEntity>()
        );
        return new PageUtils(page);
    }
     
    /**
     * 以下参数是我们自定义的,spring会自动帮我们解析
     * 参数1、Message message:原生消息详细信息。头+体
     * 参数2、T<发送的消息类型> OrderReturnApplyEntity content
     * 参数3、Channel channel 当前传输数据的通道
     * Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息
     * 场景:
     *       1)、订单服务启动多个;同一个消息,只能有一个客户端收到
     *       2)、只有一个消息完全处理完,方法运行结束,才可以接收到下一个消息
     */
    //@RabbitListener(queues = {"hello-java-queue"})
    @RabbitHandler
    public void receiverMessage(Message message,OrderReturnApplyEntity content,
                                Channel channel) throws InterruptedException {
        //消息体
        byte[] body = message.getBody();
        //消息头属性信息
        MessageProperties properties = message.getMessageProperties();
        System.out.println("接收到消息...内容:" + content);
		//Thread.sleep(3000);
        System.out.println("消息处理完成=》"+content.getReturnName());
    }
    
    @RabbitHandler
    public void receiverMessage(OrderEntity orderEntity){
        System.out.println("接收到消息...内容:" + orderEntity);
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值