RabbitMQ干货讲解二(实战与理论并存)

公众号, 访问有惊喜, 关注不迷路!!
RabbitMQ干货讲解二(实战与理论并存)_第1张图片

目录

  • 交换机
  • 死信队列
  • 延迟队列
  • 回退消息
  • 备份交换机
  • 幂等性
  • 优先级队列
  • 惰性队列

交换机

图解
RabbitMQ干货讲解二(实战与理论并存)_第2张图片

概念: P生产者从来都不会直接发送消息到队列里, 都是要先走X交换机. 交换机通过RountingKey来绑定队列, 然后推送值队列, 相当于一个中转站

之前我们都是选择的默认交换机, ““代表默认的意思.
channel.basicPublish(””, QUEUE_NAME, null, MESSAGE.getBytes());

绑定(bindings)

概念: bindings,绑定是交换机和队列之间的桥梁关系。, 一个交换机通过可以绑定多个队列

图解
RabbitMQ干货讲解二(实战与理论并存)_第3张图片

Fanout

图解
RabbitMQ干货讲解二(实战与理论并存)_第4张图片

概念: 当交换机的类型定义为fanout, 生产者发送消息至交换机, 当两个bingding相同时, 则两个队列都可以接受到消息,
达到发布订阅模式, 两个消费者都可以消费当前消息. 简称: 一人发送, 多人消费.

生产者

/**
 * ClassName: Product
 * author: bob.ly
 * Version: 1.0.0
 * DateTime: 2021/07/25-20:47:00
 * Description:
 * 生产者方:主要是发送消息到交换机中
 */
public class Product {
    private static final String EXCHANGE_NAME = "logs";
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        /**
         * 声明交换机,同时指定交换机类型
         */
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入生产者信息");
        while (sc.hasNext()) {
            String message = sc.nextLine();
            channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
            System.out.println("生产者发送消息:" + message);
        }
    }
}

消费者

public class Consumer1 {
    private static final String EXCHANGE_NAME = "logs";
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        /**
         * 声明队列名称,临时队列可以直接调用API
         */
        String queueName = channel.queueDeclare().getQueue();

        /**
         * 交换机和队列进行绑定, rountingKey都为"", 及相同的key
         */
        channel.queueBind(queueName, EXCHANGE_NAME, "");
        System.out.println("等待接受信息,把接受的信息打印到屏幕上...");

        /**
         * 成功消费消息,则调用该方法,此处可以处理业务逻辑,message就是传递过来的消息,一般是对象
         */
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("接受消息:" + new String(message.getBody(), "UTF-8"));
        };

        /**
         * 失败调用回调函数
         */
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
}
public class Consumer2 {
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        String queueName = channel.queueDeclare().getQueue();

        /**
         * 交换机和队列进行绑定
         */
        channel.queueBind(queueName, EXCHANGE_NAME, "");
        System.out.println("等待接受信息,把接受的信息打印到屏幕上...");

        /**
         * 成功消费消息,则调用该方法,此处可以处理业务逻辑,message就是传递过来的消息,一般是对象
         */
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            String s = new String(message.getBody(), "UTF-8");
            System.out.println("接受消息:" + s);
        };

        /**
         * 失败调用回调函数
         */
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
}

console
RabbitMQ干货讲解二(实战与理论并存)_第5张图片

效果: 生产者发送消息, 发送至两个队列, 同时两个消费者都可以消费

Direct

概念:交换机只给指定的rountingKey发送消息至队列,
channel.queueBind(queueName, EXCHANGE_NAME, “routingKey”);

图解
RabbitMQ干货讲解二(实战与理论并存)_第6张图片
如图, X交换机的类型是direct, 有三个rountingKey, 生产者发送消息至交换机上, 绑定键只会去到相应的队列中, 其他消息则会丢失. 如果三个rountingKey都相同的话, 则等同于Fanout类型.
生产者

/**
 * ClassName: Product
 * author: bob.ly
 * Version: 1.0.0
 * DateTime: 2021/07/25-22:28:00
 * Description:
 * 生产者发送消息至交换机.要指定对应的rountingKey
 */
public class Product {
    private static final String EXCHANGE_NAME = "direct_name_logs";

    public static void main(String[] argv) 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("warning", "警告 warning 信息");
        bindingKeyMap.put("error", "错误 error 信息");
        //debug 没有消费这接收这个消息 所有就丢失了
        bindingKeyMap.put("debug", "调试 debug 信息");
        for (Map.Entry<String, String> bindingKeyEntry :
                bindingKeyMap.entrySet()) {
            String bindingKey = bindingKeyEntry.getKey();
            String message = bindingKeyEntry.getValue();
            channel.basicPublish(EXCHANGE_NAME, bindingKey, null,
                    message.getBytes("UTF-8"));
            System.out.println("生产者发出消息:" + message);
        }

    }
}

消费者

public class Consumer01 {
    private static final String EXCHANGE_NAME = "direct_name_logs";

    private static final String QUEUE_NAME = "direct_name11";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        /**
         * 声明交换机
         */
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        /**
         * 声明队列
         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        /**
         * 队列和交换机进行绑定
         */
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "error");
        DeliverCallback deliverCallback = (s1, s2) -> {
            System.out.println("消费者消费队列C1的消息:" + new String(s2.getBody(), "UTF-8"));
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
            System.out.println("取消回调该接口");
        });
    }
}

消费者

public class Consumer02 {
    private static final String EXCHANGE_NAME = "direct_name_logs";
    private static final String QUEUE_NAME = "direct_name22";
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        /**
         * 声明交换机
         */
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        /**
         * 声明队列
         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        /**
         * 队列和交换机进行绑定
         */
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "info");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "warning");
        DeliverCallback deliverCallback = (s1, s2) -> {
            System.out.println("消费者消费队列C2的消息:" + new String(s2.getBody(), "UTF-8"));
            System.out.println("收到提示消息");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
            System.out.println("取消回调该接口");
        });
    }
}

Console
RabbitMQ干货讲解二(实战与理论并存)_第7张图片

通过对应的rountingKey, 只会给对应的队列发送消息, 然后被消费者消费

Toptic

概念:可以使用通配符,它必须是一个单词列表,以点号分隔开。在这个规则列表中,其中有两个替换符是大家需要注意的
*(星号)可以代替一个单词
#(井号)可以替代零个或多个单词

图解
RabbitMQ干货讲解二(实战与理论并存)_第8张图片
RabbitMQ干货讲解二(实战与理论并存)_第9张图片
生产者

@Slf4j
public class Product {
    private static final String TOPIC_EXCHANGE = "topic.exchange";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(TOPIC_EXCHANGE, BuiltinExchangeType.TOPIC);
        Map<String, String> map = new HashMap<>();
        map.put("quick.orange.rabbit", "被队列 Q1Q2 接收到");
        map.put("lazy.orange.elephant", "被队列 Q1Q2 接收到");
        map.put("quick.orange.fox", "被队列 Q1 接收到");
        map.put("lazy.brown.fox", "被队列 Q2 接收到");
        map.put("lazy.pink.rabbit", "虽然满足两个绑定但只被队列 Q2 接收一次");
        map.put("quick.brown.fox", "不匹配任何绑定不会被任何队列接收到会被丢弃");
        map.put("quick.orange.male.rabbit", "是四个单词不匹配任何绑定会被丢弃");
        map.put("lazy.orange.male.rabbit", "是四个单词但匹配 Q2");

        for (Map.Entry<String, String> param : map.entrySet()) {
            String key = param.getKey();
            String message = param.getValue();
            log.info("当前路由key:{}, 当前消息体:{}", key, message);
            channel.basicPublish(TOPIC_EXCHANGE, key, null, message.getBytes());
        }
    }
}

消费者

public class Consumer01 {
    private static final String TOPIC_EXCHANGE = "topic.exchange";
    private static final String QUEUE_01 = "Q1";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(TOPIC_EXCHANGE, BuiltinExchangeType.TOPIC);
        channel.queueDeclare(QUEUE_01, true, false, false, null);
        channel.queueBind(QUEUE_01, TOPIC_EXCHANGE, "*.orange.*");

        DeliverCallback deliverCallback = (tag, message) -> {
            String s = new String(message.getBody(), "UTF-8");
            System.out.println("当前队列Q1接受的消息:" + s);
        };
        channel.basicConsume(QUEUE_01, true, deliverCallback, s -> {
            System.out.println("取消时候回调的接口");
        });
    }
}

消费者

public class Consumer02 {
    private static final String TOPIC_EXCHANGE = "topic.exchange";
    private static final String QUEUE_02 = "Q2";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(TOPIC_EXCHANGE, BuiltinExchangeType.TOPIC);
        channel.queueDeclare(QUEUE_02, true, false, false, null);
        channel.queueBind(QUEUE_02, TOPIC_EXCHANGE, "*.*.rabbit");
        channel.queueBind(QUEUE_02, TOPIC_EXCHANGE, "lazy.#");
        DeliverCallback deliverCallback = (tag, message) -> {
            String s = new String(message.getBody(), "UTF-8");
            System.out.println("当前队列Q1接受的消息:" + s);
        };
        channel.basicConsume(QUEUE_02, true, deliverCallback, s -> {
            System.out.println("取消时候回调的接口");
        });
    }
}

Console
RabbitMQ干货讲解二(实战与理论并存)_第10张图片

死信队列

概念:生产者发送消息至交换机, 交换机通过rountingKey发送至队列中, 消费者在消费时, 某些原因导致队列中的消息无法被消费, 若消息未被后续处理, 则变成死信消息, 死信消息应当放到死信队列中
死信队列保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。

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

死信造成原因: 1:消息存活时间过期了, 2:队列达到最长长度, 3: 消息被拒绝.
RabbitMQ干货讲解二(实战与理论并存)_第11张图片
思路: 首先定义一个生产者, 两个消费者, C1是消费普通队列, C2是消费死信队列, 在定义MQ时, 定义两个交换机, 一个死信交换机, 一个普通交换机, 类型均为direct模式, 当因为三种原因导致消息无法被消费时, 将普通队列里面添加参数, 跟死信交换机进行绑定, 核心: 此处是在队列部分发生转换的(普通转死信)

造成原因1:TTL时间过期,代码如下
生产者


public class Product {
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        /**
         * 声明交换机
         */
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        /**
         * 设置消息的TTL时间(存活时间), 消息只存活10秒, 若未被消费,则进入死信队列
         */
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("5000").build();

        /**
         * zhangsan就是RountingKey
         */
        for (int i = 1; i < 11; i++) {
            String message = "info" + i;
            channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", null, message.getBytes());
            System.out.println("生产者发送消息:" + message);
        }
    }
}

消费者

public class Consumer01 {
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    private static final String DEAD_EXCHANGE = "dead_exchange";

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

        /**
         * 正常队列绑定死信队列信息
         * 正常队列设置死信交换机 参数 key 是固定值
         * 正常队列设置死信 routing-key 参数 key 是固定值
         */
        Map<String, Object> params = new HashMap<>();
        params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        params.put("x-dead-letter-routing-key", "lisi");

        /**
         * 声明正常队列
         */
        channel.queueDeclare("normal-queue", false, false, false, params);
        channel.queueBind("normal-queue", NORMAL_EXCHANGE, "zhangsan");

        /**
         * 声明死信队列
         */
        channel.queueDeclare("dead-queue", false, false, false, null);
        channel.queueBind("dead-queue", DEAD_EXCHANGE, "lisi");
        DeliverCallback deliverCallback = (tag, message) -> {
            String s1 = new String(message.getBody(), "UTF-8");
            System.out.println("消费者接受到普通队列的消息:" + s1);
        };
        channel.basicConsume("normal-queue", false, deliverCallback, c -> {
        });
    }
}

消费者

public class Consumer02 {
    private static final String DEAD_EXCHANGE = "dead_exchange";

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

        /**
         * 声明死信交换机
         */
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
        /**
         * 声明死信队列
         */
        channel.queueDeclare("dead-queue", false, false, false, null);
        channel.queueBind("dead-queue", DEAD_EXCHANGE, "lisi");

        DeliverCallback deliverCallback = (tag, message) -> {
            System.out.println("消费者消费死信队列中的消息:" + new String(message.getBody(), "UTF-8"));
        };
        channel.basicConsume("dead-queue", true, deliverCallback, c -> {
        });
    }
}

Console
RabbitMQ干货讲解二(实战与理论并存)_第12张图片
造成原因2: 队列过长,代码如下
生产者

public class Product {
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        /**
         * 声明交换机
         */
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        /**
         * zhangsan就是RountingKey
         */
        for (int i = 1; i < 10; i++) {
            String message = "info" + i;
            channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", null, message.getBytes());
            System.out.println("生产者发送消息:" + message);
        }

    }
}

消费者

public class Consumer01 {
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    private static final String DEAD_EXCHANGE = "dead_exchange";

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

        /**
         * 正常队列绑定死信队列信息
         * 正常队列设置死信交换机 参数 key 是固定值
         * 正常队列设置死信 routing-key 参数 key 是固定值
         * 设置最长长度
         */
        Map<String, Object> params = new HashMap<>();
        params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        params.put("x-dead-letter-routing-key", "lisi");
        // 设置队列长度6
        params.put("x-max-length", 6);
        /**
         * 声明正常队列
         */
        channel.queueDeclare("normal-queue", false, false, false, params);
        channel.queueBind("normal-queue", NORMAL_EXCHANGE, "zhangsan");

        /**
         * 声明死信队列
         */
        channel.queueDeclare("dead-queue", false, false, false, null);
        channel.queueBind("dead-queue", DEAD_EXCHANGE, "lisi");

        DeliverCallback deliverCallback = (tag, message) -> {
            String s1 = new String(message.getBody(), "UTF-8");
            System.out.println("消费者接受到普通队列的消息:" + s1);
        };
        channel.basicConsume("normal-queue", false, deliverCallback, c -> {
        });
    }
}

消费者

public class Consumer02 {
    private static final String DEAD_EXCHANGE = "dead_exchange";

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

        /**
         * 声明死信交换机
         */
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
        /**
         * 声明死信队列
         */
        channel.queueDeclare("dead-queue", false, false, false, null);
        channel.queueBind("dead-queue", DEAD_EXCHANGE, "lisi");

        DeliverCallback deliverCallback = (tag, message) -> {
            System.out.println("消费者消费死信队列中的消息:" + new String(message.getBody(), "UTF-8"));
        };
        channel.basicConsume("dead-queue", true, deliverCallback, c -> {
        });
    }
}

console
RabbitMQ干货讲解二(实战与理论并存)_第13张图片
造成原因3: 拒绝策略,作用于消费者,代码如下


DeliverCallback deliverCallback = (tag, message) -> {
    String s1 = new String(message.getBody(), "UTF-8");
    if (s1.equals("info5")) {
        System.out.println("Consumer01 接收到消息" + message + "并拒绝签收该消息");
        //requeue 设置为 false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中
        channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
    } else {
        System.out.println("Consumer01 接收到消息" + message);
        channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
    }
    System.out.println("消费者接受到普通队列的消息:" + s1);
};

console
RabbitMQ干货讲解二(实战与理论并存)_第14张图片

延迟队列

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

使用场景

  1. 订单支付,十分之之内未支付则自动取消
  2. 用户注册成功后, 三天没有登陆,发送短信提示
  3. 钉钉会议, 拉会议时候, 提前30分钟进行通知.

支付流程图
RabbitMQ干货讲解二(实战与理论并存)_第15张图片
延时队列里面要设置TTL, 表明一条消息或者该队列中的所有消息的最大存活时间

1: 消息设置TTL

在发送消息时, 当成参数配置

2: 队列设置TTL

在paras参数里面配置

两者区别

如果设置了队列的过期时间, 若队列设置的时间过期, 则消息会被丢失,(如果配置了死信队列, 那么将会放到死信队列中)

如果设置了消息的过期时间, 消息过期, 也不会立即丢弃,消息是否过期是在即将投递到消费者之前判定的,

其实延时队列就是死信队列的拓展版.
延时队列代码结构图及代码
RabbitMQ干货讲解二(实战与理论并存)_第16张图片
思路: 创建一个生产者, 一个消费者, 两个交换机, 一个普通交换机X, 一个死信交换机Y, X通过两个rountingKey与队列QA,QB队列进行连接, Y通过YD和QD队列进行链接, 同时QA,QB设置Map参数, 来和死信交换机通过YD来进行绑定.

QC作为升级版, 如interfaces中的第二个接口. 将消息设置TTL更灵活

延时队列整合springboot

<dependencies>
        <!--RabbitMQ 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--RabbitMQ 测试依赖-->
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

配置类

@Configuration
public class TtlQueueConfig {
    private static final String X_EXCHANGE = "X";
    private static final String QUEUE_A = "QA";
    private static final String QUEUE_B = "QB";

    private static final String Y_EXCHANGE_DEAD = "Y";
    private static final String Y_DEAD_QUEUE = "QD";

    /**
     * 升级版
     */
    private static final String QUEUE_C = "QC";

    /**
     * 声明xExchange交换机,类型为direct
     *
     * @return
     */
    @Bean("xExchange")
    public DirectExchange xExchange() {
        return new DirectExchange(X_EXCHANGE);
    }

    /**
     * 声明yExchange交换机,类型为direct
     *
     * @return
     */
    @Bean("yExchange")
    public DirectExchange yExchange() {
        return new DirectExchange(Y_EXCHANGE_DEAD);
    }

    /**
     * 让普通队列QA跟死信交换机进行绑定, 设置普通队列的TTL为10S;
     */
    @Bean("queueA")
    public Queue queueA() {
        Map<String, Object> args = new HashMap<>();
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", Y_EXCHANGE_DEAD);
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", "YD");
        //声明队列的 TTL
        args.put("x-message-ttl", 10000);
        return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
    }

    /**
     * 让普通队列QB跟死信交换机进行绑定, 设置普通队列的TTL为40S;
     */
    @Bean("queueB")
    public Queue queueB() {
        Map<String, Object> args = new HashMap<>();
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", Y_EXCHANGE_DEAD);
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", "YD");
        //声明队列的 TTL
        args.put("x-message-ttl", 40000);
        return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
    }

    /**
     * 让普通队列C跟死信交换机进行绑定, 此处不设置队列的TTL时间
     */
    @Bean("queueC")
    public Queue queueC() {
        Map<String, Object> args = new HashMap<>();
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", Y_EXCHANGE_DEAD);
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", "YD");
        //声明队列的 TTL
        return QueueBuilder.durable(QUEUE_C).withArguments(args).build();
    }

    /**
     * 声明死信队列QD
     */
    @Bean("queueD")
    public Queue queueD() {
        return new Queue(Y_DEAD_QUEUE);
    }

    /**
     * 普通队列A跟普通交换机进行绑定, rountingKey为XA;
     */
    @Bean
    public Binding queueBindingA(@Qualifier("queueA") Queue queueA,
                                 @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }

    /**
     * 普通队列B跟普通交换机进行绑定, rountingKey为XB;
     */
    @Bean
    public Binding queueBindingB(@Qualifier("queueB") Queue queueB,
                                 @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queueB).to(xExchange).with("XB");
    }

    /**
     * 普通队列C跟普通交换机进行绑定, rountingKey为XC;
     */
    @Bean
    public Binding queueBindingC(@Qualifier("queueC") Queue queueC,
                                 @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queueC).to(xExchange).with("XC");
    }

    /**
     * 死信队列和死信交换机进行绑定, rountingKey为YD;
     */
    @Bean
    public Binding deadLetterBindD(@Qualifier("queueD") Queue queueD,
                                   @Qualifier("yExchange") DirectExchange yExchange) {
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }
}

生产者

@Slf4j
@RequestMapping("ttl")
@RestController
public class SendMsgController {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 同时给路由为XA,XB的队列传一样的消息.
     *
     * @param message
     */
    @GetMapping("/sendMag/{message}")
    public void sendMsg(@PathVariable("message") String message) {
        rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: " + message);
        rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: " + message);
        log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date(), message);
    }

    /**
     * 增强版本, TTL设置给消息,这样更灵活
     * @param message
     * @param ttlTime
     */
    @GetMapping("/sendMag/{message}/{ttlTime}")
    public void sendMsg(@PathVariable("message") String message,
                        @PathVariable("ttlTime") String ttlTime) {
        rabbitTemplate.convertAndSend("X", "XC", message, correlationData -> {
            correlationData.getMessageProperties().setExpiration(ttlTime);
            return correlationData;
        });
        log.info("当前时间:{},发送一条信息给队列:{}, 过期时间是:{}", new Date(), message, ttlTime);
    }
}

消费者

@Slf4j
@Component
public class DeadLetterQueueConsumer {

    /**
     * 消费者监听死信队列
     */
    @RabbitListener(queues = "QD")
    public void receiveD(Message message, Channel channel) throws UnsupportedEncodingException {
        String msg = new String(message.getBody(), "UTF-8");
        log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
    }
}

console
RabbitMQ干货讲解二(实战与理论并存)_第17张图片

虽然达到了延时发送的效果, 但是延时的消息必须以第一条为准, 在第一条的基础上, 如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。

延时队列存在的缺点: 需要靠第三方延时队列插件来处理,
在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。
RabbitMQ干货讲解二(实战与理论并存)_第18张图片
架构图
RabbitMQ干货讲解二(实战与理论并存)_第19张图片
在我们自定义的交换机中,这是一种新的交换类型,该类型消息支持延迟投递机制 消息传递后并不会立即投递到目标队列中,而是存储在 mnesia(一个分布式数据系统)表中,当达到投递时间时,才投递到目标队列中。


@Configuration
public class DelayedQueueConfig {
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";

    @Bean("delayedQueue")
    public Queue delayedQueue() {
        return new Queue(DELAYED_QUEUE_NAME);
    }

    /**
     * 自定义交换机 我们在这里定义的是一个延迟交换机
     * 自定义交换机的类型
     *
     * @return
     */
    @Bean("delayedExchange")
    public CustomExchange delayedExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false,
                args);
    }

    @Bean
    public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,
                                       @Qualifier("delayedExchange") CustomExchange
                                               delayedExchange) {
        return BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

生产者

public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
      @GetMapping("sendDelayMsg/{message}/{delayTime}")
      public void sendMsg(@PathVariable String message,@PathVariable Integer delayTime) {
       rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, message,
               correlationData ->{
                                 correlationData.getMessageProperties().setDelay(delayTime);
        return correlationData;
        });
       log.info(" 当 前 时 间 :{}, 发 送 一 条 延 迟 {} 毫秒的信息给队列 delayed.queue:{}", new
       Date(),delayTime, message);
}

消费者


public static final String DELAYED_QUEUE_NAME = "delayed.queue";
@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receiveDelayedQueue(Message 
message){String msg = new
String(message.getBody());
log.info("当前时间:{},收到延时队列的消息:{}", new Date().toString(), msg);

在这里插入图片描述

回退消息以及备份交换机

初级面试题: 当生产者发送消息至队列, 消息丢失,怎么解决?
答: 生产者开启confirm模式或者开启事务. 保证消息准确被队列所接受,同时回调通知生产者已经成功接受

进一步问: 如果mq宕机了, 导致消息直接丢失, 需要手动处理和恢复, 那我们如何进行消息可靠投递呢?
答: 可以放到一个缓存中, 或者打一个日志, 或者是重新发送至备份交换机

RabbitMQ干货讲解二(实战与理论并存)_第20张图片
1: 配置文件

#添加回调机制, 使用confirm模式,发送给exchange交换机
spring.rabbitmq.publisher-confirm-type=correlated

2:添加配置类


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

    @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 queueBindingExchange(@Qualifier("confirmExchange") DirectExchange directExchange
            , @Qualifier("confirmQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(directExchange).with("key1");
    }
}

3:回退类(由交换机发送消息)


@Component
@Slf4j
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.error("消息 id:{}未成功投递到交换机,原因是:{}", id, cause);
        }
    }
}

生产者


@RestController
@RequestMapping("/confirm")
@Slf4j
public class Product {
    private static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private MyCallBack myCallBack;

    /**
     * 依赖注入rabbitTemplate之后,再设置他的回调对象, 回调可大日志,确认交换机是否接受
     */
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(myCallBack);
    }

    /**
     * 那么问题来了, 当前只接受到了key1路由的消息, key2路由的消息直接丢弃掉, 作为生产者我怎么知道我的消息是被丢失了呢?
     *
     * @param message
     */
    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable("message") String message) {
        CorrelationData correlationData1 = new CorrelationData("1");
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,
                "key1",
                message + "key1",
                correlationData1);

        /**
         * 两者的rountingKey不同,一个是真实绑定的, 一个是手动添加的
         */
        CorrelationData correlationData2 = new CorrelationData("2");
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,
                "key2",
                message + "key2",
                correlationData2);
        log.info("发送消息内容:{}", message);
    }
}

消费者

@Component
@Slf4j
public class Consumer {
    private static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";

    @RabbitListener(queues = "confirm.queue")
    public void receiveMsg(Message message) throws UnsupportedEncodingException {
        String msg = new String(message.getBody(), "UTF-8");
        log.info("接受到的队列消息为:{}", msg);
        log.info("接受到的队列消息为:{}", message);
        log.info("当前信息的ID为:{}",message.getMessageProperties().getHeaders().get("spring_returned_message_correlation"));
    }
}

Console
RabbitMQ干货讲解二(实战与理论并存)_第21张图片
分析: 本质上生产者发送了条消息, 一条rountingKey为key1,另一个为key2, 消息都被交换机所接受, 最后队列却只收到一条消息:我是小刘key1.
原因key2这条rountingKey不存在,顾消息直接丢失, 但是生产者不知道丢掉哪条消息.通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。(仅仅是打一个日志效果)

Mandatory参数

1:MyCallBack中实现接口


@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
    /**
     * 交换机不管是否收到消息的一个回调方法
     * 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.error("消息 id:{}未成功投递到交换机,原因是:{}", id, cause);
        }
    }

    //新增重写方法
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        log.info("消息:{}被服务器退回,退回原因:{}, 交换机是:{}, 路由 key:{}",
                new String(returnedMessage.getMessage().getBody()), returnedMessage.getReplyText(), returnedMessage.getExchange(), returnedMessage.getRoutingKey());
    }
}

2:将上面的生产者修改成当前生产者


@Slf4j
@RequestMapping("/test")
@RestController
public class MessageProduct {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private MyCallBack myCallBack;

    /**
     * rabbitMq注入之后就设置该值
     * true:交换机无法将消息路由时, 将消息返回给生产者
     * false: 直接丢弃
     * 设置回退消息交给谁处理
     */
    @PostConstruct
    private void init() {
        rabbitTemplate.setConfirmCallback(myCallBack);
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnsCallback(myCallBack);
    }

    @GetMapping("sendMessage/{message}")
    public void sendMessage(@PathVariable("message") 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");
    }
}

console
RabbitMQ干货讲解二(实战与理论并存)_第22张图片
用mandatory仅仅是打了一个日志, 知道哪些消息未被队列消费, 可起不了上面作用, 仅仅提供一个日志. 仅仅打个日志不优雅, 在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理

备份交换机

RabbitMQ干货讲解二(实战与理论并存)_第23张图片
1:修改ConfirmConfig(交换机参数新增一个备胎交换机)

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

    /**
     * 以下为demo3的备胎交换机和队列成员变量
     */
    private static final String BACK_UP_EXCHANGE_NAME = "backup.exchange";
    private static final String BACK_UP_QUEUE = "backup.queue";
    private static final String WARNING_QUEUE = "warning.queue";

    @Bean("confirmExchange")
    public DirectExchange confirmExchange() {
        ExchangeBuilder
                exchangeBuilder =
                ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
                        .durable(true)
                        //设置该交换机的备份交换机
                        .withArgument("alternate-exchange", BACK_UP_EXCHANGE_NAME);
        return (DirectExchange) exchangeBuilder.build();
    }

    @Bean("confirmQueue")
    public Queue confirmQueue() {
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    @Bean
    public Binding queueBindingExchange(@Qualifier("confirmExchange") DirectExchange directExchange
            , @Qualifier("confirmQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(directExchange).with("key1");
    }

    /**
     * 以下为demo3的备胎交换机和队列,以及绑定
     */
    @Bean("backUpExchange")
    public FanoutExchange backUpExchange() {
        return new FanoutExchange(BACK_UP_EXCHANGE_NAME);
    }

    @Bean("backupQueue")
    public Queue backupQueue() {
        return QueueBuilder.durable(BACK_UP_QUEUE).build();
    }

    @Bean("warningQueue")
    public Queue warningQueue() {
        return QueueBuilder.durable(WARNING_QUEUE).build();
    }

    @Bean
    public Binding warningBinding(@Qualifier("warningQueue") Queue queue,
                                  @Qualifier("backUpExchange") FanoutExchange
                                          backupExchange) {
        return BindingBuilder.bind(queue).to(backupExchange);
    }

    @Bean
    public Binding backupBinding(@Qualifier("backupQueue") Queue queue,
                                 @Qualifier("backUpExchange") FanoutExchange backupExchange) {
        return BindingBuilder.bind(queue).to(backupExchange);
    }
}

2:增加消费者,主要消费回退的消息.

@Component
@Slf4j
public class BackUpConsumer {
    @RabbitListener(queues = "backup.queue")
    public void receiveWarningMsg(Message message) {
        String msg = new String(message.getBody());
        log.error("回退发现不可路由消息:{}", msg);
    }
}

3:增加消费者,主要消费回退的消息.


@Component
@Slf4j
public class WarningConsumer {

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

console
RabbitMQ干货讲解二(实战与理论并存)_第24张图片
mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先级高,经过上面结果显示答案是备份交换机优先级高
在这里插入图片描述

幂等性, 优先级队列, 惰性队列

幂等性

概念: 一条消息被一个消费者消费了两次, 是P0级别重大Bug, 相当于一个用户花了两次钱买一个商品

解决办法:

1: 发送消息时,生成一个全局Id,到了消费端那边,通过当前id进行校验, 我的思路是:如果该消费id来了,要去DB中修改数据并且存储当前id, 若生产者重复发送一条消息,拿当前id进行比较,若DB中存在当前id, 既丢弃,反之使用

// 设置消息id
CorrelationData correlationData1 = new CorrelationData(UUID.randomUUID().toString());

2:用Redis原子性解决,利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费

优先级队列

概念: 消息本来在队列里是顺序的, 此时存在某个消息可以插队, 存在先消费的优先级.

实现方法: 1:队列实现优先级, 2: 消息也要添加优先级

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

public class Product {
    private static final String PRIORITY_NAME = "priority_queue";

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

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

        for (int i = 0; i < 10; i++) {
            String message = "info" + i;
            if (i == 5) {
                AMQP.BasicProperties properties = new
                        AMQP.BasicProperties().builder().priority(5).build();
                channel.basicPublish("", PRIORITY_NAME, properties, message.getBytes());
            } else {
                channel.basicPublish("", PRIORITY_NAME, null, message.getBytes());

            }
        }
    }
}

消费者


public class Consumer {
    private static final String PRIORITY_NAME = "priority_queue";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        /**
         * 推送的消息如何进行消费的接口回调
         * consumerTag: 理解成指定的消息叫什么名字
         * delivery: 消息内容
         */
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String receivedMessage = new String(delivery.getBody());
            System.out.println("接收到消息:" + receivedMessage);
        };
        /**
         * 取消回调:
         */
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
        };
        System.out.println("C2 消费者启动等待消费.................. ");
        /**
         * 消费者消费消息
         * 1. 消费哪个队列
         * 2. 消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
         * 3. 消费者未成功消费的回调
         */
        channel.basicConsume(PRIORITY_NAME, true, deliverCallback, cancelCallback);
    }
}

console

info5优先被消费
RabbitMQ干货讲解二(实战与理论并存)_第25张图片

惰性队列

概念: 将消息写入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,

默认情况下,当生产者将消息发送到 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);

优点: 使用惰性队列, 内存开销较优,节省内存空间
在这里插入图片描述
公众号, 访问有惊喜, 关关注不迷路!!
RabbitMQ干货讲解二(实战与理论并存)_第26张图片

你可能感兴趣的:(RabbitMQ)