4.RabbitMQ交换机

4交换机

在之前的模式中,我们创建了一个工作队列,假设工作队列的背后,每个人物都签好交付给一个消费者(工作进程)。在这一部分中,将做一些完全不同的事情,将消息传达给多个消费者,这种模式我们称之为:“发布/订阅模式”。

例如一个简单的系统日志。他由两个程序组成,第一个程序将发出日志消息,第二个程序是消费者,其中消费者将会启动两个,一个接收到后将消息存储在磁盘上,第二个消费者把消息打印在屏幕上,事实上第一个程序发出的等消息日志将官拨给所有消费者。

之前的模式,消费者都是竞争关系,同一个队里中的同一份消息只会被消费一次。

4.1.Exchanges

4.1.1.Exchanges概念

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

4.1.2.Exchange类型

直接(direct)(路由类型)

主题(topic)

标题(headers)(头类型)

扇出(fanout)(发布订阅类型)

无名exchange

4.1.3.无名exchange

之前的发送消息我们并没有制定交换机,之前之所以能实现将消息发送到队列中,因为我们使用的是默认交换机,通常用字符串(“”)进行标识。

channel.basicPublish("", queueName, null, s.getBytes());

第一个参数是交换机名称,空字符串表示默认或者无名交换机;消息之所以能发送到队列中其实是由routingKey()绑定key指定的。

4.2.临时队列

每当我们连接到RabbitMQ时,我们需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者让服务器为我们选择一个随机队列名称那就更好了,其次我们一旦断开连接,队列就会自动删除,队列没有被持久化,持久化标记没有。

String queueName=channel.queueDedare().getQueue;

4.3.绑定(bindings)

1.Add a new queue

2.添加一个交换机

3.交换机与队列绑定

4.通过123与hello1相连接

4.4.fanout(发布订阅)(扇出)

4.4.1.概念

这种类型非常简单,正如名称中那样,它是将受到的所有信息,广播到他知道的所有队列中,系统中默认有些exchange类型。

4.4.2.fanout代码

package com.rabbitmq.five;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
import com.rabbitmq.config.RabbitMQConfig;
import com.rabbitmq.utils.RabbitMQUtil;

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

public class Logs {
}


class EmitLogs{
    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtil.getChannel();
        //声明一个交换机
        channel.exchangeDeclare(RabbitMQConfig.EXCHANGE_NAME,"fanout");
        Scanner sc=new Scanner(System.in);
        while (sc.hasNext()){
            String msg=sc.next();
            channel.basicPublish(RabbitMQConfig.EXCHANGE_NAME,"",null,msg.getBytes());
            System.out.println("生产者发出消息:"+msg);
        }
    }
}

class ReceiveLogs01{
    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtil.getChannel();
        //声明一个交换机
        channel.exchangeDeclare(RabbitMQConfig.EXCHANGE_NAME,"fanout");
        //声明一个队列 临时队列 队列名称随机
        String queue = channel.queueDeclare().getQueue();
        //绑定交换机和队列
        channel.queueBind(queue,RabbitMQConfig.EXCHANGE_NAME,"");
        System.out.println("ReceiveLogs01等待接收消息,并将消息打印在控制台");
        channel.basicConsume(queue, true, (s,delivery) ->{
            System.out.println("ReceiveLogs01控制台打印:"+new String(delivery.getBody()));
            },(s)->{});

    }
}
class ReceiveLogs02{
    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtil.getChannel();
        //声明一个交换机
        channel.exchangeDeclare(RabbitMQConfig.EXCHANGE_NAME,"fanout");
        //声明一个队列 临时队列 队列名称随机
        String queue = channel.queueDeclare().getQueue();
        //绑定交换机和队列
        channel.queueBind(queue,RabbitMQConfig.EXCHANGE_NAME,"");
        System.out.println("ReceiveLogs02等待接收消息,并将消息打印在控制台");
        channel.basicConsume(queue, true, (s,delivery) ->{
            System.out.println("ReceiveLogs02控制台打印:"+new String(delivery.getBody()));
        },(s)->{});
    }
}

4.5.Direct exchange

直接交换机  路由模式

队列支队他绑定的交换机的消息感兴趣,绑定参数routingKey来表示也可以称该参数为binding key,创建绑定我们用代码channel.queueBind(queue,RabbitMQConfig.EXCHANGE_NAME,"routingKey");绑定之后的意义由交换类型决定。

当routhingKey相同时,就是发布订阅模式,当帮懂得routhingKey不同时就是路由模式,也叫直接交换机。

4.5.1 路由模式多重绑定

package com.rabbitmq.six;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.config.RabbitMQConfig;
import com.rabbitmq.utils.RabbitMQUtil;

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

public class directLogs {
}

class EmitLogs{
    public static void main(String[] args) throws IOException, TimeoutException {
        int index=0;
        String [] routingKey={"info","warning","error"};
        Channel channel = RabbitMQUtil.getChannel();
        //声明一个交换机
        Scanner sc=new Scanner(System.in);
        while (sc.hasNext()){
            String msg=sc.next();
            channel.basicPublish(RabbitMQConfig.DIRECT_EXCHANGE_NAME,routingKey[index%routingKey.length],null,msg.getBytes());
            index++;
            System.out.println("生产者发出消息:"+msg);
        }
    }
}

class ReceiveLogs01{
    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtil.getChannel();
        //声明一个交换机
        channel.exchangeDeclare(RabbitMQConfig.DIRECT_EXCHANGE_NAME,BuiltinExchangeType.DIRECT);
        //声明一个队列 临时队列 队列名称随机
       channel.queueDeclare("console",false,false,false,null);
        //绑定交换机和队列
        channel.queueBind("console",RabbitMQConfig.DIRECT_EXCHANGE_NAME,"info");
        channel.queueBind("console",RabbitMQConfig.DIRECT_EXCHANGE_NAME,"warning");
        System.out.println("ReceiveLogs01等待接收消息,并将消息打印在控制台");
        channel.basicConsume("console", true, (s,delivery) ->{
            System.out.println("ReceiveLogs01Console控制台打印:"+new String(delivery.getBody()));
        },(s)->{});

    }
}
class ReceiveLogs02{
    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtil.getChannel();
        //声明一个交换机
        channel.exchangeDeclare(RabbitMQConfig.DIRECT_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        //声明一个队列 临时队列 队列名称随机
        channel.queueDeclare("disk",false,false,false,null);
        //绑定交换机和队列
        channel.queueBind("disk",RabbitMQConfig.DIRECT_EXCHANGE_NAME,"error");
        System.out.println("ReceiveLogs02等待接收消息,并将消息打印在控制台");
        channel.basicConsume("disk", true, (s,delivery) ->{
            System.out.println("ReceiveLogs02控制台打印:"+new String(delivery.getBody()));
        },(s)->{});

    }
}

此时第一次输入routingKey取到info,因为消费者1绑定的routingKey包含info,此时消费者1接收,第二次输入,routingKey取warning消费者1绑定的routingKey也包含warning,所以第二条数据也被消费者1接收,第三次输入routingKey取error,此时消息被消费者2接收。

4.6.Topic(主题交换机)

4.6.1概念

尽管direct交换机做了某些改进,但是他仍然有局限性,例如:日志类型有“info.base”,“info.advantage”,某个队列只需要“info.base”,那么这个时候direct交换机就做不到了。需要使用topic交换机。

4.6.2 topic要求

类型是topic交换机的信息的routing_key不能随便写,必须满足要求,他必须是一个单词表,以“.”分隔,这些单词可以是任意单词,例如“stock.usd.nyse”,“nyse.vmw”,“quick.orange.rabbit”,这种类型的,但是最大不能超过255字节。

在这个规则列表中,有两个替换符需要注意

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

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

对于直接交换机来说,最多只能路由一个队列,可以捆绑多个,但是发送只会发送到一个队列中。

4.6.3 匹配案例

Q1=>绑定的是

        1.中间带orange带三个单词的字符串(*.orange.*)

Q2=>绑定的是

        1.最后一个单词是rabbit的三个单词(*.*.rabbit)

        2.第一个单词是lazy的多个单词(lazy.#)

      routing_key                             满足队列                         接收次数

quick.orange.rabbit                      Q1.1,Q2.1                        2

lazy.orange.elephant                   Q1.1                                    1

quick.orange.fox                          Q1.1                                    1

lazy.brown.fox                              Q2.2                                    1

lazy.pink.rabbit                             Q2.1,Q2.2                         1

quick.brown.fox                            无                                        0

quick.orange.male.rabbit              无                                        0 

lazy.orange.mel.rabbit                  Q2.2                                    1

注意:当一个队列绑定#,那么他将接收所有数据,类似于fanout,如果队列绑定键没有#和*那么该队列绑定类型就是direct。所以主题交换机包含了其他两个交换机。

4.6.4 Topic代码

package com.rabbitmq.seven;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.config.RabbitMQConfig;
import com.rabbitmq.utils.RabbitMQUtil;

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

public class TopicExchange {
}
class EmitLogs{
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {

        String [] routingKey={"quick.orange.rabbit","lazy.orange.elephant"," quick.orange.fox","lazy.brown.fox","lazy.pink.rabbit","quick.brown.fox","quick.orange.male.rabbit","lazy.orange.mel.rabbit"};
        Channel channel = RabbitMQUtil.getChannel();
        //声明一个交换机
        for (int i = 0; i < routingKey.length; i++) {
            Thread.sleep(1500);
            String msg=routingKey[i]+":"+i;
            channel.basicPublish(RabbitMQConfig.TOPIC_EXCHANGE_NAME,routingKey[i],null,msg.getBytes());
            System.out.println("生产者发出消息:"+msg);
        }


    }
}
class ReceiveLogs01{
    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtil.getChannel();
        //声明一个交换机
        channel.exchangeDeclare(RabbitMQConfig.TOPIC_EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        //声明一个队列 临时队列 队列名称随机
        channel.queueDeclare("Q1",false,false,false,null);
        //绑定交换机和队列
        channel.queueBind("Q1",RabbitMQConfig.TOPIC_EXCHANGE_NAME,"*.orange.*");
        System.out.println("ReceiveLogs01等待接收消息,并将消息打印在控制台");
        channel.basicConsume("Q1", true, (s,delivery) ->{
            System.out.println("接收队列:Q1绑定键:"+delivery.getEnvelope().getRoutingKey()+"内容"+new String(delivery.getBody()));
        },(s)->{});

    }
}
class ReceiveLogs02{
    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtil.getChannel();
        //声明一个交换机
        channel.exchangeDeclare(RabbitMQConfig.TOPIC_EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        //声明一个队列 临时队列 队列名称随机
        channel.queueDeclare("Q2",false,false,false,null);
        //绑定交换机和队列
        channel.queueBind("Q2",RabbitMQConfig.TOPIC_EXCHANGE_NAME,"*.*.rabbit");
        channel.queueBind("Q2",RabbitMQConfig.TOPIC_EXCHANGE_NAME,"lazy.#");
        System.out.println("ReceiveLogs02等待接收消息,并将消息打印在控制台");
        channel.basicConsume("Q2", true, (s,delivery) ->{
            System.out.println("接收队列:Q2绑定键:"+delivery.getEnvelope().getRoutingKey()+"内容"+new String(delivery.getBody()));
        },(s)->{});

    }
}

生产者:

生产者发出消息:quick.orange.rabbit:0
生产者发出消息:lazy.orange.elephant:1
生产者发出消息: quick.orange.fox:2
生产者发出消息:lazy.brown.fox:3
生产者发出消息:lazy.pink.rabbit:4
生产者发出消息:quick.brown.fox:5
生产者发出消息:quick.orange.male.rabbit:6
生产者发出消息:lazy.orange.mel.rabbit:7

消费者1:

ReceiveLogs01等待接收消息,并将消息打印在控制台
接收队列:Q1绑定键:quick.orange.rabbit内容quick.orange.rabbit:0
接收队列:Q1绑定键:lazy.orange.elephant内容lazy.orange.elephant:1
接收队列:Q1绑定键: quick.orange.fox内容 quick.orange.fox:2

消费者2:

ReceiveLogs02等待接收消息,并将消息打印在控制台
接收队列:Q2绑定键:quick.orange.rabbit内容quick.orange.rabbit:0
接收队列:Q2绑定键:lazy.orange.elephant内容lazy.orange.elephant:1
接收队列:Q2绑定键:lazy.brown.fox内容lazy.brown.fox:3
接收队列:Q2绑定键:lazy.pink.rabbit内容lazy.pink.rabbit:4
接收队列:Q2绑定键:lazy.orange.mel.rabbit内容lazy.orange.mel.rabbit:7

4.7.死信队列

4.7.1概念

从概念上来讲,死信,指的是无法被消费的消息,一般来说producer将消息投递到broker或者直接到了queue中,consumer从queue中去除消息就行消费,但是某些时候由于特殊的原因,导致queue中的某些消息无法被消费,这样的消息如果没有后续处理就变成了死信,有私心自然就有了死信队列。

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

4.7.2死信的来源

消息TTL过期(可以通过生产者设置,也可以通过消费者设置)

队列达到了最大长度(队列满了,无法在添加到MQ中)

消息被拒绝(basic.reject或者basic.nack)并且不放回队列中(requeue=false)

4.7.3死信代码

当设置TTL为10秒时,超过10秒的消息自动进入死信队列。

当设置最大长度为6时,队列内消息超过6条将进入死信队列。

当设置消息拒绝时,被拒绝的消息自动进入死信队列。

消费者

package com.rabbitmq.eight;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.utils.RabbitMQUtil;

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

public class Consumer01 {
 
    public static final String NORMAL_QUEUE = "normal_queue";
    public static final String NORMAL_EXCHANGE = "normal_exchange";
    public static final String DEAD_QUEUE = "dead_queue";
    public static final String DEAD_EXCHANGE = "dead_exchange";
 
    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtil.getChannel();
        // 声明普通和死信交换机
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
 
 
        // 声明死信队列
        channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
        // 死信的绑定
        channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");
 
 
        Map<String, Object> arguments = new HashMap<>();
        // 普通队列设置对应的交换机
        arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        // 设置过期时间
       // arguments.put("x-message-ttl", 100000);
        // 设置死信队列的RouteKey
        arguments.put("x-dead-letter-routing-key", "lisi");
        // 设置队列最大长度
       // arguments.put("x-max-length", 6);
        // 声明普通队列
        channel.queueDeclare(NORMAL_QUEUE, false, false, false, arguments);
        // 普通的绑定
        channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan");
 
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            String msg = new String(message.getBody());
            //消息被拒绝
            if (msg.equals("info5")) {
                System.out.println("Consumer01接收到消息" + message + "并拒绝签收该消息");
                channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
            } else {
                System.out.println("consumer01接收到消息:" + msg);
                channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
            }
        };
        channel.basicConsume(NORMAL_QUEUE, false, deliverCallback, consumerTag -> {
        });
    }
}
class Consumer02 {

    public static final String NORMAL_QUEUE = "normal_queue";
    public static final String NORMAL_EXCHANGE = "normal_exchange";
    public static final String DEAD_QUEUE = "dead_queue";
    public static final String DEAD_EXCHANGE = "dead_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtil.getChannel();
        System.out.println("Consumer02等待接受消息:");
        channel.basicConsume(DEAD_QUEUE, false, (s,d)->{
            String msg = new String(d.getBody());
            System.out.println("consumer01接收到消息:" + msg);
            channel.basicAck(d.getEnvelope().getDeliveryTag(), false);
        }, consumerTag -> {
        });    }
}

生产者

package com.rabbitmq.eight;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.utils.RabbitMQUtil;

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

public class Producer {
 
    public static final String NORMAL_EXCHANGE = "normal_exchange";
 
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Channel channel = RabbitMQUtil.getChannel();
        //设置过期时间
       // AMQP.BasicProperties properties=new AMQP.BasicProperties().builder().expiration("10000").build();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
            String message = "info" + i;
            channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", null, message.getBytes());
            System.out.println("发送消息"+message);
        }
 
    }
}

4.8.延迟队列

当死信队列中消费者1永久消失,过期时间设置为10s,那么从生产者1到消费者2所花费的时间就是10s,延迟队列就是死信队列中的消息过期这一种情况。

4.8.1 概念

延时队列,队列内部是有虚的,最重要的特性就是体现在他的延时属性上,延时队列中的元素是希望在指定时间到了以后或者之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。

4.8.2 使用场景

1.订单在十分钟内未支付自动取消。

2.新创建的店铺,如果10天内没有上传商品,则自动发送消息提醒。

3.用户注册成功后,如果三天内没有登陆则进行短信提醒。

4.用户发起退款,三天内没有得到处理则通知相关人员。

5.预定会议后,需要在预定的时间点前十分钟通知各个参会人员参加会议。

这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:发生订单生成时间,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎是使用定时任务轮训查询数据,每秒一次,再取出需要被处理的数据。但是如果这种方式面对的试一下对于时间不是严格限制而是宽松意义上的某段时间,那么每天晚上拍个定时任务进行自动结算也是可行的,但是面对数据量比较大,并且时效性较强的场景,如:短期内未支付订单可能达到百万甚至是千万级别,对于如此庞大的数据,人就是用轮训的方式显然是不可取的,因为同一秒内无法完成所有订单的检查,同时给数据库带来很大的严厉,无法满足业务需求而且性能低下。

4.9.整合Spring Boot

4.9.1 新建Springboot项目

4.9.1.1 引入依赖
 <dependencies>
        <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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
4.9.1.2修改配置文件
spring.rabbitmq.host=112.124.34.53
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
4.9.1.3添加swagger配置类
package com.rabbitmq02.config;


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

import java.util.HashMap;
import java.util.Map;

/**
 * @Description
 * @ClassName TtlQueueConfig
 * @Author LY
 * @Date 2023/11/6 14:24
 **/
@Configuration
public class TtlQueueConfig {
    //普通交换机名称
    public static final String X_EXCHANGE="X";
    //死信交换机名称
    public static final String Y_DEAD_EXCHANGE="Y";
    //普通队列名称
    public static final String QUEUE_A="QA";
    public static final String QUEUE_B="QB";
    //死信队列名称
    public static final String DEAD_QUEUE_D="QD";

    //声明直接交换机X
    @Bean("xExchange")
    public DirectExchange xExchange(){
        return new DirectExchange(X_EXCHANGE);
    }
    //声明死信交换机Y
    @Bean("yExchange")
    public DirectExchange yExchange(){
        return new DirectExchange(Y_DEAD_EXCHANGE);
    }
    //声明称普通队列QA
    @Bean("queueA")
    public Queue queueA(){
        Map<String, Object> arg=new HashMap<>(3);
        //死信交换机
        arg.put("x-dead-letter-exchange",Y_DEAD_EXCHANGE);
        //死信RoutingKey
        arg.put("x-dead-letter-routing-key","YD");
        //过期时间TTL
        arg.put("x-message-ttl",10000);
        return QueueBuilder.durable(QUEUE_A).withArguments(arg).build();
    }
    //声明称普通队列QB
    @Bean("queueB")
    public Queue queueB(){
        Map<String, Object> arg=new HashMap<>(3);
        //死信交换机
        arg.put("x-dead-letter-exchange",Y_DEAD_EXCHANGE);
        //死信RoutingKey
        arg.put("x-dead-letter-routing-key","YD");
        //过期时间TTL
        arg.put("x-message-ttl",40000);
        return QueueBuilder.durable(QUEUE_B).withArguments(arg).build();
    }
    //声明称死信队列QD
    @Bean("queueD")
    public Queue queueD(){
        return QueueBuilder.durable(DEAD_QUEUE_D).build();
    }
    //绑定queueA和xExchange
    @Bean
    public Binding queueABindingX(@Qualifier("queueA") Queue queueA, @Qualifier("xExchange") DirectExchange
            xExchange){
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }
    //绑定queueB和xExchange
    @Bean
    public Binding queueBBindingX(@Qualifier("queueB") Queue queueB, @Qualifier("xExchange") DirectExchange
            xExchange){
        return BindingBuilder.bind(queueB).to(xExchange).with("XB");
    }
     //绑定queueD和yExchange
    @Bean
    public Binding queueDBindingy(@Qualifier("queueD") Queue queueD, @Qualifier("yExchange") DirectExchange
            yExchange){
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }

}
4.9.1.4 新增生产者
package com.rabbitmq02.controller;

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

import java.util.Date;

/***
 * @Description
 * 发从延迟消息
 * @ClassName SendMessageController
 * @Author LY
 * @Date 2023/11/6 15:06
 **/
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMessageController {

    @Autowired
   private RabbitTemplate rabbitTemplate;
    //开始发消息
    @GetMapping("/sendMsg/{message}")
    public void sendMsg(@PathVariable String message){
        log.info("当前时间={},发送一条信息给两个ttl队列:{}",new Date().toString(),message);
        rabbitTemplate.convertAndSend("X","XA","消息来自ttl为10s的队列:"+new String(message.getBytes()) );
        rabbitTemplate.convertAndSend("X","XB","消息来自ttl为40s的队列:"+new String(message.getBytes()) );
    }

}
4.9.1.5新增消费者
package com.rabbitmq02.consumer;

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Date;

/***
 * @Description
 * TTL消费者
 * @ClassName DeadLetterQueueConsumer
 * @Author LY
 * @Date 2023/11/6 15:39
 **/
@Component
@Slf4j
public class DeadLetterQueueConsumer {
    //接收消息
    @RabbitListener(queues = "QD")
    private void receiveD(Message message, Channel channel){
        String msg=new String(message.getBody());
        log.info("当前时间:{},死信队列消息:{}",new Date().toString(),msg);
    }
}
4.9.1.6结果

请求地址:http://localhost:8080/ttl/sendMsg/123456789987456321

当前时间=Mon Nov 06 16:30:42 GMT+08:00 2023,发送一条信息给两个ttl队列:123456789987456321
当前时间:Mon Nov 06 16:30:52 GMT+08:00 2023,死信队列消息:消息来自ttl为10s的队列:123456789987456321
当前时间:Mon Nov 06 16:31:22 GMT+08:00 2023,死信队列消息:消息来自ttl为40s的队列:123456789987456321

可以看到 延时接收已经生效了,但是这个代码扩展性并不好,如果我们现在需要新增一个1小时以后得延时队列,还需要新建一个队列,重新建立链接等等,如果有无数个延时需求,就需要无数个队列来满足需求,所以扩展性并不好,也不现实。

4.9.2 延时队列优化

基于上述问题,我们应该创建一个通用的延迟队列,不设置过期时间,具体过期时间应该由生产者发消息时进行指定。这样就可以用一个延迟队列实现所有延迟需求。

4.9.2.1 新增config类QueueC

QueueC不设置过期时间,生命队列并绑定。


//声明称普通队列QC
    @Bean("queueC")
    public Queue queueC(){
        Map<String, Object> arg=new HashMap<>(2);
        //死信交换机
        arg.put("x-dead-letter-exchange",Y_DEAD_EXCHANGE);
        //死信RoutingKey
        arg.put("x-dead-letter-routing-key","YD");
        return QueueBuilder.durable(QUEUE_C).withArguments(arg).build();
    }

//绑定queueC和xExchange
    @Bean
    public Binding queueCBindingX(@Qualifier("queueC") Queue queueC, @Qualifier("xExchange") DirectExchange
            xExchange){
        return BindingBuilder.bind(queueC).to(xExchange).with("XC");
    }
4.9.2.2 创建生产者

创建生产者发送消息并设置过期时间


//开始发消息 以及TTL
    @GetMapping("/sendExpiratMsg/{message}/{ttlTime}")
    public void sendExpiratMsg(@PathVariable String message,@PathVariable String ttlTime){
        log.info("当前时间={},发送一条过期市场为:{}ms的信息给队列QC,信息:{}",new Date().toString(),ttlTime,message);
        rabbitTemplate.convertAndSend("X","XC",message,msg->{
            //设置发送消息的延迟时长
            msg.getMessageProperties().setExpiration(ttlTime);
            return msg;
        } );
    }

当前时间=Mon Nov 06 16:55:01 GMT+08:00 2023,发送一条过期市场为:200
当前时间:Mon Nov 06 16:55:04 GMT+08:00 2023,死信队列消息:你好2
当前时间=Mon Nov 06 16:55:08 GMT+08:00 2023,发送一条过期市场为:200
当前时间:Mon Nov 06 16:55:28 GMT+08:00 2023,死信队列消息:你好2

结果已经达到了动态设置过期时间。

4.9.2.3 注意

如果我们先发送20秒的消息你好1,然后发送2秒的消息你好2,他并不会先接收到你好2,因为消息队列只会检测第一条信息是否过期,并不会检测第二条信息是否过期,所以你好2会在你好1被接收后紧接着被接收。这是死信队列巨大的问题,因为你好1的时间不应该约束到你好2。

当前时间=Mon Nov 06 16:54:10 GMT+08:00 2023,发送一条过期市场为:20000
当前时间=Mon Nov 06 16:54:14 GMT+08:00 2023,发送一条过期市场为:2000m
当前时间:Mon Nov 06 16:54:30 GMT+08:00 2023,死信队列消息:你好1
当前时间:Mon Nov 06 16:54:30 GMT+08:00 2023,死信队列消息:你好2

4.9.3 RabbitMQ插件实现延迟队列

只要是基于死信队列的,上述问题都没办法处理,所以只能基于插件实现延迟队列。

4.9.3.1 下载插件

插件地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/

4.9.3.2 进入目录
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.*.*/plugins
4.9.3.3 上传文件
4.9.3.4 安装插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
4.9.3.5 重启MQ
service rabbitmq-server restart
4.9.3.6 新建交换机

安装成功之后,新建交换机,发现type多了一个延迟消息类型。所以延迟消息不再由队列来实现,而是由交换机来实现。中间省去了死信队列的步骤。

4.9.3.7 新增配置类
package com.rabbitmq02.config;

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

import java.util.HashMap;
import java.util.Map;

/***
 * @Description
 * 给予插件的延迟队列
 * @ClassName DelayedQueueConfig
 * @Author LY
 * @Date 2023/11/7 10:06
 **/
@Configuration
public class DelayedQueueConfig {
    
    //交换机
    public static final String DELAYED_EXCHANGE_NAME="delayed.exchange";
    //队列
    public static final String DELAYED_QUEUE_NAME="delayed.queue";
    //ROUTINGKEY
    public static final String DELAYED_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> arg=new HashMap<>();
        arg.put("x-delayed-type","direct");
        return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,arg);
    }
    //绑定
    @Bean
    public Binding delayedQueueBindingDelayedExchange(@Qualifier("delayedQueue") Queue delayedQueue,@Qualifier("delayedExchange") CustomExchange delayedExchange) {
        return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }

}
4.9.3.8 生产者
 //基于插件的延时消息
    @GetMapping("/sendDelayMsg/{message}/{delayTime}")
    public void sendDelayMsg(@PathVariable String message,@PathVariable Integer delayTime){
        log.info("当前时间={},发送一条延时时间为:{}ms给队列delayed.queue,信息:{}",new Date().toString(),delayTime,message);
        rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME,DelayedQueueConfig.DELAYED_ROUTING_KEY,message, msg->{
            //设置发送消息的延迟时长
            msg.getMessageProperties().setDelay(delayTime);
            return msg;
        } );
    }
9.3.8 消费者
@Component
@Slf4j
public class DelayQueueConsumer {
    //接收消息
    @RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
    private void receiveDelayedQueue(Message message ){
        String msg=new String(message.getBody());
        log.info("当前时间:{},延迟队列消息:{}",new Date().toString(),msg);
    }
}
4.9.3.9 结论

基于插件的延迟消息可以做到完全根据发送消息延迟时间来进行延迟而不受消息顺序影响

当前时间=Tue Nov 07 10:53:45 GMT+08:00 2023,发送一条延时时间为:20000ms给队列delayed.queue,信息:你好1
当前时间=Tue Nov 07 10:53:49 GMT+08:00 2023,发送一条延时时间为:2000ms给队列delayed.queue,信息:你好2
当前时间:Tue Nov 07 10:53:51 GMT+08:00 2023,延迟队列消息:你好2
当前时间:Tue Nov 07 10:54:05 GMT+08:00 2023,延迟队列消息:你好1

4.9.4 总结

延时队列在需要延时处理的情况下非常有用,使用RabbitMQ来实现延迟队列可以很好的利用RabbitMQ的特性,如:消息可靠发送,消息可靠投递,死信队列来保障消息至少被消费一次,以及未被正确处理的消息不会被丢弃,另外通过RabbitMQ的集群特性,可以很好的解决单点故障问题,不会因为单节点挂掉导致延时队列不可用或者消息丢失。

当然延时队列还有其他很多西安则,例如JAVA的DelayQueue,利用Redis的zset利用Quartz或者kafka的时间轮,这些方式各有特点,具体要看使用的场景

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值