消息中间件 RabbitMQ 之 交换机类型及其详解

5. 交换机

​ 在上一节中,我们创建了一个工作队列。假设工作队列背后,每个人物都恰好交付给一个消费者(工作进程)。在这一部分中,会将消息传达给多个消费者,这种模式被称为 “发布/订阅”。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ob9ftq4O-1650695548328)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220419095058870.png)]

5.1 Exchanges


5.1.1 Exchanges 概念


  • RabbitMQ 消息传递模型的核心思想是:生产者生产的消息从来不会直接发送到队列。实际上,通常生产者都不知道这些消息发送到了哪些队列中。
  • 相反,生产者只能将消息发送到交换机(exchange)。交换机的工作内容非常简单,一方面它接受来自生产者的消息,另一方面将这些消息推入队列。
  • 交换机必须知道如何处理收到的消息。是应该把这些消息放入特定队列还是说放到许多队列,或者直接丢弃,这就由交换机的类型决定。

5.1.2 Exchanges 的类型


  • 总共有以下类型:

    直接(direct)->路由类型,主题(topic),标题(headers)->头类型, 扇出(fanout)->发布/订阅类型

5.1.3 无名 Exchange


在之前我们对 Exchange 一无所知,但仍然能够将消息发送到队列。之前能够实现的原因是使用的是默认交换,通过空字符串(“ ”)进行标识。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HQM3915K-1650695403493)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220419101152595.png)]

第一个参数是交换机的名称。空字符串表示默认或者无名交换机:消息能路由发送到队列中其实是由 routingKey(bindingKey)实现的,如果它存在的话。

5.2 临时队列


  • 队列的名称对我们来说至关重要,需要指定消费者去消费哪个队列的消息。

每当连接 RabbitMQ 时,都需要一个全新的空队列,为此可以创建一个具有随机名称的队列,或者让服务器随机选一个队列名称。其次一旦我们断开了消费者的连接,队列将被自动删除。

  • 创建临时队列的方式如下:

    String queue = channel.queueDeclare().getQueue();
    
  • 创建出来的队列是这样的:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tst42JYU-1650695403494)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220419102914272.png)]

5.3 绑定(binding)


绑定是 exchange 和 queue 之间的桥梁,它告诉我们 exchange 和哪个队列进行了绑定关系。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HmFr2JhA-1650695403495)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220419103433933.png)]

5.4 Fanout 扇出模式(发布/订阅)


5.4.1 Fanout 介绍


Fanout 会将接收到的所有消息广播到它知道的所有队列中。

5.4.2 Fanout 实战


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G7mrY8pN-1650695403496)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220419105527976.png)]

logs(交换机名称)和临时队列的绑定关系如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iC1R0ori-1650695403501)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220419105756379.png)]

编写两个消费者,一个生产者,采用扇出模式,查看具体情况

消费者 ReceiveLog01:

package com.example.five;

import com.example.utils.RabbitUtils;
import com.example.utils.SleepUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author 且听风吟
 * @version 1.0
 * @description: 消息接收
 * @date 2022/4/19 0019 11:02
 */
public class ReceiveLog01 {

    public static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        //获取信道
        Channel channel = RabbitUtils.getChannel();
        //声名一个交换机 参数 1:交换机名称 2:交换机类型
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
        /**
         * 声名一个临时队列 队列的名称是随机的
         * 当消费者断开与队列的连接的时候,队列就会自动被删除
         */
        String queue = channel.queueDeclare().getQueue();
        //绑定交换机与队列 参数:1.队列名称 2.交换机名称 3.routingKey 此处为空字符串
        channel.queueBind(queue,EXCHANGE_NAME,"");
        System.out.println("ReceiveLog01等待接收消息,把接收到的消息打印在屏幕上···");

        //消费者接收消息的回调
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("ReceiveLog01接收到的消息:"+new String(message.getBody(),"UTF-8"));
        };
        //消费者取消消息的回调
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println("消息消费被中断");
        };

        /**
         * 消费者消费(接收)消息,参数含义如下:
         *    1.消费哪个队列
         *    2.消费之后是否要自动应答
         *    3.消费者成功消费消息的回调
         *    4.消费者取消消费消息的回调
         */
        channel.basicConsume(queue,true,deliverCallback,cancelCallback);
    }
}

消费者 ReceiveLog02:

package com.example.five;

import com.example.utils.RabbitUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author 且听风吟
 * @version 1.0
 * @description: 消息接收
 * @date 2022/4/19 0019 11:02
 */
public class ReceiveLog02 {

    public static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        //获取信道
        Channel channel = RabbitUtils.getChannel();
        //声名一个交换机 参数 1:交换机名称 2:交换机类型
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
        /**
         * 声名一个临时队列 队列的名称是随机的
         * 当消费者断开与队列的连接的时候,队列就会自动被删除
         */
        String queue = channel.queueDeclare().getQueue();
        //绑定交换机与队列 参数:1.队列名称 2.交换机名称 3.routingKey 此处为空字符串
        channel.queueBind(queue,EXCHANGE_NAME,"");
        System.out.println("ReceiveLog02等待接收消息,把接收到的消息打印在屏幕上···");

        //消费者接收消息的回调
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("ReceiveLog02接收到的消息:"+new String(message.getBody(),"UTF-8"));
        };
        //消费者取消消息的回调
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println("消息消费被中断");
        };

        /**
         * 消费者消费(接收)消息,参数含义如下:
         *    1.消费哪个队列
         *    2.消费之后是否要自动应答
         *    3.消费者成功消费消息的回调
         *    4.消费者取消消费消息的回调
         */
        channel.basicConsume(queue,true,deliverCallback,cancelCallback);
    }
}

生产者 EmitLog 发消息给交换机:

package com.example.five;

import com.example.utils.RabbitUtils;
import com.rabbitmq.client.Channel;

import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;

/**
 * @author 且听风吟
 * @version 1.0
 * @description: 发消息给交换机
 * @date 2022/4/19 0019 12:15
 */
public class EmitLog {

    public static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        //获取信道
        Channel channel = RabbitUtils.getChannel();
        //声名交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");

        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNext()){
            String message = scanner.next();
            /**
             * 发送一个消息,参数含义如下:
             *    1.发送到哪个交换机,null表示使用默认交换机
             *    2.路由的key值是哪个 本次key=""
             *    3.其他参数信息
             *    4.发送消息的消息体(发送消息的二进制码)
             */
            channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes("UTF-8"));
        }
    }
}
  1. 生产者向交换机发送 11、22、33、44 这几条消息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sb0HypaF-1650695403502)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220419124536254.png)]

  2. 观察两个消费者接收消息的情况如下,它们都接收到了来自生产者的消息:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NNgv6TOA-1650695403502)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220419124644875.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VPArQmac-1650695403503)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220419124720468.png)]

综上所述,在 Fanout 模式下,交换机 logs 将消息发送给了它知道的所有队列(两个临时队列queue)。

可以得知,Fanout 模式会将接收到的所有消息广播到它知道的所有队列中。

5.5 Direct exchange 直接交换机(路由模式)


5.5.1 回顾


  • 在上一节中,构建了一个简单的日志记录系统。能够向众多的接收者广播消息。
  • 在本节我们将添加一些特别的功能,比如只向某个消费者发布部分消息,例如只把严重错误消息定向存储到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。

绑定是交换机和队列之间的桥梁关系,也可以这么理解:队列只对它绑定的交换机的消息感兴趣。绑定用参数 routingKey 来表示,也可以称该参数为 bindingKey,绑定之后的意义由其交换类型决定。

5.5.2 Direct exchange 介绍


Fanout 这种交换机类型并不能给我们带来很大的灵活性——它只能进行无意识的广播,在这里我们将使用 direct 这种类型来进行替换,这种类型的工作方式是,消息只到交换机绑定的 routingKey 队列中去。

5.5.3 多重绑定


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rqa7eBMe-1650695403504)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220421220609529.png)]

当然如果exchange的绑定类型是 direct ,但是它绑定的多个队列的 key 如果都相同,在这种情况下虽然绑定类型是 direct,但是它表现得就和 fanout 有点相似了,就跟广播差不多。

5.5.4 实战


  • 创建两个队列 console 和 disk,console队列里信道有两个 RoutingKey,info 和 warning,disk 队列的信道中有一个 routingKey,error。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b8AWyYn4-1650695403504)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220421223820682.png)]

队列 console:

package com.example.six;

import com.example.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author 且听风吟
 * @version 1.0
 * @description: 消费者1
 * @date 2022/4/21 0021 22:09
 */
public class ReceiveLogDirect01 {

    public static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        //获取信道
        Channel channel = RabbitUtils.getChannel();
        //声名一个交换机 参数 1:交换机名称 2:交换机类型
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        //声名一个队列
        channel.queueDeclare("console",false,false,false,null);
        //绑定交换机与队列 参数:1.队列名称 2.交换机名称 3.routingKey
        channel.queueBind("console",EXCHANGE_NAME,"info");
        channel.queueBind("console",EXCHANGE_NAME,"warning");

        //消费者接收消息的回调
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("ReceiveDirect01接收到的消息:"+new String(message.getBody(),"UTF-8"));
        };

        /**
         * 消费者消费(接收)消息,参数含义如下:
         *    1.消费哪个队列
         *    2.消费之后是否要自动应答
         *    3.消费者成功消费消息的回调
         *    4.消费者取消消费消息的回调
         */
        channel.basicConsume("console",true,deliverCallback,consumerTag -> {});
    }

}

队列 disk:

package com.example.six;

import com.example.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author 且听风吟
 * @version 1.0
 * @description: 消费者2
 * @date 2022/4/21 0021 22:09
 */
public class ReceiveLogDirect02 {

    public static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        //获取信道
        Channel channel = RabbitUtils.getChannel();
        //声名一个交换机 参数 1:交换机名称 2:交换机类型
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        //声名一个队列
        channel.queueDeclare("disk",false,false,false,null);
        //绑定交换机与队列 参数:1.队列名称 2.交换机名称 3.routingKey
        channel.queueBind("disk",EXCHANGE_NAME,"error");

        //消费者接收消息的回调
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("ReceiveDirect02接收到的消息:"+new String(message.getBody(),"UTF-8"));
        };

        /**
         * 消费者消费(接收)消息,参数含义如下:
         *    1.消费哪个队列
         *    2.消费之后是否要自动应答
         *    3.消费者成功消费消息的回调
         *    4.消费者取消消费消息的回调
         */
        channel.basicConsume("disk",true,deliverCallback,consumerTag -> {});
    }

}

生产者 Direct_logs:

package com.example.six;

import com.example.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;

import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;

/**
 * @author 且听风吟
 * @version 1.0
 * @description: 生产者
 * @date 2022/4/21 0021 22:25
 */
public class DirectLogs {

    public static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        //获取信道
        Channel channel = RabbitUtils.getChannel();
        //声名交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNext()) {
            String message = scanner.next();
            /**
             * 发送一个消息,参数含义如下:
             *    1.发送到哪个交换机,null表示使用默认交换机
             *    2.路由的key值是哪个
             *    3.其他参数信息
             *    4.发送消息的消息体(发送消息的二进制码)
             */
            channel.basicPublish(EXCHANGE_NAME, "info", null, message.getBytes("UTF-8"));
            System.out.println("生产者发出消息:"+message);
        }
    }
}
  • 生产者向 RoutingKey 为 info 的console队列发送消息:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kLbEA9Gp-1650695403505)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220421224645708.png)]

  • 观察两个队列接收到消息的情况:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fE9x5afh-1650695403511)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220421224749847.png)]

只有console队列接收到了消息,说明 direct 交换机只会发送消息到与其绑定的队列中。

5.6 Topics (主题模式)


5.6.1 之前类型的问题


尽管使用 direct 交换机改进了我们的系统,但是仍然存在局限性—比如说我们想接收的日志类型有 info.base 和 info.advantage,某个队列只想 info.base 的消息,那这个时候 direct 就办不到了。这个时候就只能使用 topic 类型。

5.6.2 Topic 的要求


  • 发送类型是 topic 交换机的 routingKey 不能随便写,必须满足一定要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词,比如说 “stock.nyse”, “queue.nyse”。当然,这个单词列表最多不能超过 255 个字节。

  • 在这个规则列表中,其中有两个替换符是需要注意的:

    *(星号)可以代表一个单词

    #(井号)可以代表零个或者多个单词

5.6.3 topic 匹配案例


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eyqeKI9C-1650695403512)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220421234242669.png)]

上图绑定关系如下:

  • Q1 绑定的是

    中间带 orange 带三个单词的字符串

  • Q2 绑定的是

    最后一个单词是 rabbit 的三个单词

    第一个单词是 lazy 的多个单词

当队列绑定关系是下列这些情况时需要注意:

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

5.6.4 实战


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ul9CUcoZ-1650695403513)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220423135741865.png)]

声名两个主题交换机及其队列。

两个消费者分别为 C1 和 C2,它们的队列分别为 Q1、Q2,连接队列 Q1 和交换机 topic 的 routingKey=* . orange . *,连接队列 Q2 和交换机 topic 的 routingKey = * . * . rabbit 和 lazy.#

消费者 C1:

package com.example.seven;

import com.example.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author 且听风吟
 * @version 1.0
 * @description: 声名主题交换机及其队列
 *            消费者 C1
 * @date 2022/4/23 0023 11:42
 */
public class ReceiveLogsTopic01 {

    public static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        //获取信道
        Channel channel = RabbitUtils.getChannel();
        //声名一个交换机 参数 1:交换机名称 2:交换机类型
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        //声名一个队列
        channel.queueDeclare("Q1", false, false, false, null);
        //绑定交换机与队列 参数:1.队列名称 2.交换机名称 3.routingKey
        channel.queueBind("Q1", EXCHANGE_NAME, "*.orange.*");
        System.out.println("等待接收消息。。。");

        //消费者接收消息的回调
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("ReceiveTopic01接收到的消息:"+new String(message.getBody(),"UTF-8"));
            System.out.println("接受队列:Q1"+"  绑定键:"+message.getEnvelope().getRoutingKey());
        };

        /**
         * 消费者消费(接收)消息,参数含义如下:
         *    1.消费哪个队列
         *    2.消费之后是否要自动应答
         *    3.消费者成功消费消息的回调
         *    4.消费者取消消费消息的回调
         */
        channel.basicConsume("Q1",true,deliverCallback,consumerTag -> {});
    }
}

消费者C2:

package com.example.seven;

import com.example.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author 且听风吟
 * @version 1.0
 * @description: 声名主题交换机及其队列
 *            消费者 C2
 * @date 2022/4/23 0023 11:42
 */
public class ReceiveLogsTopic02 {

    public static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        //获取信道
        Channel channel = RabbitUtils.getChannel();
        //声名一个交换机 参数 1:交换机名称 2:交换机类型
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        //声名一个队列
        channel.queueDeclare("Q2", false, false, false, null);
        //绑定交换机与队列 参数:1.队列名称 2.交换机名称 3.routingKey
        channel.queueBind("Q2", EXCHANGE_NAME, "*.*.rabbit");
        channel.queueBind("Q2", EXCHANGE_NAME, "lazy.#");
        System.out.println("等待接收消息。。。");

        //消费者接收消息的回调
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("ReceiveTopic02接收到的消息:"+new String(message.getBody(),"UTF-8"));
            System.out.println("接受队列:Q2"+"  绑定键:"+message.getEnvelope().getRoutingKey());
        };

        /**
         * 消费者消费(接收)消息,参数含义如下:
         *    1.消费哪个队列
         *    2.消费之后是否要自动应答
         *    3.消费者成功消费消息的回调
         *    4.消费者取消消费消息的回调
         */
        channel.basicConsume("Q2",true,deliverCallback,consumerTag -> {});
    }
}

生产者 EmitLogTopic:

package com.example.seven;

import com.example.utils.RabbitUtils;
import com.rabbitmq.client.Channel;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;

/**
 * @author 且听风吟
 * @version 1.0
 * @description: 生产者
 * @date 2022/4/23 0023 12:05
 */
public class EmitLogTopic {

    public static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        //获取信道
        Channel channel = RabbitUtils.getChannel();
        /**
         * Q1绑定的是:*.orange.*
         * Q2绑定的是:*.*.rabbit  lazy.3
         */
        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.fox","被队列Q2接收到");
        bindingKeyMap.put("lazy.pink.rabbit","虽然满足两个绑定条件但只被Q2接受一次");
        bindingKeyMap.put("quick.orange.male.rabbit","不匹配任何绑定会被丢弃");
        bindingKeyMap.put("quick.brown.fox","不匹配任何绑定会被丢弃");
        bindingKeyMap.put("lazy.orange.male.rabbit","匹配Q2,被Q2接收");

        for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) {
            String routingKey = bindingKeyEntry.getKey();
            String message = bindingKeyEntry.getValue();
            channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes("UTF-8"));
            System.out.println("生产者发出消息"+message);
        }
    }
}
  • 启动生产者,轮流发送Map里存放的消息:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jeiJYTS5-1650695403514)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220423141008523.png)]

  • 查看两个消费者接收到的消息:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wWaCoROA-1650695403515)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220423141326919.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4FpvpZv4-1650695403515)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220423141417502.png)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值