RabbitMQ学习笔记(下载安装,队列,交换机,死信队列,延迟队列,持久化,发布确认,集群)

最全学习笔记,各种知识点及代码案例

一、RabbitMQ概念及作用

在这里插入图片描述

概念:是一个接收,存储,转发的消息中间件
作用:
(1)流量削峰:

比如:订单系统,最大能处理一万次请求,但是在高峰期来了两万次,那么只能进行限制。如果使用消息队列,那么就可以取消掉这个限制,使用消息队列作缓冲。在用户端就是有的下单后就成功,有的过十几秒后收到成功下单信息,总比下单失败的体验好
在这里插入图片描述

在这里插入图片描述

(2)应用解耦

可以提高可用性,使用户感受不到故障
在这里插入图片描述

3.异步处理

比如A调B,B需要很长时间才能响应,那么就要采用异步的方式去处理

基础概念可以参考此博客: link.

RabbitMQ的六大模式

(1)简单模式:简单的发送与接收,没有特别的处理
(2)工作模式:一个生产者端,多个消费者端
(3)发布订阅模式:一个生产者端,多个消费者端同时接收所有的消息
(4)路由模式:生产者按routing key发送消息,不同的消费者端按不同的routing key接收消息。
(5)主题模式(通配符):通过routing key根据一些规则进行匹配
(6)发布确认模式:手动ack确认,并可以进行相应处理

交换机的四种类型

Direct Exchange(直连交换机)
Fanout Exchange(扇型交换机)
Topic Exchange(主题交换机)
Headers Exchange(头交换机)


二、linux下载安装

(1)下载

Rabbitmq的下载地址:
https://rabbitmq.com/download.html
注意版本是有对应的

(2)安装

cd /opt
在这里插入图片描述

1.依次执行下面命令:
rpm -ivh erlang-21.3-1.el7.x86_64.rpm 
yum install socat -y
rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm 
2.添加开机启动:
chkconfig rabbitmq-server on
3.启动:
 /sbin/service rabbitmq-server start
(停止:)

( /sbin/service rabbitmq-server stop)

4.查看启动状态:
 /sbin/service rabbitmq-server status

在这里插入图片描述

5.安装web管理界面,注意要先停止rabbitmq服务
rabbitmq-plugins enable rabbitmq_management
6.然后再启动
 /sbin/service rabbitmq-server start
7.在浏览器访问(写你本机IP,可以通过 ip -addr查看) 192.168.188.112:15672

有的访问不到是因为没有关闭防火墙,可以执行下面命令

systemctl stop firewalld
systemctl enable firewalld

在这里插入图片描述
默认用户名和密码都是 guest
登录后发现不能登,是需要创建一个用户的

8.创建用户

添加用户

rabbitmqctl add_user admin 123

设置用户角色(这里设置的超级管理员)

rabbitmqctl set_user_tags admin administrator

设置用户权限(可读可写可配置)

rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"

查看用户列表

rabbitmqctl list_users
9.用创建的用户登录

在这里插入图片描述


下面就开始进入写代码阶段啦

三、代码案例准备工作

1.创建工程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.引入依赖
<dependencies>
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.8.0</version>
    </dependency>
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.6</version>
    </dependency>
</dependencies>

四、案例讲解

案例1:

简单队列模式
一个生产者,一个消费者
在这里插入图片描述
代码结构:
在这里插入图片描述

生产者代码:

//生产者:发消息
public class Producer {
    //队列名称
    public static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.188.112");
        //用户名和密码
        factory.setUsername("admin");
        factory.setPassword("123");
        //创建连接
        Connection connection = factory.newConnection();
        //获取信道
        Channel channel = connection.createChannel();
        //创建一个队列
        /*参数
         1.队列名称
         2.队列消息是否持久化 默认不持久化
         3.该队列是否只供一个消费者进行消费,是否进行消息共享,true可以多个消费者消费,默认false
         4.是否自动删除,消费完后该队列是否删除
         5.其他参数
        */
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        //定义消息
         /*参数
         1.发送到哪个交换机
         2.路由的key值是哪个,本次是队列的名称
         3.其他参数信息
         4.发送的消息消息体
          */
        String message = "hello world";
        //发送一个消息
        channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
        System.out.println("消息发送完毕");
    }
}

运行后可去web端查看
在这里插入图片描述
消费者代码:


//消费者
public class Consumer {
    //队列名称
    public static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.188.112");
        factory.setUsername("admin");
        factory.setPassword("123");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //声明 接收消息
        DeliverCallback deliverCallback = (consumerTag,message)->{
            System.out.println(new String(message.getBody()));
        };
        CancelCallback cancelCallback = consumerTag->{
            System.out.println("消息消费被中断");
        };
        //取消消息
        /*
            消费消息
            参数:
            1.消费哪个队列
            2.消费成功后是否自动应答
            3.消费者未成功消费的回调
            4.消费者取消消费者的回调
         */
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    }
}

执行后会发现,消息被成功消费
在这里插入图片描述

案例二

工作模式
N个生产者,多个消费者
在这里插入图片描述
在这里插入图片描述

1.抽取信道获取的工具类,避免重复写代码
public class RabbitMqUtils {

    public static Channel geyChannel() throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.188.112");
        factory.setUsername("admin");
        factory.setPassword("123");
        Connection connection = factory.newConnection();
        return connection.createChannel();
    }
}

2.创建消费者1和消费者2
//消费者1
public class Consumer01 {
    //队列名称
    public static final String QUEUE_NAME = "work";

    public static void main(String[] args) throws IOException, TimeoutException {
        //获取信道
        Channel channel = RabbitMqUtils.geyChannel();
        //这里就不讲解了,案例一已经讲解过了
        //消息接收
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println(new String(message.getBody()));
        };
        CancelCallback cancelCallback = consumerTag->{
            System.out.println("消息消费被中断,也就是回调接口");
        };
        System.out.println("T1:等待消息中----");
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    }
}

创建消费者2
与上面代码一模一样复制一份

//消费者2
public class Consumer01 {
    //队列名称
    public static final String QUEUE_NAME = "work";

    public static void main(String[] args) throws IOException, TimeoutException {
        //获取信道
        Channel channel = RabbitMqUtils.geyChannel();
        //这里就不讲解了,案例一已经讲解过了
        //消息接收
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println(new String(message.getBody()));
        };
        CancelCallback cancelCallback = consumerTag->{
            System.out.println("消息消费被中断,也就是回调接口");
        };
        System.out.println("T2:等待消息中----");
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    }
}

3.创建生产者
//生产者:发消息
public class Producer {
    //队列名称
    public static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.geyChannel();
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        for (int i = 0; i < 10; i++) {
            String message = "hello world"+i;
            //发送一个消息
            channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
            System.out.println("消息发送完毕");
        }

    }
}

4.启动消费者1,消费者2,生产者

可以发现:默认是采用轮询去进行消费的
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

案例三

应答模式

1.首先先了解一些概念

消息应答机制
概念:为了保证消息不丢失,引入了消息应答机制,消息应答就是:消费者在接受到消息并且处理该消息后,告诉rabbitmq已经处理了,rabbitmq可以把消息删除了

1.1 自动应答

接收到消息后就会自动应答,不管后面程序有没有问题。这样就不是那么的靠谱

1.2 手动应答

(1) Channel.basicAck 用于肯定确认
(2) Channel.basicNack 用于否定确认(比第三种多一个参数multiple,批量处理参数)
(3) Channel.basicReject 用于拒绝确认
好处:可以批量应答并且减少网络拥堵

批量应答解释:
true: 该信道上,当第一个消息成功消费后,那么该信道上的所有消息都会应答
在这里插入图片描述
false:
在这里插入图片描述

消息自动重新入队机制
如果消费者由于某些原因失去了连接,导致消息未发生AKC曲儿,那么RabbitMQ会将该消息重新入队,并很快的将其重新分发给另一个消费者进行消费,这样即使某个消费者偶尔死亡,也可以确保不会丢失任何信息

2.编写测试用例

达到的目的:当消费者1未应答时,消息不会丢失,而被消费者2应答

2.1创建生成者
//应答模式
public class Producer {
    //队列名称
    public static final String TASK_QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.geyChannel();
        channel.queueDeclare(TASK_QUEUE_NAME,false,false,false,null);
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()){
            String message = sc.next();
            //发送一个消息
            channel.basicPublish("",TASK_QUEUE_NAME,null,message.getBytes());
            System.out.println("消息发送完毕");
        }
    }
}
2.2创建一个睡眠工具类,为了代码美观
public class SleepUtils {
    public static void sleep(int second){
        try {
            Thread.sleep(second*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.3创建消费者1
//应答模式
//消费者1
public class Consumer01 {
    //队列名称
    public static final String QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        //获取信道
        Channel channel = RabbitMqUtils.geyChannel();
        //消息接收
        DeliverCallback deliverCallback = (consumerTag, message)->{
            //这里模拟消费的快的消费者
            SleepUtils.sleep(1);
            System.out.println(new String(message.getBody(),"UTF-8"));
            //手动应答
            /**
             * 参数1:消息的标记 tag
             * 参数2:是否批量应答
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);

        };
        CancelCallback cancelCallback = consumerTag->{
            System.out.println("消息消费被中断,也就是回调接口");
        };
        System.out.println("C1:等待消息中----");
        //这里设置为false,不自动应答
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
    }
}
2.4创建消费者2

将消费者1的代码赋值过来,sleep改为30s

//应答模式
//消费者1
public class Consumer01 {
    //队列名称
    public static final String QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        //获取信道
        Channel channel = RabbitMqUtils.geyChannel();
        //消息接收
        DeliverCallback deliverCallback = (consumerTag, message)->{
            //这里模拟消费的慢的消费者
            SleepUtils.sleep(30);
            System.out.println(new String(message.getBody(),"UTF-8"));
            //手动应答
            /**
             * 参数1:消息的标记 tag
             * 参数2:是否批量应答
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);

        };
        CancelCallback cancelCallback = consumerTag->{
            System.out.println("消息消费被中断,也就是回调接口");
        };
        System.out.println("C2:等待消息中----");
        //这里设置为false,不自动应答
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
    }
}
启动生产者,消费者1,消费者2
生产者发送两条消息 aa,bb,可以观察到aa被C1消费了,而bb因为时间过长还未被C2消费掉,这时候把C2程序停了,会发现bb消息被C1成功消费了!


持久化相关知识点:

队列如何实现持久化

生产者方设置

只需要将参数改为true
在这里插入图片描述
启动后可能会报错,原因是该队列原本是不持久化的,不能强制变为持久化,需要去客户端将该队列删除后再重启就好了
在这里插入图片描述
持久的会显示一个D

消息如何实现持久化

生产者方设置

在发送消息的时候添加一个属性即可
MessageProperties.PERSISTENT_TEXT_PLAIN
在这里插入图片描述
注意:将消息标记为持久化并不能完全保证不会丢失消息,比如在准备存储到磁盘的时候,还没有存储完,消息还在缓存的一个间隔点,此时并没有真正的写入磁盘。持久性保证并不强,但是对于简单任务队列而言,这已经(绰绰有余)了

不公平分发

消费者方设置

可以看到上面的事例,消费端是轮询进行消费的,就是你一条,我一条,那么假如消费者1消费消息的能力强,而消费者2消费消息的能力弱,还才用轮询的方式的话,那么效率就十分低,这时候可以设置成不公平分发消息
在消费端设置
在这里插入图片描述
可以拿案例三进行测试

预期值

作用:限制缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题
(相当于一次就取设置的数量的消息,理解成权重也行)
设置的basicQos不为1的时候就成了预期值
channel.basicQos(3);

发布确认

前提:
1.设置要求队列必须持久化
2.设置要求队列中的消息必须持久化
3.开启发布确认
channel.confirmSelect();
分类:

1.单个确认发布
这是一种简单的确认方式,它是一种**同步确认发布**的方式
缺点:发布速度特别慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布
相关方法: waitForConfirmsOrDie(long)
2.批量确认发布

批量的进行确认

3.异步确认发布

客户端噌噌噌的发就完事了,消费端 每当消费成功后就通过函数回调来进行确认,失败的也会进行回调并通知给客户端,消息会被标记上序号,代码也相当复杂。

//确认模式

/**
 * 使用的时间来比较哪种确认方式是最好的
 * 1.单个确认
 * 2.批量确认
 * 3.异步批量确认
 */
public class ConfirmMessage {
    //队列名称
    public static final String TASK_QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//        1.单个确认
//        ConfirmMessage.publishMessageIndivdually();
//        2.批量确认
//        ConfirmMessage.publishMessageBatch();
//        3.异步确认
        ConfirmMessage.publishMessageAsync();
    }
    //单个确认  900ms
    public static void publishMessageIndivdually() throws IOException, TimeoutException, InterruptedException {
        Channel channel = RabbitMqUtils.geyChannel();
        //队列声明
        String queueName = "ack_queue_01";
        channel.queueDeclare(queueName,true,false,false,null);
        //开启发布确认
        channel.confirmSelect();
        //开始时间
        long start = System.currentTimeMillis();
        //--------------------------------------------------------------------
        for (int i = 0; i < 1000; i++) {
            String message = i + "message";
            channel.basicPublish("",queueName,null,message.getBytes());
            //单个消息马上进行发布确认(true表示确认成功)
            boolean flag = channel.waitForConfirms();
        }
        //--------------------------------------------------------------------
        //结束时间
        long end = System.currentTimeMillis();
        System.out.println(end-start+"ms");
    }

    //批量发布确认  135ms
    public static void publishMessageBatch() throws IOException, TimeoutException, InterruptedException {
        Channel channel = RabbitMqUtils.geyChannel();
        //队列声明
        String queueName = "ack_queue_02";
        channel.queueDeclare(queueName,true,false,false,null);
        //开启发布确认
        channel.confirmSelect();
        //开始时间
        long start = System.currentTimeMillis();
        //--------------------------------------------------------------------
        //批量确认消息大小
        int batchSize = 1000;

        //批量发布消息  批量发布确认
        for (int i = 0; i < 1000; i++) {
            String message = i + "message";
            channel.basicPublish("",queueName,null,message.getBytes());

            //判断达到100条的时候,批量确认一次
            if(i%batchSize == 0){
                //发布确认
                channel.waitForConfirms();
            }
        }
        //--------------------------------------------------------------------
        //结束时间
        long end = System.currentTimeMillis();
        System.out.println(end-start+"ms");
    }


    //异步发布确认   67ms
    public static void publishMessageAsync() throws IOException, TimeoutException, InterruptedException {
        Channel channel = RabbitMqUtils.geyChannel();
        //队列声明
        String queueName = "ack_queue_03";
        channel.queueDeclare(queueName,true,false,false,null);
        //开启发布确认
        channel.confirmSelect();
        //--------------------------------------------------------------------
        /**
         * 参数1:消息的标记,相当于每个消息的ID
         * 参数2:是否为批量确认
         */
        //消息确认成功,回调函数
        ConfirmCallback ackCallback = (deliveryTag,multiple) ->{
            System.out.println("确认的消息:"+deliveryTag);
            //2.删除掉已经确认的消息,剩下的就是未确认的消息
        };
        //消息确认失败,回调函数
        ConfirmCallback nackCallback = (deliveryTag,multiple) ->{
            System.out.println("未确认的消息:"+deliveryTag);

            //3.打印一下未确认的消息都有哪些
        };
        /**   准备消息的监听器,监听哪些消息成功了,哪些消息失败了
         *  参数1:监听哪些消息成功了
         *  参数2:监听哪些消息失败了
         */
        channel.addConfirmListener(ackCallback,nackCallback); //异步通知

        //开始时间
        long start = System.currentTimeMillis();

        //批量发布消息
        for (int i = 0; i < 1000; i++) {
            String message = i + "message";
            channel.basicPublish("",queueName,null,message.getBytes());
        }
        //--------------------------------------------------------------------
        //结束时间
        long end = System.currentTimeMillis();
        System.out.println(end-start+"ms");
    }
}



五、交换机相关案例

1.概念:
绑定:是交换机与队列之间的桥梁,交换机中可以绑定多个队列,通过路由Key进行关联
1.类型
直接(路由类型)
主题
标题(头类型)
扇出(发布订阅)

案例四

发布订阅模式(FANOUT)

1.创建生产者代码

这里使用默认队列,即不去声明队列名

public class Producer {
    //交换机的名称
    public static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException {

        Channel channel = RabbitMqUtils.geyChannel();

        Scanner sc = new Scanner(System.in);

        while (sc.hasNext()){
            String message = sc.next();
            channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes("UTF-8"));
            System.out.println("生产者发出消息成功:"+message);
        }
    }
}
2.创建消费者1

//交换机
public class ReceiveLogs01 {

    //交换机的名称
    public static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.geyChannel();
        //1.声明一个交换机
        /**
         * 参数1:交换机名称
         * 参数2:交换机类型
         */
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
        //声明一个临时队列
        /**
         * 生成一个临时的队列,队列名称是随机的
         * 当消费者断开与队列的连接时,队列就会自动删除
         */
        String queueName = channel.queueDeclare().getQueue();
        /**
         * 绑定交换机与队列
         * 参数1:队列名称
         * 参数2:交换机名称
         * 参数3,路由key
         */
        channel.queueBind(queueName,EXCHANGE_NAME,"");
        //接收消息
        DeliverCallback deliverCallback = (consumerTag,message)->{
            System.out.println("01收到的消息"+new String(message.getBody(),"UTF-8"));
        };
        channel.basicConsume(queueName,true,deliverCallback,consumerTag->{});
    }
}

3.创建消费者2

//交换机
public class ReceiveLogs01 {

    //交换机的名称
    public static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.geyChannel();
        //1.声明一个交换机
        /**
         * 参数1:交换机名称
         * 参数2:交换机类型
         */
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
        //声明一个临时队列
        /**
         * 生成一个临时的队列,队列名称是随机的
         * 当消费者断开与队列的连接时,队列就会自动删除
         */
        String queueName = channel.queueDeclare().getQueue();
        /**
         * 绑定交换机与队列
         * 参数1:队列名称
         * 参数2:交换机名称
         * 参数3,路由key
         */
        channel.queueBind(queueName,EXCHANGE_NAME,"");
        //接收消息
        DeliverCallback deliverCallback = (consumerTag,message)->{
            System.out.println("02收到的消息"+new String(message.getBody(),"UTF-8"));
        };
        channel.basicConsume(queueName,true,deliverCallback,consumerTag->{});
    }
}

案例五

直接模式(DIRECT)
通过路由key,到指定的队列去进行消费
一个队列可以绑多个key
在这里插入图片描述

1.创建消费者1
public class ReceiveLogsDirect01 {

    public static final String EXCHANGE_NAME ="direct_logs";

    public static void main(String[] args) throws IOException, TimeoutException {

        Channel channel = RabbitMqUtils.geyChannel();
        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        //声明一个队列
        channel.queueDeclare("console",false,false,false,null);
        /*
        参数1:队列名
        参数2:交换机名
        参数3:路由key
         */
        //一个队列可以绑定多个路由key
        channel.queueBind("console",EXCHANGE_NAME,"info");
        channel.queueBind("console",EXCHANGE_NAME,"warning");
        //接收消息
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("ReceiveLogsDirect01收到的消息"+new String(message.getBody(),"UTF-8"));
        };
        //消费者消费
        channel.basicConsume("console",true,deliverCallback,consumerTag->{});
    }
}
2.创建消费者2
public class ReceiveLogsDirect02 {

    public static final String EXCHANGE_NAME ="direct_logs";

    public static void main(String[] args) throws IOException, TimeoutException {

        Channel channel = RabbitMqUtils.geyChannel();
        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        //声明一个队列
        channel.queueDeclare("disk",false,false,false,null);
        /*
        参数1:队列名
        参数2:交换机名
        参数3:路由key
         */
        //一个队列可以绑定多个路由key
        channel.queueBind("disk",EXCHANGE_NAME,"error");
        //接收消息
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("ReceiveLogsDirect02收到的消息"+new String(message.getBody(),"UTF-8"));
        };
        //消费者消费
        channel.basicConsume("disk",true,deliverCallback,consumerTag->{});
    }
}
3.创建生产者
public class DorectLogs {

    public static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.geyChannel();
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()){
            String message = sc.next();
            //发送一个消息
            //这个是发给交换机,然后通过路由key去路由到相应的消费者
            //这里的路由key可以换成别的再试试
            //参数1:交换机名称   参数2:路由key  参数3:消息
            channel.basicPublish(EXCHANGE_NAME,"error", null,message.getBytes("UTF-8"));
            System.out.println("消息发送完毕");
        }
    }
}

案例六

主题模式(TOPIC)
规定:topic类型的路由key不能随意写,必须要满足一定的条件,它必须是一个单词列表,已点号分隔开。
比如 quick.orange.rabbit
在这个规则列表里,其中有两个替换符大家注意
*(星号):可以代替一个单词
#(井号):可以代替零个或者多个单词
例1:
Q1:路由key是 ( * . orange . *)
Q2:路由key是 ( * . * . rabbit ) 和 (lazy.#)
发送 quick.orange.rabbit时 --------------- 被队列Q1,Q2接收到
发送 lazy.orange.elephant时 --------------- 被队列Q1,Q2接收到
发送 quick.orange.fox时 --------------- 被队列Q1接收到
发送 lazy.pink.elephant时 --------------- 被队列Q2接收到
发送 quick.brown.fox时 -------------------不匹配任何绑定,不会被任何队列接收到,会被丢弃

当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像fanout了
如果队列绑定键当中没有#合*出现,那么该队列绑定类型就是direct了
所以说主题模式包含了上面两个模式

1创建消费者1
public class ReceiveLogsTopic01 {

    public static final String EXCHANGE_NAME ="topic_logs";

    public static void main(String[] args) throws IOException, TimeoutException {

        Channel channel = RabbitMqUtils.geyChannel();
        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        //声明一个队列  名称为Q1
        channel.queueDeclare("Q1",false,false,false,null);
        /*
        参数1:队列名
        参数2:交换机名
        参数3:路由key
         */
        //一个队列可以绑定多个路由key
        channel.queueBind("Q1",EXCHANGE_NAME,"*.orange.*");
        //接收消息
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("ReceiveLogsDirect01收到的消息"+new String(message.getBody(),"UTF-8"));
            System.out.println("路由key:"+message.getEnvelope().getRoutingKey());
        };
        //消费者消费
        channel.basicConsume("Q1",true,deliverCallback,consumerTag->{});
    }
}
2创建消费者2
public class ReceiveLogsTopic02 {

    public static final String EXCHANGE_NAME ="topic_logs";

    public static void main(String[] args) throws IOException, TimeoutException {

        Channel channel = RabbitMqUtils.geyChannel();
        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        //声明一个队列  名称为Q1
        channel.queueDeclare("Q2",false,false,false,null);
        /*
        参数1:队列名
        参数2:交换机名
        参数3:路由key
         */
        //一个队列可以绑定多个路由key
        channel.queueBind("Q2",EXCHANGE_NAME,"lazy.#");
        //接收消息
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("ReceiveLogsDirect01收到的消息"+new String(message.getBody(),"UTF-8"));
            System.out.println("路由key:"+message.getEnvelope().getRoutingKey());
        };
        //消费者消费
        channel.basicConsume("Q2",true,deliverCallback,consumerTag->{});
    }
}
3创建生产者
//主题模式
public class TopicLogs {

    public static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.geyChannel();
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()){
            String message = sc.next();
            //发送一个消息
            //参数1:交换机名称  参数2:路由key  参数三:消息
            //这里可以尝试这些key
            //quick.orange.rabbit
            //lazy.orange.fox
            channel.basicPublish(EXCHANGE_NAME,"lazy.orange.topic", null,message.getBytes("UTF-8"));
            System.out.println("消息发送完毕");
        }
    }
}

死信队列

概念:死信,顾名思义就是无法被消费的消息,这样的消息如果没有后续处理,就变成了死信,有死信自然就有了死信队列
应用场景:
1.为了保证订单业务的消息数据不丢失,需要使用RabbitMQ的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。
2.还有比如:用户在商城下单成功并点击支付后,在指定时间未支付时自动失效

死信的来源
1.消息TTL过期
2.队列达到了最大长度
3.消息被拒绝
下面将由三个案例进行模拟三种死信产生

案例七(消息TTL过期产生死信)

说明:这里定义两个交换机,两个队列
交换机1:普通交换机 队列1 : 普通队列
交换机2:死信交换机 队列2:死信队列
队列1:路由key :zhangsan
队列2:路由key:lisi
给队列1绑定死信队列,即队列2,然后进行测试
在这里插入图片描述

1.普通队列消费者代码
//死信队列
public class Consumer01 {

    //普通交换机
    public static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机
    public static final String DEAD_EXCHANGE = "dead_exchange";
    //普通队列
    public static final String NORMAL_QUEUE = "normal_queue";
    //死信队列
    public static final String DEAD_QUEUE = "dead_queue";

    public static void main(String[] args) throws IOException, TimeoutException {

        Channel channel = RabbitMqUtils.geyChannel();

        //声明死信和普通交换机
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        Map<String,Object> arguments = new HashMap<>();
        //过期时间  两处设置,普通队列这设置,或在生产方设置,一般在生产方设置
//        arguments.put("x-message-ttl",10000);
        //正常队列设置死信交换机
        arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
        //设置死信路由key
        arguments.put("x-dead-letter-routing-key","lisi");
        //声明死信和普通队列(第五个参数很重要)
        channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);
        //----------------------
        channel.queueDeclare(DEAD_QUEUE,false,false,false,null);

        //绑定普通的交换机与队列
        channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");

        //绑定死信的交换机与队列
        channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");

//        -----------------------上面都是配置代码------------------------------
        System.out.println("等待接收消息。。。");
        //接收消息
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("普通队列收到的消息"+new String(message.getBody(),"UTF-8"));
            System.out.println("路由key:"+message.getEnvelope().getRoutingKey());
        };
        channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,consumerTag->{});
    }
}

去控制台可观察到
在这里插入图片描述
在这里插入图片描述

2.生产者代码
public class Producer01 {
    public static final String NORMAL_EXCHANGE = "normal_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {

        Channel channel = RabbitMqUtils.geyChannel();
        //死信消息 设置TTL时间 5s,5s过后没有被消费则会自动进入死信队列

        //AMQP参数
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("5000").build();

        for (int i = 0; i < 11; i++) {
            String message = "message"+i;
            channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties,message.getBytes("UTF-8"));
        }
    }
}
3死信队列消费者代码
//死信队列
public class Consumer02 {

    //死信队列
    public static final String DEAD_QUEUE = "dead_queue";

    public static void main(String[] args) throws IOException, TimeoutException {

        Channel channel = RabbitMqUtils.geyChannel();

//        -----------------------上面都是配置代码------------------------------
        System.out.println("等待接收消息。。。");
        //接收消息
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("死信队列收到的消息"+new String(message.getBody(),"UTF-8"));
            System.out.println("路由key:"+message.getEnvelope().getRoutingKey());
        };
        channel.basicConsume(DEAD_QUEUE,true,deliverCallback,consumerTag->{});
    }
}
4.测试

3.1 先启动生产者1的代码,创建出队列,交换机及绑定
3.2 停止掉生成者1的代码,模拟消费失败的情况,看消息能否进入到死信队列
3.3 启动普通队列消费者代码,然后去Rabbitmq控制台观察会发现,五秒后,消息全部堆积到了死信队列上
3.4 启动死信队列消费者代码,会发现刚才消费失败的消息在死信队列进行消费了在这里插入图片描述

案例七(队列达到最大长度)

更改上面代码即可

1.设置队列最大长度,修改Consumer01 中部分代码
     //设置正常队列长度限制
        arguments.put("x-max-length",5);

在这里插入图片描述

2.在控制台中删除普通队列然后再重启Consumer01 ,可以观察到控制台的变化

在这里插入图片描述

3.消息生产者代码

//进入死信的方式1:设置最大长度
public class Producer02 {
    public static final String NORMAL_EXCHANGE = "normal_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {

        Channel channel = RabbitMqUtils.geyChannel();

        //发送消息
        for (int i = 0; i < 10; i++) {
            String message = "message"+i;
            System.out.println("发送消息····"+message);
            channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",null,message.getBytes("UTF-8"));
        }
    }
}
4.测试
  1. 停掉消费者Consumer02 和 Consumer01
  2. 启动生产者代码
  3. 观察控制台,会发现 死信队列中堆积了五条,普通队列中堆积了五条

在这里插入图片描述

案例八(消息被拒绝)

1.修改Consumer01 中部分代码

在这里插入图片描述

2.修改Consumer01 中部分代码,修改为手动应答,并拒绝掉某一条消息

截图部分替换为以下部分,做的改动主要是,设置成手动应答,然后拒绝掉其中一条消息
在这里插入图片描述

     DeliverCallback deliverCallback = (consumerTag, message)->{
            String msg = new String(message.getBody());
            if(msg.equals("message2")){
                System.out.println("此消息被拒绝:"+msg);
                //参数1:标号  参数2:表示是否批量应答
                channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
            }else {
                System.out.println("普通队列收到的消息"+new String(message.getBody(),"UTF-8"));
                channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
            }
        };
        channel.basicConsume(NORMAL_QUEUE,false,deliverCallback,consumerTag->{});
3.测试

3.1 去控制台删除普通队列
3.2 启动普通消费者Consumer01,停止死信消费者Consumer02
3.3 启动生产者Producer02
3.4 观察控制台会发现,其中有一条消息被拒绝了,并且在死信队列中
3.5 启动死信消费者Consumer02后,该 消息被消费
在这里插入图片描述



六、高级部分及案例

以下案例整合springboot进行实现

案例九 延迟队列

在这里插入图片描述

通过设置ttl过期时间+死信队列 来实现

1.新建子项目springboot-rabbitmq

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
继续NEXT,不选任何起步依赖,后面自己引入
在这里插入图片描述

2.引入依赖,编写yml文件
 <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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.58</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
        <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>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

yml

spring:
  rabbitmq:
    host: 192.168.188.112
    port: 5672
    username: admin
    password: 123

配置类:在上面的代码中,队列,交换机及绑定关系的声明都是在消费者或者生产者方去声明出来的,在springboot整合中,这些关系要写在配置类中,配置类只关注于这些配置信息
下面根据这个图去写配置类
X生成者,Y消费者
XA,XB 交换机
QA,QB,QC 队列
QD 死信队列

在这里插入图片描述

3.编写配置类

在这里插入图片描述

/**
 * TTL队列  配置文件类
 */
@Configuration
public class TtlQueueConfig {

    //普通交换机名称
    public static final String X_EXCHANGE = "X";
    //死信交换机名称
    public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
    //普通队列名称
    public static final String QUEUE_A = "QA";
    public static final String QUEUE_B = "QB";
    //死信队列名称
    public static final String DEAD_LETTER_D = "QD";

    //通用延迟队列
    public static final String QUEUE_C = "QC";
    //-------------声明交换机---------------

    //声明普通交换机
    @Bean("xExchange")
    public DirectExchange xExchange(){
        return new DirectExchange(X_EXCHANGE);
    }
    //声明死信交换机
    @Bean("yExchange")
    public DirectExchange YExchange(){
        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
    }

    //-------------声明队列-----------------
    //声明普通队列A,B
    @Bean("queueA")
    public Queue queueA(){
        Map<String,Object> arguments = new HashMap<>();
        //设置死信交换机,设置死信Key,设置过期时间(ms)
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        arguments.put("x-dead-letter-routing-key","YD");
        arguments.put("x-message-ttl",10000);
        return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();
    }
    @Bean("queueB")
    public Queue queueB(){
        Map<String,Object> arguments = new HashMap<>();
        //设置死信交换机,设置死信Key,设置过期时间(ms)
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        arguments.put("x-dead-letter-routing-key","YD");
        arguments.put("x-message-ttl",40000);
        return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();
    }
    //声明死信队列
    @Bean("queueD")
    public Queue queueD(){
        return QueueBuilder.durable(DEAD_LETTER_D).build();
    }
    //声明通用延迟队列
    @Bean("queueC")
    public Queue queueC(){
        Map<String,Object> arguments = new HashMap<>();
        //设置死信交换机,设置死信Key,设置过期时间(ms)
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        arguments.put("x-dead-letter-routing-key","YD");
        return QueueBuilder.durable(QUEUE_C).withArguments(arguments).build();
    }
    //--------------绑定关系----------------------
    @Bean
    public Binding queueABindingX(@Qualifier("queueA") Queue queueaA,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        //队列,交换机,routingKey
        return BindingBuilder.bind(queueaA).to(xExchange).with("XA");
    }
    @Bean
    public Binding queueBBindingX(@Qualifier("queueB") Queue queueaB,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        //队列,交换机,routingKey
        return BindingBuilder.bind(queueaB).to(xExchange).with("XB");
    }
    @Bean
    public Binding queueDBindingY(@Qualifier("queueD") Queue queueaD,
                                  @Qualifier("yExchange") DirectExchange yExchange){
        //队列,交换机,routingKey
        return BindingBuilder.bind(queueaD).to(yExchange).with("YD");
    }
    @Bean
    public Binding queueCBindingX(@Qualifier("queueC") Queue queueaC,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        //队列,交换机,routingKey
        return BindingBuilder.bind(queueaC).to(xExchange).with("XC");
    }
}

swagger是个自动生成接口文档的工具,这里之后用于测试

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    public Docket webApiConfig(){
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("webApi")
                .apiInfo(webApiInfo())
                .select()
                .build();
    }

    public ApiInfo webApiInfo(){
        return new ApiInfoBuilder()
                .title("rabbitmq接口文档")
                .description("本文档描述了rabbitmq微服务接口定义")
                .version("1.0")
                .contact(new Contact("gzx","www.baidu.com","7852@qq.com"))
                .build();
    }
}

4.编写生产者

在这里插入图片描述

@Controller
@RequestMapping("/ttl")
public class SendMsgController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    //开始发消息
    @GetMapping("/sendMsg/{message}")
    public void sendMsg(@PathVariable String message){

        //参数1 交换机名  参数2  routingKey   参数3  消息
        rabbitTemplate.convertAndSend("X","XA","消息来自ttl为10s的队列:"+message);
        rabbitTemplate.convertAndSend("X","XB","消息来自ttl为40s的队列:"+message);
    }
    
    //开始发消息  消息 TTL 可以自己设置过期时间
    @GetMapping("/sendMsg/{message}/{ttlTime}")
    public void sendTimeMsg(@PathVariable("message") String message,@PathVariable("ttlTime") String ttlTime){
        log.info("当前时间:{},发送死信队列的消息:{},延迟时间为: {}",new Date().toString(),message,ttlTime);
        //参数1 交换机名  参数2  routingKey   参数3  消息
        rabbitTemplate.convertAndSend("X","XC",message,msg->{

            //发送消息的时候,设置延迟时长
            msg.getMessageProperties().setExpiration(ttlTime);
            return msg;
        });
    }
}
5.编写消费者
@Slf4j
@Controller
@RequestMapping("/ttl")
public class SendMsgController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    //开始发消息
    @GetMapping("/sendMsg/{message}")
    public void sendMsg(@PathVariable String message){
        log.info("当前时间:{},发送死信队列的消息:{}",new Date().toString(),message);
        //参数1 交换机名  参数2  routingKey   参数3  消息
        rabbitTemplate.convertAndSend("X","XA","消息来自ttl为10s的队列:"+message);
        rabbitTemplate.convertAndSend("X","XB","消息来自ttl为40s的队列:"+message);
    }
}
6.测试

启动项目
网址输入:localhost:8080/ttl/sendMsg/哈哈哈发个消息
观察控制台
10s后 收到
40s后 收到

上面就是延迟队列的测试了,但是实际生产中,延迟时间肯定是不能像上面这样写死的,下面进行改进,使得延迟时间可以随意填写

1.在配置类中添加队列C的配置

    //通用延迟队列
    public static final String QUEUE_C = "QC";
    
    //声明通用延迟队列
    @Bean("queueC")
    public Queue queueC(){
        Map<String,Object> arguments = new HashMap<>();
        //设置死信交换机,设置死信Key,设置过期时间(ms)
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        arguments.put("x-dead-letter-routing-key","YD");
        return QueueBuilder.durable(QUEUE_C).withArguments(arguments).build();
    }
    
    @Bean
    public Binding queueCBindingX(@Qualifier("queueC") Queue queueaC,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        //队列,交换机,routingKey
        return BindingBuilder.bind(queueaC).to(xExchange).with("XC");
    }
2.编写测试接口

    //开始发消息  消息 TTL
    @GetMapping("/sendMsg/{message}/{ttlTime}")
    public void sendTimeMsg(@PathVariable("message") String message,@PathVariable("ttlTime") String ttlTime){
        log.info("当前时间:{},发送死信队列的消息:{},延迟时间为:{}",new Date().toString(),message,ttlTime);
        //参数1 交换机名  参数2  routingKey   参数3  消息
        rabbitTemplate.convertAndSend("X","XC",message,msg->{

            //发送消息的时候,设置延迟时长
            msg.getMessageProperties().setExpiration(ttlTime);
            return msg;
        });
    }
3.进行测试

固定好过期时间的接口测试:

3.1 网址输入
http://localhost:8080/ttl/sendMsg/哈哈哈哈/4000
http://localhost:8080/ttl/sendMsg/哈哈哈哈/20000

3.2 然后观察控制台
4s后消费一条
20s后消费一条

自定义过期时间的接口测试:

3.3 网址输入
http://localhost:8080/ttl/sendMsg/哈哈哈哈/2000
http://localhost:8080/ttl/sendMsg/哈哈哈哈/20000

3.4 然后观察控制台
2s后消费一条
20s后消费一条
注意:如果先发送20s的,再发送2的,会发现2s后消费的消息也会在第20s的时候才会被消费

3.5
网址输入
http://localhost:8080/ttl/sendMsg/哈哈哈哈/2000
http://localhost:8080/ttl/sendMsg/哈哈哈哈/20000

3.4 然后观察控制台
20s后消费了两条

原因:因为队列是FIFO,先进先出的特性,因此第二条消息也会等到第一条消息被消费后才能被消费

因此:这种延迟队列的实现是有弊端的

解决办法:下载一个延迟队列的插件

插件下载链接: 点击跳转.
进入到RabbitMQ的安装目录下的plgins目录,执行下面命令让该插件生效,然后重启RabbitMQ
1.将插件拷贝到下面这个路径下
/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
cp rabbitmq_delayed_message_exchange-3.8.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins

2.安装插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

3.重启rabbitmq
systemctl restart rabbitmq-server

4.检查是否安装成功
在控制台新增交换机选择类型时会多出来一项
在这里插入图片描述

案例十 ------ 延迟队列(基于插件的实现)正确用法!

在这里插入图片描述

概念:基于死信队列的实现,消息的延迟是在队列中去实现的,而基于插件的延迟是在交换机中去实现的,因此更为方便,并且解决了上面的问题

案例实现
在这里插入图片描述

1.编写配置类

在这里插入图片描述

@Configuration
public class DelayedQueueConfig {

    //队列名
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    //交换机名
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    //路由key
    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";

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

    //声明交换机
    @Bean
    public CustomExchange delayedExchage(){
        /**
         * 参数1:交换机名称
         * 参数2:交换机的类型
         * 参数3:是否需要持久化
         * 参数4:是否需要自动删除
         * 参数5:自定义参数
         */
        Map<String,Object> arguments = new HashMap<>();
        arguments.put("x-delayed-type","direct");
        return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,arguments);
    }

    //绑定
    public Binding delayedQueueBindingDelayedExchange(
            @Qualifier("delayedQueue") Queue delayedQueue,
            @Qualifier("delayedExchage") CustomExchange delayedExchage){
        return BindingBuilder.bind(delayedQueue).to(delayedExchage).with(DELAYED_ROUTING_KEY).noargs();
    }
}
2.编写发送者

在SendMsgController里增加下面代码

    //基于插件的 发消息  消息及延迟时间
    @GetMapping("/sendDelayedTimeMsg/{message}/{delayTime}")
    public void sendDelayedTimeMsg(@PathVariable("message") String message,@PathVariable("delayTime") Integer delayTime){
        log.info("当前时间:{},发送死信队列的消息:{},延迟时间为: {}",new Date().toString(),message,delayTime);
        //参数1 交换机名  参数2  routingKey   参数3  消息
        rabbitTemplate.convertAndSend("delayed.exchange","delayed.routingkey",message,msg->{

            //发送消息的时候,设置延迟时长
            msg.getMessageProperties().setDelay(delayTime);
            return msg;
        });
    }
3.编写消费者

在这里插入图片描述

//基于插件-延迟队列消费者
@Component
@Slf4j
public class DelayQueueConsumer {

    @RabbitListener(queues = "delayed.queue")
    public void receiveDealyQueue(Message message) throws Exception{
        String msg = new String(message.getBody());
        log.info("基于插件---当前时间:{},收到延迟队列的消息:{}",new Date().toString(),msg);
    }
}

4.测试

启动项目
在网址输入:
http://localhost:8080/ttl/sendDelayedTimeMsg/哈哈哈哈/20000
http://localhost:8080/ttl/sendDelayedTimeMsg/哈哈哈哈/2000

在控制台可以看到:
在这里插入图片描述
可以发现,先发送延迟20s的消息后消费了,2s延迟的正常先消费了

发布确认高级

引论:消息从生产者到交换机的过程失败,以及消息从交换机路由到消费者的时候失败,这两种情况下的处理
配置文件需要添加spring.rabbitmq.publisher-confirm-type=correlated

  • NONE
    禁用发布确认模式,是默认值
  • CORRELATED
    发布消息成功到交换机后会触发回调方法
  • SIMPLE
    相当于之前的单个确认
1.编写配置类

在这里插入图片描述


//发布确认(高级)
@Configuration
public class ConfirmConfig {

    //队列名
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    //交换机名
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    //路由key
    public static final String CONFIRM_ROUTING_KEY = "confirm.routingkey";

    //声明队列
    @Bean("confirmQueue")
    public Queue confirmQueue(){
        return new Queue(CONFIRM_QUEUE_NAME);
    }

    //声明交换机
    @Bean("confirmExchage")
    public DirectExchange confirmExchage(){
        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
    }

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

2.编写发送者

添加到controller里

    //发布确认 发消息
    @GetMapping("/sendConfirmMsg/{message}")
    public void sendConfirmTimeMsg(@PathVariable("message") String message){
        //这个对象用于给回调接口传送参数信息
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        log.info("当前时间:{},发送发布确认的消息:{}",new Date().toString(),message);
        //参数1 交换机名  参数2  routingKey   参数3  消息  参数4  用于回调的消息
        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData);

    }
3.编写消费者

在这里插入图片描述

@Component
@Slf4j
public class ConfirmConsumer {

    @RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
    public void receiveDealyQueue(Message message) throws Exception{
        String msg = new String(message.getBody());
        log.info("确认发布高级:{},收到的消息:{}",new Date().toString(),msg);
    }
}
4.编写交换机接收消息成功与失败回调接口

在这里插入图片描述

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

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
    }

    /**
     *
     * @param correlationData 保存回调消息的ID及相关信息
     * @param ack  交换机是否收到消息
     * @param s  null(交换机收到消息) 失败原因(交换机未收到消息)
     */

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String s) {
        if(ack){
            log.info("交换机收到ID为:{}的消息",correlationData.getId());
        }else {
            log.info("交换机未收到ID为:{}的消息,原因{}",correlationData.getId(),s);
        }
    }
}
5.测试

1.在浏览器输入
http://localhost:8080/ttl/sendConfirmMsg/哈哈哈
2.观察到控制台,消息消费成功
3.修改发送消息方,将交换机名称换成一个不存在的交换机名称,比如加个123
在这里插入图片描述
4.然后再在浏览器输入http://localhost:8080/ttl/sendConfirmMsg/哈哈哈
会发现走了回调接口中的未成功发送的方法
在这里插入图片描述
注意:当交换机名称正确,而路由key不存在时,这时候发送出的消息会丢失掉,并不会走回调方法,要想走回调需要下面的步骤

6.实现队列的回调接口

在这里插入图片描述
在这里插入图片描述

添加如下内容到上面的 MyCallBack 类

public class MyCallBack  implements RabbitTemplate.ConfirmCallback , RabbitTemplate.ReturnsCallback 
    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    } 

    //只有消息不可达目的地时候 将 消息返还给生产者
    @Override
    public void returnedMessage(ReturnedMessage message) {
        log.error("消息{},被交换机{}退回,退回原因:{},路由key:{}",
                message.getMessage().getBody(),message.getExchange(),message.getReplyText(),message.getRoutingKey());
    }
7.测试交换机未到队列的消息的回退

1.修改接口,发送到一个不存在的路由key
在这里插入图片描述
2.在浏览器输入
http://localhost:8080/ttl/sendConfirmMsg/%E5%93%88%E5%93%88%E5%93%88%E5%93%88
3.观察控制台
在这里插入图片描述

总结

在这里插入图片描述

1.ConfirmCallback 接口的实现是判断并回调的 消息从生产者到交换机这个过程的成功与失败
2.ReturnsCallback 接口的实现是判断并回调的 消息从交换机到目的地队列这个过程的成功与失败

备份交换机

引论:当生产者发送消息到交换机A失败,会再将消息转发到交换机B上,这样的好处是可以防止消息丢失以及对消息的告警,一般备份交换机使用广播类型
这里新增一个备份交换机,两个队列,然后作为上面确认交换机的备份交换机
在上面的确认案例进行更改
1.在配置类ConfirmConfig中增加如下内容

    //------备份交换机
    //队列名
    public static final String BACKUP_QUEUE_NAME = "backup.queue";
    public static final String WARNING_QUEUE_NAME = "warning.queue";
    //交换机名
    public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
    //-------------
    //-----备份交换机的配置
    //声明队列
    @Bean("backupQueue")
    public Queue backupQueue(){
        return new Queue(BACKUP_QUEUE_NAME);
    }

    @Bean("warningQueue")
    public Queue warningQueue(){
        return new Queue(WARNING_QUEUE_NAME);
    }
    //声明交换机
    @Bean("backupExchage")
    public FanoutExchange backupExchage(){
        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }
    //绑定
    @Bean
    public Binding backupQueueBindingbackupExchage(
            @Qualifier("backupQueue") Queue backupQueue,
            @Qualifier("backupExchage") FanoutExchange backupExchage) {
        return BindingBuilder.bind(backupQueue).to(backupExchage);
    }
    @Bean
    public Binding warningQueueBindingbackupExchage(
            @Qualifier("warningQueue") Queue warningQueue,
            @Qualifier("backupExchage") FanoutExchange backupExchage){
    return BindingBuilder.bind(warningQueue).to(backupExchage);
    }

2.修改要备份的交换机的配置代码
增加配置备份交换机的属性

 //声明交换机
    @Bean("confirmExchage")
    public DirectExchange confirmExchage(){
        return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(true).withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();
    }

3.编写备份交换机绑定队列的消息接收
在这里插入图片描述

@Component
@Slf4j
public class WarningConsumer {

    //接收报警消息
    @RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
    public void receiveDealyQueue1(Message message) throws Exception{
        String msg = new String(message.getBody());
        log.info("备份交换机转发---接收到报警消息:{},收到的消息:{}",new Date().toString(),msg);
    }

    //接收报警消息
    @RabbitListener(queues = ConfirmConfig.BACKUP_QUEUE_NAME)
    public void receiveDealyQueue2(Message message) throws Exception{
        String msg = new String(message.getBody());
        log.info("备份交换机转发---接收到消息:{},收到的消息:{}",new Date().toString(),msg);
    }
}

4.修改生产者,使得原交换机不能路由到指定交换机,从而走备份交换机
在这里插入图片描述

5.测试
网址输入:http://localhost:8080/ttl/sendConfirmMsg/123
观察控制台:
在这里插入图片描述
原交换机没有将消息成功路由,从而走了备份交换机
并且,备份交换机的优先级高于发布确认中配置的消息不可达时所调用的方法。
综上所述:备份交换机的意义就在于,当原交换机收到消息后,未能成功路由消息,从而走备份交换机进行消息的传递

消息重复消费

MQ在返回ack确认时发生网络中断,导致消息重复消费
解决思路:
1.保证幂等性,一般是使用全局ID,每条消息都有自己唯一的ID,每次消费消息时先判断一下该ID消息是否被消费过,这个ID可以有两种方式,一种是通过唯一ID+ 指纹码机制(指纹码就是指基于业务拼接出来的),另一种就是使用Redis的setnx命令,它天然具有幂等性
setnx():
将 key 的值设为 value,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。

优先级队列

设置优先级,根据优先级进行消息的消费

惰性队列

消息保存在内存中还是保存在磁盘中
正常队列:消息保存在内存中
惰性队列:消息保存在磁盘中

使用场景:大量消息不能消费而堆积的时候,很占内存

Rabbitmq集群的搭建

1.准备两台虚拟机

修改两个主机名称为node1 , node2
vi /etc/hostname

2.配置各个节点的hosts文件,让各个节点都能互相认识对方

vi /etc/hostsx
在这里插入图片描述](https://img-blog.csdnimg.cn/c94aa2ebf2a14ae7a49cf9b7d219e889.png)
![在这里插入图片描述

3.确保各个文件使用的cookie是一个值

分别执行命令
node1执行:
scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie
node2执行
scp /var/lib/rabbitmq/.erlang.cookie root@node1:/var/lib/rabbitmq/.erlang.cookie

4.启动RabbitMQfuwu,顺带启动Erlang虚拟机和RabbitMQ应用服务(两台节点上分别执行下面命令)
5.在node2上执行下面命令(相当于把node2加入到node1中,主要步骤)

rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node1
rabbitmqctl start_app

6.查看集群状态

rabbitmqctl cluster_status
在这里插入图片描述

7.需要重新设置用户

添加用户

rabbitmqctl add_user admin 123

设置用户角色(这里设置的超级管理员)

rabbitmqctl set_user_tags admin administrator

设置用户权限(可读可写可配置)

rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"

查看用户列表

rabbitmqctl list_users
8.登录到控制台

192.168.188.110.15762
192.168.188.112.15762
任意一台都可以
在这里插入图片描述
补充:
脱离集群命令
在 node1上执行

注意:
假设在node1上创建一个队列hello,发送消息后,不进行消费,然后把node1宕机了,消息会被node2消费吗?
答案是不能,在node1上创建那只能是在node1上
解决办法:配置镜像队列

配置镜像队列(才能使得集群起到集群的作用)

1.登录进入控制台
2.创建策略
在这里插入图片描述
在这里插入图片描述
说明:
ha-mode( 备机模式):exactly( 指定模式)
ha-params(备份几份,备份几个节点):2
ha-sync-mode(备份模式):automatic(自动)

测试:创建一个mirrior_hello队列,不进行消费,去控制台看
在这里插入图片描述
测试:
这时候再进行上面的操作,在node1上创建一个队列hello,发送消息后,不进行消费,然后把node1宕机了,然后启动消费者,会发现消息会被成功消费,因为该消息成功备份了

集群下实现负载均衡

可以使用nginx或使用keeplived

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JavaSupeMan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值