RabbitMQ笔记(交换机,发布确认,延时队列,死信队列,整合SpringBoot)

RabbitMQ

1.1 MQ相关概念

1.1.1 什么是MQ

​ MQ(message queue) ,消息队列,FIFO先入先出,只不过队列中存放的消息是message而已,还是一种跨进程的通信机制,用于上下游传递消息.

​ 在互联网架构当中,MQ是一种非常常见的上下游";逻辑解耦+消息解耦"的消息通信服务.消息的发送上游只需要依赖于MQ.不用依赖于其他服务

1.1.2 MQ的应用

1.流量消锋

举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正
常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。
使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。

image-20220722105539185

2.应用解耦

以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合
调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。
在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。

image-20220722105941506

3.异步处理

有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提供一个 callback api, B 执行完之后调用 api 通知 A 服务。
这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样B 服务也不用做这些操作。A 服务还能及时的得到异步处理成功的消息。

image-20220722134931581

1.1.3 MQ的选择

1.Kafka

Kafka 主要特点是基于Pull 的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输,适合产生大量数据的互联网服务的数据收集业务。大型公司建议可以选用,如果有日志采集功能,肯定是首选 kafka 了。尚硅谷官网 kafka 视频连接http://www.gulixueyuan.com/course/330/tasks

2.RocketMQ

天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况。RoketMQ 在稳定性上可能更值得信赖,这些业务场景在阿里双 11 已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择 RocketMQ。

3.RabbitMQ

结合 erlang 语言本身的并发优势,性能好时效性微秒级社区活跃度也比较高,管理界面用起来十分方便,如果你的数据量没有那么大,中小型公司优先选择功能比较完备的 RabbitMQ。

1.2 RabbitMQ

1.2.1 Rabbit的概念

​ RabbitMq是一个消息中间件,他负责接受并转发消息.

可以把它当做一个快递站点,当你要发送包裹时,你把你的包裹放到快递栈,快递员最终会吧你的包裹送到收件人那里,按照这种逻辑RabbitMq就是一个快递站,一个快递员帮你传递快件,RabbitMq与快递站的主要区别在于他不处理快件而是接受,存储和转发消息数据

image-20220722135854399

1.2.2 四大核心概念

  • 生产者

    产生数据发送消息的程序是生产者

  • 交换机

    交换机是RabbitMQ中非常重要一个部件,一方面他来接受来自生产者的消息,另一方法他将消息推送到队列当中,交换机必须确切的指导如何处理它接受到的消息,`是将这些消息推送到指定队列还是推送到多个队列当中去,亦或是把消息丢失,这个由交换机类型决定

  • 队列

    队列是RabbitMQ内部使用的一种数据结构,尽管消息流经RabbitMq和应用程序,但他们只能存在队列中,队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区,许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列中结束数据.这就是我们使用队列的方式

  • 消费者

    消费和接受具有相同的含义,消费者大多数都是一个等待接受消息的程序,请注意生产者,消费者和消息中间件很多时候不是在同一台机器上,同一个应用程序即可以是生产者也可以是消费者

1.2.3 各个名词介绍

image-20220722141318688

Broker:接收和分发消息的应用,RabbitMQ Server 就是 Message Broker

Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个 vhost,每个用户在自己的 vhost 创建 exchange/queue 等

Connection:publisher/consumer 和 broker 之间的 TCP 连接

Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCPConnection 的开销将是巨大的,效率也较低.Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的

Connection 极大减少了操作系统建立 TCP connection 的开销

Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout(multicast)

Queue:消息最终被送到这里等待 consumer 取走

Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

1.2.2 RabbitMQ安装

使用docker安装

上传到虚拟机中后,使用命令加载镜像即可:

docker load -i mq.tar

在线拉取

docker pull rabbitmq:3-management

执行下面的命令来运行MQ容器:

docker run \
 -e RABBITMQ_DEFAULT_USER=root \
 -e RABBITMQ_DEFAULT_PASS=xing0317. \
 --name mq \
 --hostname mq1 \
 -p 15672:15672 \
 -p 5672:5672 \
 -d \
 rabbitmq:3-management

1.3 RabbitMq应用

1.3.1 编写Hello World

Producer生产者

public class Producer {

    // 队列名称
    public static final String QUEUE_NAME = "hello";

    // 发消息
    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建一个工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 工厂ip,连接rabbitMq队列
        factory.setHost("192.168.37.128");
        // 用户名
        factory.setUsername("root");
        factory.setPassword("xing0317.");

        // 创建连接
        Connection connection = factory.newConnection();
        // 获取信道
        Channel channel = connection.createChannel();
        /*
         * 创建队列
         * 参数含义 1.队列名称 2.队列里的消息是否支持持久化(磁盘) 默认消息存储在内存中
         * 3.改队里是否只提供一个消费者进行消费,是否进行消息共享 TRUE可以多个消费者消费 false只能一个消费者消费
         * 4.是否自动删除 最后一个消费者断开连接后 改队列是否自动删除 TRUE自动删除 false不自动删除
          */
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        String msg = "hello world";
        /*
         * 发送一个消息
         * 1.消息的交换机
         * 2.消息队列的名称(路由的key)
         * 3.其他的参数信息
         * 4.发送消息的消息体
         */
        channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
        System.out.println("消息发送完成");

    }
}

生产者

public class Consumer {

    // 队列的名称
    public static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        // 工厂ip,连接rabbitMq队列
        factory.setHost("192.168.37.128");
        // 用户名
        factory.setUsername("root");
        factory.setPassword("xing0317.");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //声明
        DeliverCallback deliverCallback = (consumerTag,message) ->{
            System.out.println(message);
            System.out.println(message.getBody());
            String msg = new String(message.getBody());
            System.out.println(msg);
        };
        CancelCallback cancelCallback = new CancelCallback() {
            @Override
            public void handle(String s) throws IOException {
                System.out.println(s);
            }
        };


        /**
         * 消费者消费消息
         * 参数含义
         * 1.消费哪个队列
         * 2.消费成功之后时候要自动应答 TRUE代表自动应答 false代表手动应答
         * 3.接受消息时候的回调
         * 4.消费者取消息时候的回调 消费异常 消费中断情况下
         */
        channel.basicConsume(QUEUE_NAME,false,deliverCallback,cancelCallback);
    }
}

1.3.2 Work Queues

工作队列(任务队列)主要是避免立即执行资源紧密任务,而不等待完成.

我们在安排任务在之后执行,我们把任务封装成消息发送其队列,在后台运行的工作进程将弹出任务并最终执行作业.当有多个工作线程时,这些工作线程同时执行这些工作

启动两个工作线程

public class Worker01 {

    /**
     * 队列名称
     */
    public static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        DeliverCallback deliverCallback = (consumerTag,message)->{
            String msg = new String(message.getBody());
            System.out.println("接收到的消息:"+msg);
        };
        CancelCallback cancelCallback = (consumerTag) ->{
            int a = 1+1;
            System.out.println(a);
            System.out.println("消息者取出消费接口回调逻辑"+consumerTag);
        };
        // 消息的接受
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    }
}

消息发送者

/**
 * @author XingLuHeng
 * @date 2022/7/22 21:21)
 * @description 生产者可以发送大量的消息
 */
public class Task01 {
    public static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        /*
         * 创建队列
         * 参数含义 1.队列名称 2.队列里的消息是否支持持久化(磁盘) 默认消息存储在内存中
         * 3.改队里是否只提供一个消费者进行消费,是否进行消息共享 TRUE可以多个消费者消费 false只能一个消费者消费
         * 4.是否自动删除 最后一个消费者断开连接后 改队列是否自动删除 TRUE自动删除 false不自动删除
         */
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        // 控制台中接受信心
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String s = scanner.next();
            /*
             * 发送一个消息
             * 1.消息的交换机
             * 2.消息队列的名称(路由的key)
             * 3.其他的参数信息
             * 4.发送消息的消息体
             */
            channel.basicPublish("",QUEUE_NAME,null,s.getBytes());
        }
    }
}

结果

通过生产者发送4个消息,消费者1,消费者2分别得到两个消息,并且是按照有序的一个一个接受消息

image-20220726175152681

1.3.3 消息应答

​ 消费者完成一个任务需要一段时间,如果其中一个消费者处理一个很长的任务而只完成了部分而挂掉,我们将丢失正在处理的消息.

​ 默认RabbitMq在接受到消息后,立即将消息标位为删除

​ 为了保证消息在发送过程中不丢失,引入消息应答机制;消费者在接受到消息并处理消息完成之后,告诉生产者已经删除,此时将消息删除

默认的自动应答

​ 消息发送成功后立即被认为已经传达成功,这种模式需要在在高吞吐量和传输安全性方面做出权衡

​ 一方面可能因为连接故障或者Channel关闭,就会导致消息的丢失;另一方面可以这种模式消费者那边可以接受大量的消息,没有对消息传递数量进行限制,有可能因为接受过量的消息而来不及进行处理,导致消息的积压,从而导致内存耗尽,消费者线程被操作系统杀死

这种模式石勇勇消费者能够高效并以某种速率能够处理这些消息的情况使用

手动进行消息应答

  • Channel.basicAck(用于肯定应答) 已经知道该消息并且处理成功,可以将其丢弃
  • Channel.basicNack(用于否定回答)
  • Channel.basicReject(用于否定回答)相比于basicNack少于一个参数,不处理改消息直接拒绝可以丢弃

Multiple的解释

手动应答可以批量应答,并减少网络拥堵

channel.basicAck(deliverTag,true)

true:代表批量应答

false:代表只应答头部消息

image-20220726184026789

消息自动重新入队

​ 如果消费者因为某些原因失去连接(其通道关闭,连接关闭),导致消息未发送ACK确认,RabbitMq了解到消息未进行处理,消息将重新入队,如果其他消费者可以处理,将很快分发到另一个消费者,即使有一个消费者死亡,也可以确认消息的不丢失

image-20220726190524788

消息手动应答代码

/**
 * @author XingLuHeng
 * @date 2022/7/23 9:17)
 * @description 消息在手动应答时不丢失放回队列中重新消费
 */
@Slf4j
public class Work02 {
    // 队列名称
    private static final String TASK_QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws Exception {

        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("c1等待接受消息处理的时间较短");
        //消息回调接口
        DeliverCallback callback = (consumerTag,message)->{
            String msg = new String(message.getBody(),"UTF-8");
            // 接受到消息前进行沉睡
            SleepUtils.sleep(1);
            System.out.println("接受到消息:"+msg);
            // 进行手动应答代码
            /*
            * 1.消息的标记 atg
            * 2.消息的批量应答 批量应答可以出现消息的丢失 false不批量应答信道中的消息
            * */
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };
        // 设置不公平分发
        /*
        * 如果是0 轮训
        * 如果是1 不公平分发
        * 其他值 信道预取值
        * */
        channel.basicQos(1);
        //消息异常 取消消息
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag+"消费者取消消费");
        };
        // 采用手动应答
        channel.basicConsume(TASK_QUEUE_NAME, false,callback,cancelCallback);

    }
}

第二个消费者

/**
 * @author XingLuHeng
 * @date 2022/7/23 9:31)
 * @description What To DO
 */
@Slf4j
public class Work03 {
    // 队列名称
    private static final String TASK_QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws Exception {
        Logger log = LoggerFactory.getLogger(Work02.class);
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("c2等待接受消息处理的时间较长");
        //消息回调接口
        DeliverCallback callback = (consumerTag, message) -> {
            String msg = new String(message.getBody(), "UTF-8");
            // 接受到消息前进行沉睡
            SleepUtils.sleep(30);
            System.out.println("接受到消息:"+ msg);
            // 进行手动应答代码
            /*
             * 1.消息的标记 atg
             * 2.消息的批量应答 批量应答可以出现消息的丢失 false不批量应答信道中的消息
             * */
            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
        };
        //消息异常 取消消息
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费");
        };
        // 设置不公平分发
        channel.basicQos(1);
        // 采用手动应答
        channel.basicConsume(TASK_QUEUE_NAME, false, callback, cancelCallback);

    }
}

生产者

/**
 * @author XingLuHeng
 * @date 2022/7/23 9:11)
 * @description 消息在手动应答时不丢失,放回队列中重新消费
 */
@Slf4j
public class Task2 {

    // 队列名称
    private static final String TASK_QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        // 声明队列
        channel.queueDeclare(TASK_QUEUE_NAME,false,false,false,null);
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish("",TASK_QUEUE_NAME,null,message.getBytes("UTF-8"));
            log.info("消费者发出消息:{}成功",message);
        }
    }
}

当我们发送消息给消费者2时,消费者2休眠时间较长,此时我们停掉消费者2线程,也就是说消费者2未执行ACK代码的时候,C2就会被停止掉,此时消息会重新入队,被消费者1进行处理.

1.3.4 RabbitMq持久化

​ 刚才我们保证了在处理任务的时候消息不丢失的情况,但我们如何保证RabbitMQ重启之后消息不丢失,此时就需要对队列和消息进行持久化;

队列如何实现持久化

// 让消息队列持久化
bollean durable = true;
channel.queueDeclare(TASK_QUEUE_NAME,durable,false,false,null);

需要删除之前所创建队列

1.3.5不公平分发

BabbitMq默认是轮训分发,但是在某在场景下这种策略并不是很好,比如说消费者在处理任务,其中有个消费者1消费速度很快,而消费者2消费很慢,这个时候就会造成消费者1很大一段时间处于空闲状态,导致资源的浪费

// 设置不公平分发
/*
        * 如果是0 轮训
        * 如果是1 不公平分发
        * 其他值 信道预取值
        * */
channel.basicQos(1);

1.3.6 发布确认

​ 生产者将通道设置成Confirm模式,一旦通道进入Confirm模式,所有在信道上发布的消息都会指派一个唯一Id,一旦消息被投递在匹配队列中,broker就会发送一个确认给生产者,使得生产者得知消息已经达到了消息队列中

开启发布确认模式

// 开启发布确认
channel.confirmSelect();

发布确认策略

  • 单个发布确认(同步确认发布,发布速度较慢,但能保证每条消息的正确性)

  • 批量发布确认(当出现故障时,无法确认哪条消息)

  • 异步发布确认

        //异步发布确认
        public static void publishMessageAsync() throws Exception {
            Channel channel = RabbitMqUtils.getChannel();
            String queue = UUID.randomUUID().toString();
            // 声明队列
            channel.queueDeclare(queue, true, false, false, null);
            // 开启发布确认
            channel.confirmSelect();
            /**
             * 线程安全有序的哈希表,适用于高并发的场景
             * 1.轻松的将序号与消息进行关联
             * 2.轻松批量的删除条目 只要给到序列号
             * 3.支持并发访问
             */
            ConcurrentSkipListMap<Long, String> outStandingConfirms = new ConcurrentSkipListMap<>();
            //监听成功的消息
            ConfirmCallback ackCallback = (deliveryTag, multiple) -> {
                if (multiple) {
                    //返回的是小于等于当前序号的未确认的消息 是一个map
                    ConcurrentNavigableMap<Long, String> longStringConcurrentNavigableMap = outStandingConfirms.headMap(deliveryTag, true);
                    longStringConcurrentNavigableMap.clear();
                } else {
                    //只清除当前序列号的消息
                    outStandingConfirms.remove(deliveryTag);
                }
                log.info("确认发送的消息{}", deliveryTag);
            };
            //监听失败的消息 1.消息的标记 2.是否为批量确认
    
            ConfirmCallback nackCallback = (deliveryTag, multiple) -> {
                String s = outStandingConfirms.get(deliveryTag);
                log.info("未确认的消息:{},序列号{}", s, deliveryTag);
            };
            // 准备消息的监听器 监听哪些消息成功了 哪些消息失败了
            channel.addConfirmListener(ackCallback, nackCallback);
    
            // 开始时间
            long begin = System.currentTimeMillis();
            for (int i = 0; i < MESSAGE_COUNT; i++) {
                String message = i + "";
                outStandingConfirms.put(channel.getNextPublishSeqNo(), message);
                // 发布确认
                channel.basicPublish("", queue, null, message.getBytes());
            }
    
            // 结束时间
            long end = System.currentTimeMillis();
            log.info("发布{}条消息总耗时{}ms", MESSAGE_COUNT, end - begin);
        }
    }
    

    如何处理异步未处理的消息

    最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,

    比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递。

1.4 交换机

​ RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递传递到了哪些队列中。

​ 相反,生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定。

image-20220726201038819

交换机的类型

  1. 直接交换机(direct)
  2. 主题交换机(topic)
  3. 标题交换机(haeders)
  4. 扇出交换机(fanouts)

绑定(Bindings)

bingding其实就是exchange和queue之间的桥梁,它告诉我们exchange和那个队列进行了绑定关系

image-20220726201342628

1.4.1 扇出交换机

Fanout交换机,就是将接受到的消息广播到他知道的所有队列当中

image-20220727084151316

生产者代码

/**
 * @author XingLuHeng
 * @date 2022/7/23 20:23)
 * @description 发消息 交换机
 */
public class EmitLog {
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes());
        }
    }
}

消费者代码


/**
 * @author XingLuHeng
 * @date 2022/7/23 20:07)
 * @description 消息交换机
 */
@Slf4j
public class ReceiveLog1 {

    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        // 声明一个交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
        // 声明一个队列 生成一个随机队列 队列的名称是随机的 当消费者断开与队列的连接的时候 队列就自动删除
        String queue = channel.queueDeclare().getQueue();
        log.info("获取到名字为{}的临时队列",queue);
        channel.queueBind(queue,EXCHANGE_NAME,"");
        System.out.println("等待接受消息,把接受消息输入到下方屏幕上~~");

        channel.basicConsume(queue,true,(a,b)->{
            log.info("控制台接收到消息{}",new String(b.getBody()));
        }, System.out::println);

    }
}

1.4.2 直接交换机

直接交换机只对它所绑定的交换机的消息感兴趣,绑定使用参数RoutingKey,绑定之后的意义由其交换类型决定

Fanout这种交换类型不能给我们带来很大的灵活性-只能无意识的进行广播,使用Direct交换机,这种交换机的工作方式是:将消息只去它绑定的RoutingKey队列当中去

image-20220727084804458

生产者代码

/**
 * @author XingLuHeng
 * @date 2022/7/24 9:06)
 * @description 直接交换机生产者
 */
@Slf4j
public class EmitLogDirect {

    public static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        // 创建多个bindingKey
        Map<String,String> bindingKeyMap = new HashMap<>();
        bindingKeyMap.put("info","普通info信息");
        bindingKeyMap.put("warn","警告warn信息");
        bindingKeyMap.put("error","错误error信息");
        //debug 没有这个类型的routekey 此消息丢失
        bindingKeyMap.put("debug","调试debug信息");
        Set<Map.Entry<String, String>> entries = bindingKeyMap.entrySet();
        System.out.println(Arrays.toString(entries.toArray()));
        for (Map.Entry<String, String> entry : entries) {
            String bindingKey = entry.getKey();
            String msg = entry.getValue();
            channel.basicPublish(EXCHANGE_NAME,bindingKey,null,msg.getBytes());
            log.info("生产者发出消息:{}",msg);
        }
    }
}

消费者1

/**
 * @author XingLuHeng
 * @date 2022/7/24 8:47)
 * @description 直接交换机消费者1
 */
@Slf4j
public class ReceiveLogsDirect01 {

    public static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        // 声明一个交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        // 声明一个队列
        channel.queueDeclare("console",false,false,false,null);
        // 交换机队列绑定
        channel.queueBind("console",EXCHANGE_NAME,"info");
        channel.queueBind("console",EXCHANGE_NAME,"warn");

        //接受消息
        DeliverCallback callback = (consumerTag,message)->{
            String msg = new String(message.getBody());
            log.info("控制台接受到消息:{}",msg);
        };
        // 消费者取消息时间回调接口
        channel.basicConsume("console",true,callback,consumerTag ->{});
    }
}

消费者2

/**
 * @author XingLuHeng
 * @date 2022/7/24 8:47)
 * @description 直接交换机消费者2
 */
@Slf4j
public class ReceiveLogsDirect02 {

    public static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        // 声明一个交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        // 声明一个队列
        channel.queueDeclare("disk",false,false,false,null);
        // 交换机队列绑定
        channel.queueBind("disk",EXCHANGE_NAME,"error");

        //接受消息
        DeliverCallback callback = (consumerTag,message)->{
            String msg = new String(message.getBody());
            log.info("控制台接受到消息:{}",msg);
        };
        // 消费者取消息时间回调接口
        channel.basicConsume("disk",true,callback,consumerTag ->{});
    }
}

1.4.3 Topic交换机

刚才使用了直接交换机,从而实现了选择性的接受日志,但仍有其局限性.如果我们想接受info.base和info.advantage的消息,只能添加两个RoutingKey.这时候就需要用到Topic交换机

Topic交换的消息需要满足一定的要求

  1. 是一个单词列表,以点号分隔开
  2. *号可以代替一个单词
  3. #可以代替零个或多个单词

案例

image-20220727090140410

主题交换机生产者代码

/**
 * @author XingLuHeng
 * @date 2022/7/24 10:07)
 * @description 主题交换机生产者代码
 */
@Slf4j
public class EmitLogTopic {
    public static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        Map<String, String> bindingKeyMap = new HashMap<>();
        bindingKeyMap.put("quick.orange.rabbit", "被队列q1,q2接收到");
        bindingKeyMap.put("lazy.orange.elephant", "被队列q1,q2接收到");
        bindingKeyMap.put("quick.orange.fox", "被队列q1接受到");
        bindingKeyMap.put("lazy.pink.rabbit", "满足两个绑定队列但只能被q2队列接受一次");
        bindingKeyMap.put("quick.brown,fo  x", "不满足任何队列会被丢弃");
        for (Map.Entry<String, String> entry : bindingKeyMap.entrySet()) {
            String bingKey = entry.getKey();
            String msg = entry.getValue();
            channel.basicPublish(EXCHANGE_NAME, bingKey, null, msg.getBytes());
            log.info("生产者发送消息{}成功", msg);
        }

    }
}

主题交换机消费者1代码

/**
 * @author XingLuHeng
 * @date 2022/7/24 10:20)
 * @description 主题交换消费者代码
 */
@Slf4j
public class ReceiveLogsTopic01 {
    public static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        String queue = "q1";
        channel.queueDeclare(queue,false,false,false,null);
        channel.queueBind(queue,EXCHANGE_NAME,"*.orange.*");
        log.info("开始接受消息");
        DeliverCallback deliverCallback = (consumerTag,message) ->{
            String msg = new String(message.getBody());
            String routingKey = message.getEnvelope().getRoutingKey();
            log.info("接受到队列{},绑定键{}的消息:{}",queue,routingKey,msg);
        };
        channel.basicConsume(queue,true,deliverCallback,(a)->{});

    }
}

主题交换机消费者2代码

/**
 * @author XingLuHeng
 * @date 2022/7/24 10:20)
 * @description What To DO
 */
@Slf4j
public class ReceiveLogsTopic02 {
    public static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        String queue = "q1";
        channel.queueDeclare(queue,false,false,false,null);
        channel.queueBind(queue,EXCHANGE_NAME,"*.*.rabbit");
        channel.queueBind(queue,EXCHANGE_NAME,"lazy.#");
        log.info("开始接受消息");
        DeliverCallback deliverCallback = (consumerTag,message) ->{
            String msg = new String(message.getBody());
            String routingKey = message.getEnvelope().getRoutingKey();
            log.info("接受到队列{},绑定键{}的消息:{}",queue,routingKey,msg);
        };
        channel.basicConsume(queue,true,deliverCallback,(a)->{});
    }
}

1.5 死信队列

​ 死信,顾名思义就是无法消费的消息;一般来说producer将消息投递给broker或者直接到queue里,Consumer从queue取出消息进行消费,但某些时候由于特定的原因到这queue中i的某些消息无法被消费,这样的消息没有得到处理,就成为了死信,有了死信也就有了死信队列

应用场景

​ 为了保证订单业务的消息数据不丢失,需要使用到RabbitMq的死信队列机制,当消息消费发生异常,将消息投入到死信队列中去.

​ 用户在下单成功点击支付,当在指定时间未支付时自动失效

1.5.1死信的来源

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

image-20220727091441159

time to live时间过期

  1. 启动消费者1,创建普通队列的绑定和交换机等的实例
  2. 关闭消费者1(模拟其接受不到消息),启动生产者
  3. 启动消费者2,消费死信队列中的消息

队列达到最大长度

  1. 设置普通队列的最大容量参数(需要删除之前的队列),启动消费者1
  2. 启动生产者,发送超过容量的消息
  3. 消费者c2接收到消息,进行消费
//设置队列最大长度限制
map.put("x-max-length",6);

消息被拒

channel.basicReject(message.getEnvelope().getDeliveryTag(),false);

1.5.2死信队列代码

生产者代码

/**
 * @author XingLuHeng
 * @date 2022/7/24 14:17)
 * @description 死信队列生产者代码
 */
public class Producer {
    private static Logger logger = LoggerFactory.getLogger(Producer.class);
    // 普通交换机
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    // 普通队列的名称
    private static final String NORMAL_QUEUE = "normal_queue";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(NORMAL_EXCHANGE,BuiltinExchangeType.DIRECT);
        // 死信消息 设置TTL时间 time to live
        // 死信消息 设置过期时间 单位是ms
        // BasicProperties properties =
        // new AMQP.BasicProperties().builder().expiration("10000").build();
        for (int i = 1; i < 11; i++) {
            String msg = "info"+i;            channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",null,msg.getBytes());
            logger.info("消息发送完成");
        }
        logger.info("消息发送完成");
    }
}

消费者1


/**
 * @author XingLuHeng
 * @date 2022/7/24 13:50)
 * @description 死信队列实战 消费者1
 */
public class Consumer01 {

    private static final Logger log = LoggerFactory.getLogger(Consumer01.class);
    // 普通交换机
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    // 死信交换机的名称
    private static final String DEAD_EXCHANGE = "dead_exchange";
    // 普通队列的名称
    private static final String NORMAL_QUEUE = "normal_queue";
    // 死信队列的名称
    private static final String DEAD_QUQUE = "dead_queue";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        // 声明死信和普通交换机 交换机类型为direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        //声明普通队列
        // 需要准备一个参数 来将死信转发到死信交换机
        Map<String,Object> map = new HashMap<>();
        //设置死信交换机
        map.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        //设置死信路由
        map.put("x-dead-letter-routing-key", "lisi");
        //设置队列最大长度限制
        map.put("x-max-length",6);
        //声明普通队列
        channel.queueDeclare(NORMAL_QUEUE,false,false,false,map);
        channel.queueDeclare(DEAD_QUQUE,false,false,false,null);

        //将队列和交换机进行绑定
        channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");
        channel.queueBind(DEAD_QUQUE,DEAD_EXCHANGE,"lisi");
        log.info("等待接受消息~~~");

        DeliverCallback callback = (consumerTag,message)->{
            String msg = new String(message.getBody());
            String routingKey = message.getEnvelope().getRoutingKey();
            if (msg.equals("info5")){
                log.info("Conusmer01接受的消息是{},此消息是拒绝的",msg);
                channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
            }else {
                log.info("接受到队列{},绑定键{}的消息:{}",NORMAL_QUEUE,routingKey,msg);
                channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
            }
        };
        channel.basicConsume(NORMAL_QUEUE,false,callback,(message)->{});
    }
}

消费者2

/**
 * @author XingLuHeng
 * @date 2022/7/24 14:48)
 * @description What To DO
 */
public class Consumer02 {
    // 死信队列的名称
    private static final String DEAD_QUQUE = "dead_queue";
    private static final Logger log = LoggerFactory.getLogger(Consumer02.class);
    private static final String DEAD_EXCHANGE = "dead_exchange";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        channel.queueDeclare(DEAD_QUQUE,false,false,false,null);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.queueBind(DEAD_QUQUE,DEAD_EXCHANGE,"lisi");
        log.info("等待接受消息~~~");
        DeliverCallback callback = (consumerTag, message)->{
            String msg = new String(message.getBody());
            String routingKey = message.getEnvelope().getRoutingKey();
            log.info("接受到队列{},绑定键{}的消息:{}",DEAD_QUQUE,routingKey,msg);
        };
        channel.basicConsume(DEAD_QUQUE,true,callback,(message)->{});
    }
}

1.6 延迟队列

​ 延迟队列,队列内部是有序的,最重要的特性就是体现在他延时的属性上,延时队列中的元素是希望到了指定时间之后取出和处理

延时队列就是用来存放需要在指定时间被处理的元素的队列

延时队列使用场景

  • 订单在十分内未支付自动取消
  • 新创建的店铺,如果在十天内都没有上传过商品,则自动发消息提醒
  • 用户注册后三天没有登录,进行短信提醒
  • 用户退款后,如果三天没有得到处理通知相关运营人员
  • 预定会议成功后,需要在预定时间点前十分钟通知各个会议人员参加

为什么不采取定时任务?

当数据量较大的时候,使用检索数据会导致性能降低,而且时效性较弱.如果时效性不是很强,而是宽松意义上的一周,那么使用定时任务也可以,但如果在活动期间百万的订单,使用轮训检索的方式是不可取,无法完成订单的检查,会给数据库带来巨大的压力,无法满级业务要求而性能低下.

设置过期时间的两种方式

  • 消息设置TTL

    rabbitTemplate.convertAndSend("X","XC",msg, message -> {
                message.getMessageProperties().setExpiration(ttlTime);
                return message;
            });
    
  • 队列设置TTL

    //声明队列的TTL
    map.put("x-message-ttl",40000);
    

1.6.1 队列TTL

创建两个队列QA和QB,两个队列分别设置为10s和40s,然后创建一个交换机x和死信队列交换机y,类型为direct,创建一个死信队列QD

image-20220727095506531

交换机队列声明代码

/**
 * @author XingLuHeng
 * @date 2022/7/24 18:19)
 * @description 配置文件类代码
 */
@Configuration
public class TtlQueueConfig {
    //普通交换机名称
    public static final String X_EXCHANGE = "X";
    //死信交换机缓存
    public static final String Y_DEAD_LATTER_QUEUE = "Y";
    //普通队列名称
    public static final String QUEUE_A = "QA";
    public static final String QUEUE_B = "QB";
    public static final String QUEUE_C = "QC";
    //死信交换机名称
    public static final String DEAD_LATTER_QUEUE = "QD";

    //声明exchange
    @Bean("xExchange")
    public DirectExchange xExchange(){
        return new DirectExchange(X_EXCHANGE);
    }

    @Bean("yExchange")
    public DirectExchange yExchange(){
        return new DirectExchange(Y_DEAD_LATTER_QUEUE);
    }

    //声明队列A ttl 10s 并绑定对应的死信队列
    @Bean("queueA")
    public Queue queueA(){
        Map<String, Object> map = new HashMap<>();
        //声明当前队列绑定的死信交换机
        map.put("x-dead-letter-exchange",Y_DEAD_LATTER_QUEUE);
        //声明当前队列的死信路由 key
        map.put("x-dead-letter-routing-key","YD");
        //声明队列的TTL
        map.put("x-message-ttl",10000);
        return QueueBuilder.durable(QUEUE_A).withArguments(map).build();
    }

    // 声明队列a绑定x交换机
    @Bean
    public Binding queueBindingA(@Qualifier("queueA") Queue queueA, @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }

    //声明队列B ttl 40s 并绑定对应的死信队列
    @Bean("queueB")
    public Queue queueB(){
        Map<String, Object> map = new HashMap<>();
        //声明当前队列绑定的死信交换机
        map.put("x-dead-letter-exchange",Y_DEAD_LATTER_QUEUE);
        //声明当前队列的死信路由 key
        map.put("x-dead-letter-routing-key","YD");
        //声明队列的TTL
        map.put("x-message-ttl",40000);
        return QueueBuilder.durable(QUEUE_B).withArguments(map).build();
    }

    @Bean
    public Binding queueBindB(@Qualifier("queueB")Queue queueB,@Qualifier("xExchange")DirectExchange exchange){
        return  BindingBuilder.bind(queueB).to(exchange).with("XB");
    }

    @Bean("queueD")
    public Queue queueD(){
        return new Queue(DEAD_LATTER_QUEUE);
    }
    @Bean
    public Binding deadLetterBindingQAD(@Qualifier("queueD") Queue queueD,
                                        @Qualifier("yExchange") DirectExchange yExchange){
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }

}

生产者代码

@Slf4j
@RestController
@RequestMapping("/ttl")
@Api(tags = "发送消息",description = "使用RabbitMq发送消息")
public class SendMsgController {

    @Resource
    private RabbitTemplate rabbitTemplate;
    private String message;

    @RequestMapping("/sendMsg/{msg}")
    public void sendMsg(@PathVariable String msg){
        log.info("当前时间{},发送一条消息给两个TTL队列:{}",new Date().toString(),msg);
        rabbitTemplate.convertAndSend("X","XA","消息来自于ttl为10s的队列"+msg);
        rabbitTemplate.convertAndSend("X","XB","消息来自于ttl为40s的队列"+msg);
        log.info("消息发送完成");
    }
}

消费者代码

@Slf4j
@Component
public class DeadLetterQueueConsumer {

    //接受消息
    @RabbitListener(queues = "QD")
    public void receive(Message message, Channel channel){
        String msg = new String(message.getBody());
        log.info("当前时间{},收到死信队列的消息:{}",new Date().toString(),msg);
    }
}

发送请求,进行测试,第一条消息在10s后变成了死信队列被消费,第二条消息在40s后称为了死信

但如果我们每增加一个时间需求,都需要新增加一个队列嘛!这样肯定是不合理的

1.6.2 延时队列优化

新增加一个队列Qc,这里对队列不设置TTL时间

image-20220727100612530

配置文件代码

   @Bean("queueC")
    public Queue queueC(){
        Map<String, Object> map = new HashMap<>();
        //声明当前队列绑定的死信交换机
        map.put("x-dead-letter-exchange",Y_DEAD_LATTER_QUEUE);
        //声明当前队列的死信路由 key
        map.put("x-dead-letter-routing-key","YD");
        //没有声明TTL属性
        return QueueBuilder.durable(QUEUE_C).withArguments(map).build();
    }

    // queueC 与交换机绑定
    // 声明队列a绑定x交换机
    @Bean
    public Binding queueBindingC(@Qualifier("queueC") Queue queueC, @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueC).to(xExchange).with("XC");
    }

消息生产者代码

@RequestMapping("/sendExpirationMsg/{msg}/{ttlTime}")
public void  sendMsg(@PathVariable String msg,@PathVariable String ttlTime){
    log.info("当前时间{},发送一条消息时长为{}毫秒给队列QC :{}",new Date().toString(),ttlTime,msg);
    rabbitTemplate.convertAndSend("X","XC",msg, message -> {
        message.getMessageProperties().setExpiration(ttlTime);
        return message;
    });
}

发送请求进行测试

http://localhost:8080/ttl/sendExpirationMsg/你好 1/20000

http://localhost:8080/ttl/sendExpirationMsg/你好 2/2000

我们查看结果,RabbitMq只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息延时时间很长,第二个消息延时时长很短,第二个消息并不会优先得到执行

image-20220727101419651

1.6.3 RabbitMQ插件实现延迟队列

我们使用插件之后,会出现新的一种交换机类型,这种类型支付延迟投递机制,消息传递后并不会立即投递到目标队列当中,而是存储在mnesia(分布式系统)表中,当到达投递时间后,才会投递到目标队列中

配置文件代码

/**
 * @author XingLuHeng
 * @date 2022/7/25 14:18)
 * @description 插件实现延迟队列交换机代码
 */
@Configuration
public class DelayedQueueConfig {

    //队列
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    //交换机
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    //routingKey
    public static final String DEALYED_ROUTING_KEY = "delayed.routingkey";

    //声明队列
    @Bean
    public Queue delayedQueue(){
        return new Queue(DELAYED_QUEUE_NAME);
    }

    //声明插件交换机
    @Bean
    public CustomExchange delayedExchange(){
        /**
         * 1.交换机的名称
         * 2.交换机的类型
         * 3.是否需要持久化
         * 4.是否需要自动删除
         * 5.其他的参数
         */
        Map<String,Object> map = new HashMap<>();
        map.put("x-delayed-type","direct");

        return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,map);
    }

    //延迟队列绑定到延迟交换机
    @Bean
    public Binding delayedQueueBindingDelayExchange(@Qualifier("delayedQueue") Queue queue,
                                                    @Qualifier("delayedExchange") CustomExchange delayedExchange){
        return BindingBuilder.bind(queue).to(delayedExchange).with(DEALYED_ROUTING_KEY).noargs();
    }
}

消息生产者代码

 /**
     * 基于插件发送延迟消息
     * @param msg 消息内容
     * @param ttlTime 延迟时间
     */
    @RequestMapping("/sendDelayMsg/{message}/{ttlTime}")
    public void  sendMsg(@PathVariable("message") String msg,@PathVariable Integer ttlTime){
        log.info("当前时间{},发送一条消息时长为{}毫秒给队列QC :{}",new Date().toString(),ttlTime,msg);
        rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME,
                DelayedQueueConfig.DEALYED_ROUTING_KEY, msg,message->{
                    message.getMessageProperties().setDelay(ttlTime);
                    return message;
                });

    }

消费者代码

 //接受插件延迟消息
    @RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
    public void receive1(Message message,Channel channel){
        String msg = new String(message.getBody());
        log.info("当前时间{},收到延迟队列的消息:{}",new Date().toString(),msg);
    }

测试结果,符合我们的预期

image-20220727102715136

1.7 发布确认

交换机确认

实现RabbitTemplate.ConfirmCallback接口
  @Autowired
    private RabbitTemplate rabbitTemplate;
    //注入
    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
    } 


/**
     * 交换机确认回调方法
     * @param correlationData 保存这回调消息的ID及其相关信息
     * @param ack 交换机收到消息返回true 未收到消息 false
     * @param cause 成功返回null 未成功返回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);
        }
    }

消息队列确认

在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。

实现RabbitTemplate.ReturnCallback接口
    
 /**
     * 只有当消息在传递过程中不可达目的地时将消息返回给生产者
     * @param message 消息
     * @param replyCode 失败码
     * @param replyText 失败原因
     * @param exchange  交换机名称
     * @param routingKey 路由Key
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.info("消息{},被交换机{}退回,退回原因{},路由key{}",message.getBody(),exchange,replyText,routingKey);
    }   

交换机备份

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

image-20220727151959445

备份交换机绑定

@Bean("confirmExchange")
public DirectExchange confirmExchange(){
    return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE).durable(true)
        .withArgument("alternate-exchange",BACKUP_EXCHANGE).build();
}

现RabbitTemplate.ReturnCallback接口

/**
* 只有当消息在传递过程中不可达目的地时将消息返回给生产者
* @param message 消息
* @param replyCode 失败码
* @param replyText 失败原因
* @param exchange 交换机名称
* @param routingKey 路由Key
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info(“消息{},被交换机{}退回,退回原因{},路由key{}”,message.getBody(),exchange,replyText,routingKey);
}


**交换机备份**

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

在这里插入图片描述

备份交换机绑定

@Bean("confirmExchange")
public DirectExchange confirmExchange(){
    return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE).durable(true)
        .withArgument("alternate-exchange",BACKUP_EXCHANGE).build();
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值