C1-RabbitMQ(消息队列)--- 分解 C 2021年8月8日 20:42:34

分布式框架中间件总纲

https://www.jianshu.com/p/00aa796bb5b8

友情链接(消息三解序)

1、RabbitMQ(消息队列)--- 分解 A
2、RabbitMQ(消息队列)--- 分解 B
3、RabbitMQ(消息队列)--- 分解 C
4、RabbitMQ(消息队列)--- 面试题

本章目录

一、发布确认高级
       1、发布确认 springboot 版本
       2、回退消息
       3、备份交换机
二、RabbitMQ 其他知识点
       1、幂等性
       2、优先级队列
       3、惰性队列
三、RabbitMQ 集群
       1、clustering
       2、镜像队列
       3、Haproxy+Keepalive 实现高可用负载均衡
       4、Federation Exchange
       5、Federation Queue
       6、Shovel

一、发布确认高级

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

1、发布确认 springboot 版本

1、消息确认机制方案


image.png

2、代码架构图


image.png

3、配置文件

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

spring.rabbitmq.host=106.52.23.202
spring.rabbitmq.port=5672
spring.rabbitmq.username=mykk
spring.rabbitmq.password=abc666
spring.rabbitmq.publisher-confirm-type=correlated #开启回调

4、配置类

package com.rabbit.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@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 CONFIRM_ROUTING_KEY = "key1";


    // 声明交换机
    @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("confirmExchange")DirectExchange confirmExchange,
                                @Qualifier("confirmQueue")Queue queue){
        return  BindingBuilder.bind (queue).to (confirmExchange).with (CONFIRM_ROUTING_KEY);
    }
}

5、回调接口

package com.rabbit.callback;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

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

    /**
     * 交换机无论是否接受消息都会进入回调
     *
     * CorrelationData 消息相关数据
     * ack             交换机是否接受消息
     * cause ack 为 true 成功返回null ,akc 为 false 失败 返回原因
     */
    @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、消息生产者

package com.rabbit.controller;

import com.rabbit.callback.MyCallback;
import com.rabbit.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;

@Slf4j
@RestController
@RequestMapping("/confirm")
public class ProducerController {
    @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 (ConfirmConfig.CONFIRM_EXCHANGE_NAME, routingKey, message, correlationData1);
        log.info ("发送消息内容:{}", message);

        // 模拟发不出去(发送的交换机名字 改错)
        rabbitTemplate.convertAndSend ("123"+ConfirmConfig.CONFIRM_EXCHANGE_NAME, routingKey, message, correlationData1);
        log.info ("发送消息内容:{},测试交换机问题", message);
        // 默认队列出问题
        CorrelationData correlationData2 = new CorrelationData ("2");
        routingKey = "key2";// 改为一个没有绑定过的key2
        rabbitTemplate.convertAndSend (ConfirmConfig.CONFIRM_EXCHANGE_NAME, routingKey, message , correlationData2);

        log.info ("发送消息内容:{},测试队列问题", message);

    }
}

image.png

7、消息消费者

package com.rabbit.consumer;

import com.rabbit.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ConfirmConsumer {

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

8、结果分析


image.png

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

2、回退消息

1、Mandatory 参数
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者(简单来说就是解决上面的routingKey错误,导致队列发送的消息被丢弃,解决的就是队列发送的消息不被丢弃而是回退,有点类似事务的回滚)

2、回调接口(改写:初始化主要在这里)

package com.rabbit.callback;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

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

    @Autowired
    private RabbitTemplate rabbitTemplate;

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

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



    /**
     * 交换机无论是否接受消息都会进入回调
     * 

* CorrelationData 消息相关数据 * ack 交换机是否接受消息 * cause ack 为 true 成功返回null ,akc 为 false 失败 返回原因 */ @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); } } }

3、消息生产者代码(改写:取消注入回调,不在此处初始化)

package com.rabbit.controller;

import com.rabbit.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/confirm")
public class ProducerController {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message) {
        // 指定消息的id 为 1
        CorrelationData correlationData1 = new CorrelationData ("1");
        String routingKey = "key1";
        rabbitTemplate.convertAndSend (ConfirmConfig.CONFIRM_EXCHANGE_NAME, routingKey, message, correlationData1);
        log.info ("发送消息内容:{}", message);

        // 模拟发不出去(发送的交换机名字 改错)
//        rabbitTemplate.convertAndSend ("123"+ConfirmConfig.CONFIRM_EXCHANGE_NAME, routingKey, message, correlationData1);
//        log.info ("发送消息内容:{},测试交换机问题", message);

        // 默认队列出问题
        CorrelationData correlationData2 = new CorrelationData ("2");
        routingKey = "key2";// 改为一个没有绑定过的key2
        rabbitTemplate.convertAndSend (ConfirmConfig.CONFIRM_EXCHANGE_NAME, routingKey, message , correlationData2);

        log.info ("发送消息内容:{},测试队列问题", message);

    }
}

4、配置文件

spring.rabbitmq.host=106.52.23.202
spring.rabbitmq.port=5672
spring.rabbitmq.username=mykk
spring.rabbitmq.password=aaa666
#回调开启
spring.rabbitmq.publisher-confirm-type=correlated
#回退开启
spring.rabbitmq.publisher-returns=true

5、结果分析:捕获到队列不可路由的数据(消息)


image.png
3、备份交换机

个人小结:上面提到了交换机出问题,就无法接受到消息,通过回调可以检测到,但是需要重新发送消息,可以通过备份交换机的队列去发送。

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

1、代码架构图(已下代码根据此图编写)


image.png

2、修改配置

package com.rabbit.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ConfirmConfigBack {

    // 交换机
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    // 队列
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    // routingKey
    public static final String CONFIRM_ROUTING_KEY = "key1";
    // 备份交换机
    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
    public FanoutExchange backupExchange(){
        return new FanoutExchange (BACKUP_EXCHANGE_NAME);
    }

    // 声明备份队列
    @Bean
    public Queue backupQueue(){
        return QueueBuilder.durable (BACKUP_QUEUE_NAME).build ();
    }
    // 声明报警队列
    @Bean
    public Queue warningQueue(){
        return QueueBuilder.durable (WARNING_QUEUE_NAME).build ();
    }

    // 备份队列绑定备份交换机
    @Bean
    public Binding backupQueueBindingBackupExchange(@Qualifier("backupExchange")FanoutExchange backupExchange,
                                @Qualifier("backupQueue")Queue backupQueue){
        return  BindingBuilder.bind (backupQueue).to (backupExchange);// 无绑定 routingKey,扇出(广播)类型没有必要有
    }

    // 报警队列绑定备份交换机
    @Bean
    public Binding warningQueueBindingBackupExchange(@Qualifier("backupExchange")FanoutExchange backupExchange,
                                @Qualifier("warningQueue")Queue warningQueue){
        return  BindingBuilder.bind (warningQueue).to (backupExchange);// 无绑定 routingKey,扇出(广播)类型没有必要有
    }





    // 声明交换机(改写:若无法投递需要转发到备份交换机上面)
    @Bean("confirmExchange")
    public DirectExchange confirmExchange(){
        //return new DirectExchange (CONFIRM_EXCHANGE_NAME);
        // 代码意思:直接交换机 confirm.exchange ,持久化,并且无法投递的时候转发到备份交换机
        return ExchangeBuilder.directExchange (CONFIRM_EXCHANGE_NAME).durable (true)
                .withArgument ("alternate-exchange",BACKUP_EXCHANGE_NAME).build ();
    }

    // ------------------------------以上为核心扩展部分--------------------------------------------------

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

    // 绑定
    @Bean
    public Binding queueBinding(@Qualifier("confirmExchange")DirectExchange confirmExchange,
                                @Qualifier("confirmQueue")Queue queue){
        return  BindingBuilder.bind (queue).to (confirmExchange).with (CONFIRM_ROUTING_KEY);
    }
}

3、报警消费者

package com.rabbit.consumer;

import com.rabbit.config.ConfirmConfigBack;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

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

4、注意事项:需要删除掉之前定义好的交换机(confrim.exchange)因为配置关系已经变了


image.png

5、输出结果


image.png

mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,备份交换机优先级高。

二、RabbitMQ 其他知识点

1、幂等性

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

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

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

4、消费端的幂等性保障
在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,
这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:a.
唯一 ID+指纹码机制,利用数据库主键去重, b.利用 redis 的原子性去实现

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

6、Redis原子性(最佳方案)
利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费

2、优先级队列

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

2、如何添加
方式一:客户端操作


image.png

方式二:代码控制,在队列层面

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

方式三:代码控制,在消息层面

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

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

3、惰性队列

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

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

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

如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。
在队列声明的时候可以通过“x-queue-mode”参数来设置队列的模式,取值为“default”和“lazy”。下面示
例中演示了一个惰性队列的声明细节:

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

3、内存开销问题


image.png

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

三、RabbitMQ 集群(因没机子没硬盘搞集群,暂更新)

1、clustering
2、镜像队列
3、Haproxy+Keepalive 实现高可用负载均衡
4、Federation Exchange
5、Federation Queue
6、Shovel

四、遇到的问题

1、rabbitmq宕机:Management API returned status code 500


image.png

重启命令不是一下博客的:因为有改过
rabbitmqctl start_app,其他参考以下
解决方案:
https://blog.csdn.net/qq_38890137/article/details/104403234

2、补充:查看liunx剩余内存:free -h

image.png

五、总结了几点

1、交换机出问题可以通过回调,但是队列无法区分;所以队列无法路由只能通过回退操作。

2、
image.png

你可能感兴趣的:(C1-RabbitMQ(消息队列)--- 分解 C 2021年8月8日 20:42:34)