第五章_RabbitMQ高级特性

发布确认高级

在生产环境中由于一些不明原因,导致rabbitmq重启,在RabbitMQ重启期间生产者消息投递失败, 导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行RabbitMQ的消息可靠投递呢? 特别是在这样比较极端的情况,RabbitMQ集群不可用的时候,无法投递的消息该如何处理呢:

1.发布确认springboot 版本

确认机制方案

代码架构图 

配置文件 

在配置文件当中需要添加:

spring.rabbitmq.publisher-confirm-type=correlated

⚫ NONE 禁用发布确认模式,是默认值

⚫ CORRELATED 发布消息成功到交换器后会触发回调方法

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

spring.rabbitmq.host=182.92.234.71
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
spring.rabbitmq.publisher-confirm-type=correlated

添加配置类 

@Configuration
public class ConfirmConfig {
    
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";

    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";

    //声明业务 Exchange
    @Bean("confirmExchange")
    public DirectExchange confirmExchange() {
        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
    }

    // 声明确认队列
    @Bean("confirmQueue")
    public Queue confirmQueue() {
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }
 
    // 声明确认队列绑定关系
    @Bean
    public Binding queueBinding(@Qualifier("confirmQueue") Queue queue, @Qualifier("confirmExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("key1");
    }

}

消息生产者 

@Slf4j
@RestController
@RequestMapping("/confirm")
public class Producer {

    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private MyCallBack myCallBack;

    //依赖注入 rabbitTemplate 之后再设置它的回调对象
    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(myCallBack);
    }

    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message){
        //指定消息 id 为 1
        CorrelationData correlationData1 = new CorrelationData("1");
        String routingKey = "key1";
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message + routingKey, correlationData1);

        CorrelationData correlationData2 = new CorrelationData("2");
        routingKey = "key2";
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message + routingKey, correlationData2);
        log.info("发送消息内容:{}", message);
    }

}

回调接口 

@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback {

    /**
     * 交换机不管是否收到消息的一个回调方法
     * CorrelationData
     * 消息相关数据
     * ack
     * 交换机是否收到消息
     */
    @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);
        }
    }

}

消息消费者 

@Slf4j
public class ConfirmConsumer {

    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";

    @RabbitListener(queues = CONFIRM_QUEUE_NAME)
    public void receiveMsg(Message message){
        String msg = new String(message.getBody());
        log.info("接受到队列 confirm.queue 消息:{}", msg);
    }

}

结果分析 

可以看到,发送了两条消息,第一条消息的RoutingKey为"key1",第二条消息的RoutingKey为 "key2",两条消息都成功被交换机接收,也收到了交换机的确认回调,但消费者只收到了一条消息,因为第二条消息的RoutingKey与队列的BindingKey不一致,也没有其它队列能接收这个消息,所有第二条消息被直接丢弃了。 

2.回退消息

Mandatory 参数

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

配置文件

消息生产者代码

@Slf4j
@Component
public class MessageProducer implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    //rabbitTemplate
    注入之后就设置该值
    @PostConstruct
    private void init() {
        rabbitTemplate.setConfirmCallback(this);
        /**
         * 配置文件设置了,这里可以不设置
         * true:交换机无法将消息进行路由时,会将该消息返回给生产者
         * false:如果发现消息无法进行路由,则直接丢弃
         */
        rabbitTemplate.setMandatory(true);
        //设置回退消息交给谁处理
        rabbitTemplate.setReturnCallback(this);
    }

    @GetMapping("/sendMessage")
    public void sendMessage(String message){
        //让消息绑定一个id值
        CorrelationData correlationData1 = new     CorrelationData(UUID.randomUUID().toString());
        rabbitTemplate.convertAndSend("confirm.exchange", "key1", message + "key1", correlationData1);

        log.info("发送消息 id 为:{}内容为{}", correlationData1.getId(), message + "key1");
        CorrelationData correlationData2 = new CorrelationData(UUID.randomUUID().toString());
        rabbitTemplate.convertAndSend("confirm.exchange", "key2", message + "key2", correlationData2);
        log.info("发送消息 id 为:{}内容为{}",correlationData2.getId(),message+"key2");
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack) {
            log.info("交换机收到消息确认成功, id:{}", id);
        } else {
            log.error("消息 id:{}未成功投递到交换机,原因是:{}", id, cause);
        }
    }

    //可以在当消息传递过程中不可达目的地时将消息返回给生产者
    //只有不可达目的地的时候,才进行回退
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.info("消息:{}被服务器退回,退回原因:{}, 交换机是:{}, 路由 key:{}", new String(message.getBody()), replyText, exchange, routingKey);
    }

}

回调接口 

@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    /**
     *
     * 交换机不管是否收到消息的一个回调方法
     * CorrelationData消息相关数据
     * ack交换机是否收到消息
     */
    @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);
        }
    }

    //当消息无法路由的时候的回调方法
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.error("消息{}, 被交换机{}退回, 退回原因:{}, 路由key:{}", new String(message.getBody()), exchange, replyText, routingKey);
    }

}

结果分析 

3.备份交换机

有了mandatory参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息
无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。在RabbitMQ中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份交换机可以理解为RabbitMQ中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为Fanout,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。

代码架构图

修改配置类 

@Configuration
public class ConfirmConfig {

    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";

    //声明确认队列
    @Bean("confirmQueue")
    public Queue confirmQueue() {
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    //声明确认队列绑定关系
    @Bean
    public Binding queueBinding(@Qualifier("confirmQueue") Queue queue, @Qualifier("confirmExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("key1");
    }

    //声明备份 Exchange
    @Bean("backupExchange")
    public FanoutExchange backupExchange() {
        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }

    //声明确认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();
    }

    //声明警告队列
    @Bean("warningQueue")
    public Queue warningQueue() {
        return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
    }

    //声明报警队列绑定关系
    @Bean
    public Binding warningBinding(@Qualifier("warningQueue") Queue queue, @Qualifier("backupExchange") FanoutExchange backupExchange) {
        return BindingBuilder.bind(queue).to(backupExchange);
    }

    //声明备份队列
    @Bean("backQueue")
    public Queue backQueue()    {
        return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
    }

    //声明备份队列绑定关系
    @Bean
    public Binding backupBinding(@Qualifier("backQueue") Queue queue, @Qualifier("backupExchange") FanoutExchange backupExchange) {
        return BindingBuilder.bind(queue).to(backupExchange);
    }

}

报警消费者 

@Slf4j
@Component
public class WarningConsumer {

    public static final String WARNING_QUEUE_NAME = "warning.queue";

    @RabbitListener(queues = WARNING_QUEUE_NAME)
    public void receiveWarningMsg(Message message) {
        String msg = new String(message.getBody());
        log.error("报警发现不可路由消息:{}", msg);
    }

}

测试注意事项

重新启动项目的时候需要把原来的 confirm.exchange 删除因为我们修改了其绑定属性,不然报以下错:

结果分析 

mandatory参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先级高,经过上面结果显示答案是备份交换机优先级高。 

4.发布确认拓展

发布确认原理

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

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

生产端向rabbitmq发送消息时,由于网络等原因可能导致消息发送失败。所以,rabbitmq必须有机制确保消息能准确到达mq,如果不能到达,必须反馈给生产端进行重发。

发布确认的策略

开启发布确认的方法

发布确认默认是没有开启的,如果要开启需要调用方法confirmSelect,每当你要想使用发布确认,都需要在channel上调用该方法。

怎么做

RabbitMQ消息的可靠性投递主要两种实现:

1、通过实现消费的重试机制,通过@Retryable来实现重试,可以设置重试次数和重试频率;

2、生产端实现消息可靠性投递。

两种方法消费端都可能收到重复消息,要求消费端必须实现幂等性消费。

消息的可靠投递

生产端

在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式:

  • confirm 确认模式
  • return 退回模式

消息投递到exchange的确认模式

rabbitmq的消息投递的过程为:

producer ——> rabbitmq broker cluster ——> exchange ——> queue ——> consumer

  • 生产端发送消息到rabbitmq broker cluster后,异步接受从rabbitmq返回的ack确认信息
  • 生产端收到返回的ack确认消息后,根据ack是true还是false,调用confirmCallback接口进行处理

1、改yml

spring:
  # rabbitmq 连接配置
  rabbitmq:
    publisher-confirm-type: correlated # 开启confirm确认模式

2、实现confirm方法

实现ConfirmCallback接口中的confirm方法,消息只要被 rabbitmq broker接收到就会触ConfirmCallback 回调,ack为true表示消息发送成功,ack为false表示消息发送失败

package com.rabbitmq.config;

import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

/**
 * @Author: xj0927
 * @Description: 实现ConfirmCallback接口
 * @Date Created in 2021-01-13 12:46
 * @Modified By:
 */
@Component
public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback {

    /**
     * @param correlationData 相关配置信息
     * @param ack exchange交换机 是否成功收到了消息。true 成功,false代表失败
     * @param cause 失败原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            //接收成功
            System.out.println("成功发送到交换机<===>");
        } else {
            //接收失败
            System.out.println("失败原因:===>" + cause);

            //TODO 做一些处理:消息再次发送等等

        }
    }
}

3、测试

定义 Exchange 和 Queue

定义交换机 confirmTestExchange 和队列 confirm_test_queue ,并将队列绑定在交换机上。

package com.rabbitmq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author: xj0927
 * @Description:  队列与交换机绑定
 * @Date Created in 2021-01-13 12:41
 * @Modified By:
 */
@Configuration
public class QueueConfig {

    @Bean(name = "confirmTestQueue")
    public Queue confirmTestQueue() {
        return new Queue("confirm_test_queue", true, false, false);
    }

    @Bean(name = "confirmTestExchange")
    public FanoutExchange confirmTestExchange() {
        return new FanoutExchange("confirmTestExchange");
    }

    @Bean
    public Binding confirmTestFanoutExchangeAndQueue(
            @Qualifier("confirmTestExchange") FanoutExchange confirmTestExchange,
            @Qualifier("confirmTestQueue") Queue confirmTestQueue) {
        return BindingBuilder.bind(confirmTestQueue).to(confirmTestExchange);
    }
}

生产者

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RabbitmqApplication.class)
public class Producer {

    @Autowired
    private RabbitTemplate rabbitTemplate;  //注入rabbitmq对象

    @Autowired
    private ConfirmCallbackService confirmCallbackService; //注入ConfirmCallback对象

    @Test
    public void test() {
        //
        rabbitTemplate.setConfirmCallback(confirmCallbackService);
        //发送消息
        rabbitTemplate.convertAndSend("confirmTestExchange1", "", "hello,ConfirmCallback你好");
    }
}

正确情况,ack返回true,表示投递成功。

现在我们改变交换机名字,发送到一个不存在的交换机

//发送消息
rabbitTemplate.convertAndSend("confirmTestExchange1", "", "hello,ConfirmCallback你好");

消息未投递到queue的退回模式

消息从 exchange–>queue 投递失败则会返回一个 returnCallback

生产端通过实现ReturnCallback接口,启动消息失败返回,消息路由不到队列时会触发该回调接口

1、改yml

spring:
  # rabbitmq 连接配置
  rabbitmq:
    publisher-returns: true # 开启退回模式

2、设置投递失败的模式

如果消息没有路由到Queue,则丢弃消息(默认)

如果消息没有路由到Queue,返回给消息发送方ReturnCallBack(开启后)

rabbitTemplate.setMandatory(true);

2、实现returnedMessage方法

启动消息失败返回,消息路由不到队列时会触发该回调接口

package com.rabbitmq.config;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

/**
 * @Author: xj0927
 * @Description:
 * @Date Created in 2021-01-13 12:55
 * @Modified By:
 */
@Component
public class ReturnCallbackService implements RabbitTemplate.ReturnCallback {

    /**
     * @param message    消息对象
     * @param replyCode  错误码
     * @param replyText  错误信息
     * @param exchange   交换机
     * @param routingKey 路由键
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {

        System.out.println("消息对象===>:" + message);
        System.out.println("错误码===>:" + replyCode);
        System.out.println("错误信息===>:" + replyText);
        System.out.println("消息使用的交换器===>:" + exchange);
        System.out.println("消息使用的路由key===>:" + routingKey);

        //TODO ===>做业务处理
    }
}

3、测试

生产者

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RabbitmqApplication.class)
public class Producer {

    @Autowired
    private RabbitTemplate rabbitTemplate;  //注入rabbitmq对象
    @Autowired
    private ConfirmCallbackService confirmCallbackService;
    @Autowired
    private ReturnCallbackService returnCallbackService;

    @Test
    public void test() {

        /**
         * 确保消息发送失败后可以重新返回到队列中
         */
        rabbitTemplate.setMandatory(true);

        /**
         * 消息投递到队列失败回调处理
         */
        rabbitTemplate.setReturnCallback(returnCallbackService);

        /**
         * 消息投递确认模式
         */
        rabbitTemplate.setConfirmCallback(confirmCallbackService);

        //发送消息
        rabbitTemplate.convertAndSend("confirmTestExchange", "info", "hello,ConfirmCallback你好");
    }
}

如果不存在路由key"dire",会调用ReturnCallback接口

消费端

消息确认机制ack

ack指Acknowledge确认。 表示消费端收到消息后的确认方式

消费端消息的确认分为:自动确认(默认)、手动确认、不确认

  •     AcknowledgeMode.NONE:不确认
  •     AcknowledgeMode.AUTO:自动确认
  •     AcknowledgeMode.MANUAL:手动确认

其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。

但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了 手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用 channel.basicNack()方法,让其自动重新发送消息。

1、改yml

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual # 手动确认

2、确认配置

@Component
@RabbitListener(queues = "confirm_test_queue")
public class ReceiverMessage {

    @RabbitHandler
    public void processHandler(String msg, Channel channel, Message message) throws IOException {

        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {

            System.out.println("消息内容===>" + new String(message.getBody()));

            //TODO 具体业务逻辑

            //手动签收[参数1:消息投递序号,参数2:批量签收]
            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
            //拒绝签收[参数1:消息投递序号,参数2:批量拒绝,参数3:是否重新加入队列]
            channel.basicNack(deliveryTag, true, true);
        }
    }
}

channel.basicNack 方法与 channel.basicReject 方法区别在于basicNack可以批量拒绝多条消息,而basicReject一次只能拒绝一条消息。

3、测试

正常情况

异常情况

在业务处理模块增加异常

//TODO 具体业务逻辑
System.out.println("开始处理业务逻辑代码:==>");
int i = 3/0;

发生异常,拒绝确认,重新加入队列,一直循环,知道确认消息。

RabbitMQ 其他知识点

1.幂等性

概念

用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等。

消息重复消费

消费者在消费MQ中的消息时,MQ已把消息发送给消费者,消费者在给MQ返回ack时网络中断,故MQ未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。

解决思路

MQ消费者的幂等性的解决一般使用全局ID或者写个唯一标识比如时间戳或者UUID或者订单消费
者消费MQ中的消息也可利用MQ的该id来判断,或者可按自己的规则生成一个全局唯一id,每次消费消息时用该id先判断该消息是否已消费过。

消费端的幂等性保障

在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:

a.唯一ID+指纹码机制,利用数据库主键去重;

b.利用redis的原子性去实现。

唯一ID+指纹码机制

指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id是否存在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。

Redis 原子性

利用redis执行setnx命令,天然具有幂等性。从而实现不重复消费。

2.优先级队列

使用场景

在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tmall商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用redis来存放的定时轮询,大家都知道redis只能用List做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级,否则就是默认优先级。

如何添加

a.控制台页面添加

b.队列中代码添加优先级

Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare("hello", true, false, false, params);

c.消息中代码添加优先级

AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();

 d.注意事项

要让队列实现优先级需要做的事情有如下事情:队列需要设置为优先级队列,消息需要设置消息的优先级,消费者需要等待消息已经发送到队列中才去消费因为,这样才有机会对消息进行排序。

实战

a.消息生产者

public class Producer {

    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel();) {
            //给消息赋予一个 priority属性
            AMQP.BasicProperties properties = new
            AMQP.BasicProperties().builder().priority(5).build();

            for (int i = 1; i < 11; i++) {
                String message = "info" + i;

                if(i == 5) {
                    channel.basicPublish("", QUEUE_NAME, properties, message.getBytes());
                } else {
                    channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
                }

                System.out.println("发送消息完成:" + message);
            }
        }
    }
}

b.消息消费者

public class Consumer {

    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        //设置队列的最大优先级 最大可以设置到 255官网推荐 1-10,如果设置太高比较吃内存和CPU
        Map<String, Object> params = new HashMap();
        params.put("x-max-priority", 10);
        channel.queueDeclare(QUEUE_NAME, true, false, false, params);

        System.out.println("消费者启动等待消费......");
        DeliverCallback deliverCallback=(consumerTag, delivery) -> {
            String receivedMessage = new String(delivery.getBody());
            System.out.println("接收到消息:" + receivedMessage);
        };

        channel.basicConsume(QUEUE_NAME, true, deliverCallback, (consumerTag) -> {
            System.out.println("消费者无法消费消息时调用,如队列被删除");
        });
    }
}

 3.惰性队列

使用场景

RabbitMQ从3.6.0版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。

默认情况下,当生产者将消息发送到RabbitMQ的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当RabbitMQ需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然RabbitMQ的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。

两种模式

队列具备两种模式:default和lazy。默认的为default模式,在3.6.0之前的版本无需做任何变更。lazy模式即为惰性队列的模式,可以通过调用channel.queueDeclare方法的时候在参数中设置,也可以通过Policy的方式设置,如果一个队列同时使用这两种方式设置的话,那么Policy的方式具备更高的优先级。如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。

在队列声明的时候可以通过“x-queue-mode”参数来设置队列的模式,取值为“default”和“lazy”。下面示例中演示了一个惰性队列的声明细节:

Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);

内存开销对比 


在发送1百万条消息,每条消息大概占1KB的情况下,普通队列占用内存是1.2GB,而惰性队列仅仅占用 1.5MB。

消费端限流

假设一个场景,首先,我们 Rabbitmq 服务器积压了有上万条未处理的消息,我们随便打开一个消费者客户端,会出现这样情况: 巨量的消息瞬间全部推送过来,但是我们单个客户端无法同时处理这么多数据!

当数据量特别大的时候,我们对生产端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,我们无法约束生产端,这是用户的行为。所以我们应该对消费端限流,用于保持消费端的稳定,当消息数量激增的时候很有可能造成资源耗尽,以及影响服务的性能,导致系统的卡顿甚至直接崩溃。

TTL

Time To Live,消息过期时间设置

声明队列时,指定即可

TTL:过期时间

  1. 队列统一过期
  2. 消息单独过期

如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准。

​ * 队列过期后,会将队列所有消息全部移除

​ * 消息过期后,只有消息在队列顶端,才会判断其是否过期(移除掉)

死信队列

死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以

被重新发送到另一个交换机,这个交换机就是DLX

消息成为死信的三种情况:

  1. 队列消息长度到达限制;

  2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;

  3. 原队列存在消息过期设置,消息到达超时时间未被消费;

队列绑定死信交换机:

给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key

也就是说此时Queue作为"生产者"

延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费

需求:

  1. 下单后,30分钟未支付,取消订单,回滚库存。

  2. 新用户注册成功7天后,发送短信问候。

实现方式:

  1. 定时器 (×)

  2. 延迟队列 (√)

实现步骤:

在RabbitMQ中并未提供延迟队列功能

替代实现: TTL+死信队列 组合实现延迟队列的效果

设置队列过期时间30分钟,当30分钟过后,消息未被消费,进入死信队列,路由到指定队列,调用库存系统,判断订单状态。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烟雨忆南唐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值