java学习之RabbitMq


参考资料
尚硅谷RabbitMq视频教程
官网:
RabbitMQ官网

1.MQ的相关概念与安装

a)什么是MQ
MQ,即消息队列,本质是一个先进先出的队列,只不过队列中存放的是message而已,在互联网架构中,MQ 是一种非常常
见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了 MQ 之后,消息发送上游只需要依赖 MQ,不用依赖其他服务。

b)为什么要用MQ
1、流量削峰:假设一个订单系统的上线是1万次每秒,当流量太大时,就可以使用MQ,使访问的人员开始排队,以保护系统不宕机。
请添加图片描述
2、应用解耦:
在这里插入图片描述
3、异步处理: 有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提供一个 callback api, B 执行完之后调用 api 通知 A 服务。使用消息总线,A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样 B 服务也不用做这些操作。
在这里插入图片描述
c)四大核心概念

RabbitMQ就类似于一个快递站,可以存储和转发消息

生产者:产生数据发送消息的程序是生产者
交换机:交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定
队列:队列是 RabbitMQ 内部使用的一种数据结构,尽管消息流经 RabbitMQ 和应用程序,但它们只能存储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。
消费者:消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。
请添加图片描述
d)RabbitMQ的工作原理
在这里插入图片描述
e)rabbitmq的安装

官网下载地址:
https://www.rabbitmq.com/download.html

将下载的文件放到centos的opt文件夹下
在这里插入图片描述
然后按照顺序执行下面的命令
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

添加开机启动rabbitmq服务

chkconfig rabbitmq-server on
在这里插入图片描述
启动服务

/sbin/service rabbitmq-server start
在这里插入图片描述
查看服务状态

/sbin/service rabbitmq-server status
~~在这里插入图片描述~~
f)安装web界面插件
首先要先关闭rabbitmq的服务:/sbin/service rabbitmq-server stop

输入命令:rabbitmq-plugins enable rabbitmq_management开启web管理插件,安装完后重启即可

注意要关闭防火墙,命令:systemctl stop firewalld

随后输入:systemctl enable firewalld可以使下次开机时防火墙也不启动

然后再浏览器中输入你的IP地:15672 就可以访问web监管页面了
在这里插入图片描述
g)添加用户并设置权限
上一步打开页面后,我们需要添加用户才能登录,命令:rabbitmqctl list_users可以查看所有用户

创建账号:rabbitmqctl add_user 账户名称 账户密码

设置用户权限为超级管理员:rabbitmqctl set_user_tags 账户名称 administrator

设置用户权限格式:set_permissions [-p <vhostpath>] <user> <conf> <write> <read>
rabbitmqctl set_permissions -p “/” admin “.*” “.*” “.*” :用户 admin 具有 / 这个 virtual host 中所有资源的配置、写、读权限

然后用这个账户登录即可
在这里插入图片描述

2.HelloWorld

接下来是一个最简单的案例,用java来模拟一个发送单个消息的生产者和接收消息并打印出来的消费者。

请添加图片描述

a)引入依赖,创建java开发环境

    <!--指定 jdk 编译版本-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!--rabbitmq 依赖客户端-->
        <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>

b)生产者代码

public class Producter {
    //队列名称
    public static final String QUENE_NAME = "hello";
    //发消息
    public static void main(String[] args) throws IOException, TimeoutException {
        //创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置工厂的ip,连接rabbitmq的队列
        factory.setHost("服务器ip");
        //用户名
        factory.setUsername("admin");
        //密码
        factory.setPassword("你的密码");

        //创建连接
        Connection connection = factory.newConnection();
        //获取信道
        Channel channel = connection.createChannel();
        /**
         * 生成一个队列
         * 参数一:队列名称
         * 参数二:队列里面的消息是否持久化(存储在磁盘),默认情况消息储存在内存中
         * 参数三:该队列是否只供一个消费者进行消费
         * 参数四:是否自动删除,最后一个消费者断开连接后,该队列是否自动删除
         * 参数五:其他参数
         */
        channel.queueDeclare(QUENE_NAME,false,false,false,null);

        //发消息
        String message = "hello world";
        /**
         * 发送一个消息
         * 参数一:发送到哪个交换机
         * 参数二:路由的key值是哪个,本次是队列名称
         * 参数三:其他参数
         * 参数四:发送的消息的消息体
         */
        channel.basicPublish("",QUENE_NAME,null,message.getBytes());
        System.out.println("消息发送完毕");
    }
}

运行后打开监管界面,就可以看到队列了
在这里插入图片描述
在这里插入图片描述

c)消费者代码

/**
 * 消费者,接受消息的
 */
public class Consumer {
    //接受消息的队列的名称
    public static final String QUENE_NAME = "hello";
    //接受消息
    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接工程
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("服务器ip");
        factory.setUsername("admin");
        factory.setPassword("你的密码");
        Connection connection = factory.newConnection();

        Channel channel = connection.createChannel();

        //声明消息成功消费的回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message)->{
            System.out.println(new String(message.getBody()));//输出消息体
        };
        //声明取消接受消息的回调
        CancelCallback cancelCallback = (String consumerTag)->{
            System.out.println("消息消费被中断");
        };
        /**
         * 消费者接受消息
         * 参数一:消费哪个队列
         * 参数二:消费成功之后是否自动应答
         * 参数三:消费者成功消费的回调
         * 参数四:消费者取消消费的回调
         */
        channel.basicConsume(QUENE_NAME,true,deliverCallback,cancelCallback);
    }
}

3.Work Quenes(工作队列模式)

当生产者大量发消息时,一个工作线程就不够用了,需要用多个工作队列轮训分发消息,也就是轮流处理消息,保证一个消息只能被处理一次

请添加图片描述

3.1轮训分发处理消息

a)抽取工具类,获取连接

//此类是连接工厂创建信道的工具类

public class RabbitMqUtils {
    //得到一个连接的 channel
    public static Channel getChannel() throws Exception{
        //创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("服务器ip");
        factory.setUsername("admin");
        factory.setPassword("你的密码");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        return channel;
    } }

b)工作线程(消费者)代码

/**
 * 这是一个工作线程
 */
public class Work01 {
    //队列名称
    public static final String QUENE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        //使用刚刚的工具类来获取信道连接
        Channel channel = RabbitMqUtils.getChannel();

        //声明消息成功消费的回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message)->{
            System.out.println("接受到的消息:"+new String(message.getBody()));//输出消息体
        };
        //声明取消接受消息的回调
        CancelCallback cancelCallback = (String consumerTag)->{
            System.out.println(consumerTag+"消息消费被中断");
        };

        //消息的接受
        channel.basicConsume(QUENE_NAME,true,deliverCallback,cancelCallback);
    }
}

c)启动两个工作现场
先运行main,启动第一个线程,然后点击设置
在这里插入图片描述
允许多线程,然后再次运行一次
在这里插入图片描述
运行后我们就可以看到两个线程的窗口了
在这里插入图片描述
d)生产者代码

/**
 * 生产者,可以发送大量消息
 */
public class Task01 {
    //队列名称
    public static final String QUENE_NAME = "hello";

    //发送大量消息
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        //声明队列
        channel.queueDeclare(QUENE_NAME,false,false,false,null);

        //从控制台中接受信息来发送
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String next = scanner.next();
            channel.basicPublish("",QUENE_NAME,null,next.getBytes());
            System.out.println("发送消息完成:"+next);
        }
    }
}

e)测试轮训接受,结果成功
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.2消息应答(消费者)

消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费这的消息,因为它无法接收到。
为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。

a)自动应答

这种模式是只要接受到消息就马上应答,但不考虑后续的处理代码,如果在后续处理时出现问题,消息也会处理失败并消失,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用,所以一般推荐手动应答。

b)手动应答的方法
1、Channel.basicAck(用于肯定确认):RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了
2、Channel.basicNack(用于否定确认)
3、Channel.basicReject(用于否定确认),与 Channel.basicNack 相比少一个参数Multiple

Multiple:可以批量应答并且减少网络拥堵
在这里插入图片描述
下面是一个图例,Multiple一般建议设置为false来增加稳定性
在这里插入图片描述
c)消息自动重新入队
如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。

d)生产者的代码
由于应答和生产者没什么关系,所以生产者的代码没有变化

/**
 * 生产者,实现消息在手动应答时是不丢失的
 */
public class Task2 {
    //队列名称
    public static final String task_quene_name = "adk_quene";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        channel.getConnection();
        //声明队列
        channel.queueDeclare(task_quene_name,false,false,false,null);
        //从控制台中输入信息
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish("",task_quene_name,null,message.getBytes("UTF-8"));
            System.out.println("生产者发出消息:"+message);
        }
    }
}

d)消息手动应答的消费者代码
应答是由工作线程负责的,所以我们只需要修改工作线程的代码即可

C1是一个处理速度快的消费者

public class Work03 {
    //队列名称
    public static final String task_quene_name = "adk_quene";

    //接收消息
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C1等待接收消息处理时间较短");

        //声明消息成功消费的回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message)->{
            //沉睡1s,模拟一个场景,代码需要执行1s才能处理消息
            SleepUtils.sleep(1);//这个是自己写的睡眠工具类
            System.out.println("接受到的消息:"+new String(message.getBody(),"UTF-8"));
            //进行手动应答的代码,在消费成功的回调中写
            /**
             * 参数一:消息的标记tag
             * 参数二:是否批量应答
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };
        //声明取消接受消息的回调
        CancelCallback cancelCallback = (String consumerTag)->{
            System.out.println(consumerTag+"消息消费被中断");
        };

        //采用手动应答
        boolean autoACK = false;//第二个参数为false表示不自动应答
        channel.basicConsume(task_quene_name,autoACK,deliverCallback,cancelCallback);
    }
}

C2是一个处理速度慢的消费者

public class Work04 {
    //队列名称
    public static final String task_quene_name = "adk_quene";

    //接收消息
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C2等待接收消息处理时间较长");

        //声明消息成功消费的回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message)->{
            //沉睡30s,模拟一个场景,代码需要执行30s才能处理消息
            SleepUtils.sleep(30);//这个是自己写的睡眠工具类
            System.out.println("接受到的消息:"+new String(message.getBody(),"UTF-8"));
            //进行手动应答的代码,在消费成功的回调中写
            /**
             * 参数一:消息的标记tag
             * 参数二:是否批量应答
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };
        //声明取消接受消息的回调
        CancelCallback cancelCallback = (String consumerTag)->{
            System.out.println(consumerTag+"消息消费被中断");
        };

        //采用手动应答
        boolean autoACK = false;//第二个参数为false表示不自动应答
        channel.basicConsume(task_quene_name,autoACK,deliverCallback,cancelCallback);
    }
}

e)测试结果,消息没有丢失
我们以此发送aa,bb,cc,dd,由于C2模拟了一个工作30s后才能处理消息的场景,按照轮训的顺序,原本应该是C1处理aa和bb,C2处理cc和dd,接下来假设C2在处理dd之前的30秒的业务代码中宕机了,我们测试看看dd会不会丢失,测试结果是dd重新回到了队列并由C1处理
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.3RabbitMQ持久化(生产者)

消息应答可以保证在处理消息的时候消息不丢失,默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化。

a)队列持久化
在这里插入图片描述
之前我们创建的队列都是非持久化的,rabbitmq 如果重启的化,该队列就会被删除掉,如果要队列实现持久化 需要在声明队列的时候把 durable 参数设置为持久化
在这里插入图片描述
开启持久化之后,及时mq宕机重启,队列依旧存在
在这里插入图片描述
b)消息持久化
队列如果持久化了,是不能保证消息不丢失的,所以发消息的时候,如果要持久化,生产者也要通知要想让消息实现持久化需要在消息生产者修改代码,在发送消息发方法上添加MessageProperties.PERSISTENT_TEXT_PLAIN 这个属性。

在这里插入图片描述
将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果需要更强有力的持久化策略,参考发布确认章节。

3.4不公平分发处理消息(消费者)

在最开始的时候我们学习到 RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2 处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是RabbitMQ 并不知道这种情况它依然在很公平的进行分发。

采用不公平分发,简单来说就是能者多劳。

为了避免这种情况,我们可以设置参数 channel.basicQos(1);
所有的消费者都要写上,默认是0,即轮训分发
在这里插入图片描述

3.5预期值分发处理消息(消费者)

简单的来说就是指定每个消费者能处理的消息数量,不过当堆积消息达到了欲取值后,就不再给他分发新的消息了

请添加图片描述
在这里插入图片描述
在这里插入图片描述

4.发布确认(生产者)

第三章中的持久化并不能完全保证消息不丢失,假如rabbitmq服务器在将数据保存到磁盘时宕机了,此时数据也会丢失,为了确保万无一失,我们还需要进行发布确认,发布确认的前提就是开启队列的持久化以及消息的持久化。

请添加图片描述
a)开启发布确认方法
发布确认默认是没有开启的,如果要开启需要调用方法 confirmSelect,每当你要想使用发布确认,都需要在 channel 上调用该方法
在这里插入图片描述

4.1三种发布确认策略(重点:异步方式)

a)单个发布确认

这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布
这种确认方式有一个最大的缺点就是:发布速度特别的慢

    //单个确认的方法
    public static void publishMessageIndividually () throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        //队列的声明
        String queneName = UUID.randomUUID().toString();
        channel.queueDeclare(queneName,true,false,false,null);

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

        //记录开始时间
        long begin = System.currentTimeMillis();

        //批量的发消息
        for (int i=0;i<1000;i++){
            String message = i+"";
            channel.basicPublish("",queneName,null,message.getBytes());
            //单个消息马上进行发布确认,即发一个确认一次
            boolean flag = channel.waitForConfirms();//发一次确认一次
            if (flag){
                System.out.println("消息发送成功");
            }
        }

        //结束时间
        long end = System.currentTimeMillis();
        System.out.println("发布1000条单独确认消息耗时:"+(end-begin)+"ms");
    }

经过测试,耗时540ms
b)批量发布确认

与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。

    //批量确认的方法
    public static void publishMessageBatch() throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        //队列的声明
        String queneName = UUID.randomUUID().toString();
        channel.queueDeclare(queneName,true,false,false,null);

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

        //记录开始时间
        long begin = System.currentTimeMillis();

        //批量确认消息的大小
        int batchSize = 100;

        //批量发送消息并且批量发布确认
        for (int i =1;i<=1000;i++){
            String message = i+"";
            channel.basicPublish("",queneName,null,message.getBytes());
            //达到100条消息的时候批量确认一次
            if (i%batchSize==0){
                channel.waitForConfirms();//每间隔100条数据确认一次
            }
        }

        //结束时间
        long end = System.currentTimeMillis();
        System.out.println("发布1000条批量确认消息耗时:"+(end-begin)+"ms");
    }

经过测试,耗时108ms
c)异步发布确认

简单的说就是,生产者只需要一直发送消息,并给每个消息编号,消息被确认收到或者没有确认收到都会发送回调,所以生产者只管一直发消息,出错的消息可以通过回调信号确定
信价比最高,速度最快

在这里插入图片描述

    //异步确认的方法
    public static void publishMessageAsync() throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        //队列的声明
        String queneName = UUID.randomUUID().toString();
        channel.queueDeclare(queneName,true,false,false,null);

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

        //记录开始时间
        long begin = System.currentTimeMillis();

        //准备一个消息的监听器,来监听哪些消息成功了,哪些消息失败了,从而知道哪些消息需要重发
        //消息确认成功回调方法
        /**
         * 参数一:消息的标记
         * 参数二:是否为批量确认
         */
        ConfirmCallback ackCallback = (long deliveryTag, boolean multiple)->{
            System.out.println("确认的消息:"+deliveryTag);
        };
        //消息确认失败回调方法
        ConfirmCallback nackCallback = (long deliveryTag, boolean multiple)->{
            System.out.println("未确认的参数:"+deliveryTag);
        };
        /**
         * 参数一:监听哪些消息成功了
         * 参数二:监听哪些消息失败了
         */
        channel.addConfirmListener(ackCallback,nackCallback);//异步通知

        //批量发送消息,异步确认的核心代码
        for (int i = 0;i<1000;i++){
            String message = i+"";
            channel.basicPublish("",queneName,null,message.getBytes());
            //异步确认这里就不用确认了,消费者只负责发消息
        }

        //结束时间
        long end = System.currentTimeMillis();
        System.out.println("发布1000条异步确认消息耗时:"+(end-begin)+"ms");
    }

通过测试,耗时31ms

那么接下来有一个问题,我们如何处理异步未确认的消息呢?

监听器和发消息其实是两个线程,所以可以使用并发链路式队列ConcurrentLinkedQueue,在发布线程与确认回调线程之间进行消息的传递,我们在发消息时,可以将消息都存在这个队列中,收到确认就把对应的消息,最后剩下的就是未确认的消息。

下面我们修改一下代码
首先准备一个线程安全有序的哈希表

        /**
         * 线程安全有序的一个哈希表,适用于高并发的情况下
         *1、可以轻松地将序号与消息进行关联(序号为key,内容为value)
         *2、可以轻松地批量删除条目,只需要给到序号
         *3、支持高并发(多线程)
         */
        ConcurrentSkipListMap<Long,String> outstandingConfirms = new ConcurrentSkipListMap<>();

然后修改发送消息的代码
在这里插入图片描述
修改确认和未确认的回调
在这里插入图片描述

5.交换机(发布订阅模式)

我们之前的案例中,交换机传的参数都是空字符串,这代表使用默认的交换机

在这里插入图片描述
注意,一个队列中的消息只能被消费一次,那么当有不止一个对消费者需要消费同一条消息时,应该怎么办呢?这个时候,就需要使用交换机了,使用了交换机,就是发布订阅模式了。

请添加图片描述

5.1交换机的概念

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

交换机的类型:
直接(direct), 主题(topic) ,标题(headers) , 扇出(fanout)

在之前的学习中,交换机的位置我们都是使用空字符串,也就是默认交换
在这里插入图片描述
第一个参数是交换机的名称。空字符串表示默认或无名称交换机:消息能路由发送到队列中其实是由 routingKey(bindingkey)绑定 key 指定的,如果它存在的话,之前我们都是用队列的名字来绑定的,也就是第二个参数。

5.2临时队列

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

临时队列的创建方式:
String queueName = channel.queueDeclare().getQueue();

创建出来后的样子:AD,Exd都是表示临时的意思
在这里插入图片描述

5.3绑定

什么是 绑定呢,绑定其实是交换机和队列之间的桥梁,它告诉我们交换机和那个队列进行了绑定关系。比如说下面这张图告诉我们的就是 X 与 Q1 和 Q2 进行了绑定

交换机是负责接收消息的,通过其与队列之间binding的Routing key来把消息转发给相应的队列,再由队列发送给消费者

在这里插入图片描述

5.4fanout:扇出交换机(发布订阅)

Fanout 这种类型非常简单。正如从名称中猜到的那样,它是将接收到的所有消息广播到它知道的所有队列中。
也就是说交换机不管routingKey,会广播发送消息

在这里插入图片描述
此次我们模拟一个生产者两个消费者

a)消费者代码
两个消费者的代码是一模一样的,这里由于是广播,我们交换机与队列的绑定routingKey不会影响结果

/**
 * 消息接受
 */
public class ReceiveLogs01 {
    //交换机的名称
    public static final String EXCHANGE_NAME = "logs";
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        //声明一个交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");//指定名字与类型
        //声明一个临时队列
        String queue = channel.queueDeclare().getQueue();//临时队列的名称是随机的,消费者断开与队列的连接时,队列就自动删除
        //绑定交换机与队列
        channel.queueBind(queue,EXCHANGE_NAME,"");//routingKey可以不写,即空串
        System.out.println("等待接受消息......");

        //声明消息成功消费的回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message)->{
            System.out.println("C1在控制台打印接受到的消息"+new String(message.getBody()));//输出消息体
        };
        //声明取消接受消息的回调
        CancelCallback cancelCallback = (String consumerTag)->{
            System.out.println(consumerTag+"消息消费被中断");
        };


        //接受消息
        channel.basicConsume(queue,true,deliverCallback,cancelCallback);
    }
}

b)生产者代码

/**
 *生产者: 发消息给交换机
 */
public class EmitLog {
    //交换机的名称
    public static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");//这里也可以不声明,因为已经在消费者中声明了交换机

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes());
            System.out.println("生产者发出消息:"+message);
        }
    }
}

经过测试,两个消费者都能接收到消费者发送的消息

5.5direct:直接交换机(路由模式)

直接交换机与扇出交换机的区别就是:这种类型的工作方式是,消息只去到它绑定的routingKey 队列中去,也就是说不会广播消息
当然如果 exchange 的绑定类型是 direct,但是它绑定的多个队列的 key 如果都相同,在这种情况下虽然绑定类型是 direct 但是它表现的就和 fanout 有点类似了,就跟广播差不多

消费者代码:

public class ReceiveLogsDirect01 {
    //交换机的名字
    public static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        //声明一个交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"direct");//指定名字与类型
        //声明一个队列
        channel.queueDeclare("console",false,false,false,null);
        //绑定交换机与队列
        channel.queueBind("console",EXCHANGE_NAME,"info");//console队列的routingKey为info
        channel.queueBind("console",EXCHANGE_NAME,"warning");//一个队列可以多重绑定

        //声明消息成功消费的回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message)->{
            System.out.println("ReceiveLogsDirect01在控制台打印接受到的消息"+new String(message.getBody()));//输出消息体
        };
        //声明取消接受消息的回调
        CancelCallback cancelCallback = (String consumerTag)->{
            System.out.println(consumerTag+"消息消费被中断");
        };

        channel.basicConsume("console",false,deliverCallback,cancelCallback);
    }
}

生产者和之前也是一模一样的,并且在basicPublish中修改参数,只要是info或者是waring,就可以由我们上面的消费者接收
在这里插入图片描述

5.6Topic:主题交换机(最灵活)

发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表以点号分隔开。这些单词可以是任意单词,比如说:“stock.usd.nyse”, “nyse.vmw”, “quick.orange.rabbit”.这种类型的。当然这个单词列表最多不能超过 255 个字节。
*(星号)可以代替一个单词
#(井号)可以替代零个或多个单词

一个例子:
Q1–>绑定的是
中间带 orange 带 3 个单词的字符串(*.orange.*)
Q2–>绑定的是
最后一个单词是 rabbit 的 3 个单词(*.*.rabbit)
第一个单词是 lazy 的多个单词(lazy.#)
在这里插入图片描述
当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像 fanout 了
如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是 direct 了

a)消费者代码

/**
 * 消费者:声明主题交换机及相关队列
 * 消费者C1
 */
public class ReceiverLogsTopic01 {
    //交换机的名字
    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        //声明一个交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"topic");//指定名字与类型
        //声明一个队列
        channel.queueDeclare("Q1",false,false,false,null);//第一个队列Q1

        //绑定队列
        channel.queueBind("Q1",EXCHANGE_NAME,"*.orange.*");
        System.out.println("等待接受消息......");

        //声明消息成功消费的回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message)->{
            System.out.println("ReceiveLogsDirect01在控制台打印接受到的消息"+new String(message.getBody()));//输出消息体
            System.out.println("接受队列是Q1 "+"绑定的键是:"+message.getEnvelope().getRoutingKey());
        };
        //声明取消接受消息的回调
        CancelCallback cancelCallback = (String consumerTag)->{
            System.out.println(consumerTag+"消息消费被中断");
        };

        //接受消息
        channel.basicConsume("Q1",true,deliverCallback,cancelCallback);
    }
}

b)生产者代码

/**
 * 生产者
 */
public class EmitLogTopic {
    //交换机的名字
    private static final String EXCHANGE_NAME = "topic_logs";

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

        //交换机和队列都在消费者中定义过了,所以可以不写

        Map<String,String> bingingKeyMap = new HashMap<>();
        bingingKeyMap.put("quick.orange.rabbit","被队列 Q1Q2 接收到");
        bingingKeyMap.put("lazy.orange.elephant","被队列 Q1Q2 接收到");
        bingingKeyMap.put("quick.orange.fox","被队列 Q1 接收到");
        bingingKeyMap.put("lazy.brown.fox","被队列 Q2 接收到");
        bingingKeyMap.put("lazy.pink.rabbit","虽然满足两个绑定但只被队列 Q2 接收一次");
        bingingKeyMap.put("quick.brown.fox","不匹配任何绑定不会被任何队列接收到会被丢弃");

        for (Map.Entry<String, String> stringStringEntry : bingingKeyMap.entrySet()) {
            String routingKey = stringStringEntry.getKey();
            String message = stringStringEntry.getValue();
            channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes("UTF-8"));//消息中有中文,注意编码
            System.out.println("生产者发出消息"+message);
        }
    }
}

经过测试,功能正常

6.死信队列

先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,消费者从队列取出消息进行消费,但某些时候由于特定的原因导致队列中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时间未支付时自动失效(死信的延迟性)

死信的来源:
消息 TTL 过期
队列达到最大长度(队列满了,无法再添加数据到 mq 中)
消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false,通俗的来说就是拒绝应答,且不放回队列中,这个时候消息就到了死信队列中。

6.1死信实战(消息过期)

a)代码架构
在这里插入图片描述
b)消费者C1代码

启动C1创建完交换机与队列后关闭该交换机,模拟收不到消息

/**
 * 死信队列实战
 * 消费者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 Exception {
        Channel channel = RabbitMqUtils.getChannel();
        //声明交换机(死信和普通的交换机,类型为direct)
        channel.exchangeDeclare(NORMAL_EXCHANGE,"direct");
        channel.exchangeDeclare(DEAD_EXCHANGE,"direct");

        //声明普通队列(在我们的结构中,普通队列会把死信转发给死信交换机,所以在这里就要用到我们从没有用过个第五个参数了)
        Map<String,Object> arguments = new HashMap<>();
        //过期时间
        //arguments.put("x-message-ttl",100000);//单位是毫秒,也可以不设置,由生产者设置
        //正常队列设置过期之后的死信交换机是谁
        arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);//key是固定的
        //设置死信routingKey
        arguments.put("x-dead-letter-routing-key", "lisi");//key也是固定的
        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 = (String consumerTag, Delivery message)->{
            System.out.println("C1在控制台打印接受到的消息"+new String(message.getBody(),"UTF-8"));//输出消息体
        };
        //声明取消接受消息的回调
        CancelCallback cancelCallback = (String consumerTag)->{
            System.out.println(consumerTag+"消息消费被中断");
        };

        //C1通过普通队列接收消息
        channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,cancelCallback);
    }
}

c)生产者代码

/**
 * 死信队列之生产者
 */
public class Producter {
    //普通交换机的名字
    public static final String NORMAL_EXCHANGE = "normal_exchange" ;

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        //延迟消息或者死信消息 设置TTL的时间 在TTL之内消费者C1不进行接受,就会发给死信交换机
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();//注意单位是毫秒,也就是10秒后过期

        for (int i = 1; i < 11; i++) {
            String message = "info"+i;
            channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties,message.getBytes());
        }
    }
}

经过测试,运行C1创建完交换机和队列后关闭C1,再由生产者发送十条消息,结果如下
在这里插入图片描述

d)消费者C2代码
C2只需要负责接受死信队列的消息即可,所以代码非常简单,运行后,死信队列的10条消息就被C2接受了

/**
 * 死信队列实战
 * 消费者2
 */
public class Consumer02 {
    //死信队列的名称
    public static final String DEAD_QUEUE = "dead_queue" ;

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

        System.out.println("等待接收消息......");

        //声明消息成功消费的回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message)->{
            System.out.println("C2在控制台打印接受到的消息"+new String(message.getBody(),"UTF-8"));//输出消息体
        };
        //声明取消接受消息的回调
        CancelCallback cancelCallback = (String consumerTag)->{
            System.out.println(consumerTag+"消息消费被中断");
        };

        //C1通过普通队列接收消息
        channel.basicConsume(DEAD_QUEUE,true,deliverCallback,cancelCallback);
    }
}

6.2死信实战(队列达到最大长度)

首先修改生产者,去掉过期时间
在这里插入图片描述
然后修改C1,设置队列长度
在这里插入图片描述
删除原来的队列,然后运行C1再关闭,随后发10条消息,我们发现有4条消息进入了死信队列
在这里插入图片描述

6.3死信实战(消息被拒)

修改C1代码
在这里插入图片描述
发了十条消息,info1。。。。。info10,发现只有info5进入了死信队列
在这里插入图片描述

7.整合springboot

7.1基本整合

a)创建一个新的模块
在这里插入图片描述
b)添加依赖

        <!--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>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>

c)添加配置

spring.rabbitmq.host=ip地址
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=你的密码

d)创建Swagger配置类

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket webApiConfig(){
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("webApi")
                .apiInfo(webApiInfo())
                .select()
                .build();
    }
    private ApiInfo webApiInfo(){
        return new ApiInfoBuilder()
                .title("rabbitmq 接口文档")
                .description("本文档描述了 rabbitmq 微服务接口定义")
                .version("1.0")
                .contact(new Contact("enjoy6288", "http://atguigu.com", "1551388580@qq.com"))
                .build();
    }
}

7.2配置文件类

之前我们对于交换机和队列的声明,都是由消费者进行的,在整合springboot之后,就可以由配置类来完成这个注解了

a)声明交换机

    //声明一个交换机
    @Bean("")//交换机的别名,注入时有用
    public DirectExchange directExchange(){
        return new DirectExchange("交换机的名字");
    }

b)声明队列

    //声明队列
    @Bean("")//别名,注入时有用
    public Queue queue(){
        return QueueBuilder.durable().withArguments().build();//参数可以多个也可以一个
    }

8.延迟队列

延迟队列其实就是死信队列中的消息过期
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列

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

对于上面的例子,如果我们使用轮询查询,如果数据量少还可以单收,但是如果数据量大且精确度要求高,那么轮询查询或者定时器就不是很合适了

8.1延迟队列实战

a)代码架构
创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如下:
在这里插入图片描述
从P发送到C接受,经历的延时为10s或者40s,取决于经过XA还是XB

b)配置类的代码

/**
 * 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_QUEUE = "QD";

    //声明xExchange交换机
    @Bean("xExchange")//交换机的别名,注入时有用
    public DirectExchange xExchange(){
        return new DirectExchange(X_EXCHANGE);
    }
    //声明yExchange交换机
    @Bean("yExchange")//交换机的别名,注入时有用
    public DirectExchange yExchange(){
        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
    }
    //声明队列A
    @Bean("queueA")
    public Queue queueA(){
        Map<String,Object> arguments = new HashMap<>();
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        //设置死信routingKey
        arguments.put("x-dead-letter-routing-key","YD");
        //设置TTL(过期时间)
        arguments.put("x-message-ttl",10000);
        return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();//参数可以多个也可以一个
    }
    //声明队列B
    @Bean("queueB")
    public Queue queueB(){
        Map<String,Object> arguments = new HashMap<>();
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        //设置死信routingKey
        arguments.put("x-dead-letter-routing-key","YD");
        //设置TTL(过期时间)
        arguments.put("x-message-ttl",40000);
        return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();//参数可以多个也可以一个
    }
    //死信队列
    @Bean("queueD")
    public Queue queueD(){
        return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
    }

    //设置绑定关系
    @Bean
    //@Qualifier按照名称注入,也就是上面bean的别名
    public Binding queueABindingX(@Qualifier("queueA") Queue queueA,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }
    @Bean
    //@Qualifier按照名称注入,也就是上面bean的别名
    public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueB).to(xExchange).with("XB");
    }
    @Bean
    //@Qualifier按照名称注入,也就是上面bean的别名
    public Binding queueDBindingX(@Qualifier("queueD") Queue queueD,
                                  @Qualifier("yExchange") DirectExchange yExchange){
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }
}

代码完成的工作:
在这里插入图片描述
c)生产者
生产者是一个controller层组件

/**
 * 发送延迟消息
 */
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMsgController {
    @Autowired
    private RabbitTemplate rabbitTemplate;//用这个来发消息

    //开始发消息
    @GetMapping("/sengMsg/{message}")
    public void sengMsg(@PathVariable String message){
        log.info("当前时间:{},发送一条信息给两个ttl队列:{}",new Date().toString(),message);

        rabbitTemplate.convertAndSend("x","XA","消息来自ttl为10s的队列"+message);
        rabbitTemplate.convertAndSend("x","XB","消息来自ttl为40s的队列"+message);
    }
}

d)消费者
消费者是一个监听器

@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);
    }
}

在浏览器中发送请求:http://localhost:8080/ttl/sendMsg/嘻嘻嘻,测试成功
在这里插入图片描述

8.2延迟队列优化(基于死信存在排队的问题)

上面的队列存在一个很大的缺陷,每增加一个新的时间需求,就要新增一个队列,这里只有 10S 和 40S两个时间选项,如果需要一个小时后处理,那么就需要增加 TTL 为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?

a)代码架构:
QC是一个不设置ttl的通用队列,由生产者决定ttl
在这里插入图片描述
b)配置类代码
声明队列的方式很相似,只是不设置TTL了而已

    //延迟队列优化
    //普通队列名称
    public static final String QUEUE_C = "QC";
    //声明QC
    @Bean("queueC")
    public Queue queueC(){
        Map<String,Object> arguments = new HashMap<>();
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        //设置死信routingKey
        arguments.put("x-dead-letter-routing-key","YD");
        //这里就不设置TTL了
        return QueueBuilder.durable(QUEUE_C).withArguments(arguments).build();
    }
    @Bean
    public Binding queneCBindingX(@Qualifier("queueC") Queue queueC,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueC).to(xExchange).with("XC");
    }

c)生产者
生产者不仅要发消息,还要设置过期时间ttl

    //开始发消息
    @GetMapping("/sendExpriationMsg/{message}/{ttlTime}")
    public void sengMsg(@PathVariable String message,@PathVariable String ttlTime){
        log.info("当前时间:{},发送一条时长{}毫秒的信息给ttl队列:{}",new Date().toString(),ttlTime,message);
        
        //发消息的时候发ttl
        //第四个参数是MessagePostProcessor类的lambda表达式形式
        rabbitTemplate.convertAndSend("x","XC",message,msg->{
            msg.getMessageProperties().setExpiration(ttlTime);
            return msg;
        });
    }

消费者我们用原先的消费者即可,发送下面的两个链接
http://localhost:8080/ttl/sendExpriationMsg/你好1/20000
http://localhost:8080/ttl/sendExpriationMsg/你好2/2000

测试结果却让我们很失望,原先延迟2秒的消息,却也过了20秒才收到,所以我们发现基于死信队列存在一个排队的问题
因为 RabbitMQ 只会检查第一个消息是否过期,不会检测第二个,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行
在这里插入图片描述

8.3基于插件的延迟队列(最优)

我们使用基于插件的延迟队列去解决基于死信的延迟队列的问题

a)插件的安装
前往官网下载rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。

1、将opt下的插件移动到RabbitMQ的插件目录/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins

2、通过cd进入该目录

3、运行rabbitmq-plugins enable rabbitmq_delayed_message_exchange执行下面命令让该插件生效,然后重启 RabbitMQ
在这里插入图片描述
在web监控界面添加交换机中,出现下面的选项说明安装成功,这就意味着这个插件,延迟消息由交换机完成延迟的,而基于死信的延迟队列,是使用死信队列完成延迟的
在这里插入图片描述
基于插件的延迟原理非常简单:
请添加图片描述
b)代码架构图

在这里新增了一个队列 delayed.queue,一个自定义交换机 delayed.exchange,绑定关系如下:
在这里插入图片描述
b)配置类代码

@Configuration
public class DelayedQueueConfig {
    //队列名
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    //交换机
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    //routingKey
    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";

    //声明交换机
    @Bean
    public CustomExchange delayedExchange(){
        Map<String,Object> arguments = new HashMap<>();
        arguments.put("x-delayed-type","direct");//用那种匹配模式,也可以用topic
        return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,arguments);//名字,类型,是否要持久化,是否自动删除,参数
    }

    //队列
    @Bean
    public Queue delayedQueue(){
        return new Queue(DELAYED_QUEUE_NAME);
    }
    
    @Bean
    public Binding delayedBingingDelayedExchange(@Qualifier("delayedQueue")Queue queue,
                                                 @Qualifier("delayedExchange") CustomExchange customExchange){
        return BindingBuilder.bind(queue).to(customExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

c)生产者代码

    //开始发消息(基于插件的) 发送消息及时间
    @GetMapping("/sendDelayMsg/{message}/{delayTime}")
    public void sengMsg(@PathVariable String message,@PathVariable Integer delayTime){
        log.info("当前时间:{},发送一条时长{}毫秒的信息给延迟队列:{}",new Date().toString(),delayTime,message);

        rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME,DelayedQueueConfig.DELAYED_ROUTING_KEY,message, msg->{
            //设置延迟时间,单位也是毫秒
            msg.getMessageProperties().setDelay(delayTime);
            return msg;
        });
    }

d)消费者代码

/**
 * 消费者 消费基于插件的延迟消息
 */
@Slf4j
@Component
public class DelayQueueConsumer {
    //监听消息
    @RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
    public void receiveDelayQueue(Message message) throws UnsupportedEncodingException {
        String msg = new String(message.getBody(),"UTF-8");
        log.info("当前时间:{},收到延迟队列的消息:{}",new Date().toString(),msg);
    }
}

发送两个请求来测试
http://localhost:8080/ttl/sendDelayMsg/come on baby1/20000
http://localhost:8080/ttl/sendDelayMsg/come on baby2/2000
基于插件的就很好的解决了基于死信队列的延迟问题
在这里插入图片描述

8.4总结

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

9.发布确认高级

在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢?特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理
前面讲的发布确认是MQ没有宕机的情况下,消息不管成功失败都会给生产者这边回应,现在高级发布确认就是MQ宕机了无法给回应

这个时候就需要使用缓存来防止消息丢失了
在这里插入图片描述

9.1发布确认springboot版本

a)代码架构图
在这里插入图片描述
b)配置类

@Configuration
public class ConfirmConfig {
    //交换机
    public static final String CONFIRM_EXCHANGE_NAME = "confirm_exchange";
    //队列
    public static final String CONFIRM_QUEUE_NAME = "confirm_queue";
    //routingKey
    public static final String CONFIRM_ROUTING_KEY = "key1";

    //交换机声明
    @Bean
    public DirectExchange confirmExchange(){
        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
    }
    
    @Bean
    public Queue confirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }
    
    @Bean
    public Binding queueBingExchange(@Qualifier("confirmQueue") Queue confirmQueue,
                                     @Qualifier("confirmExchange")DirectExchange confirmExchange) {
        return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);//不是自定义交换机就不需要加noargs
    }
}

c)生产者

@RequestMapping("/confirm")
@RestController
@Slf4j
public class ProducterController {
    @Autowired
    RabbitTemplate rabbitTemplate;
    
    //发消息
    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message){
        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,message);
        log.info("发送消息内容为:{}",message);
    }
}

d)消费者

@Slf4j
@Component
public class ConfirmConsumer {
    @RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
    public void receiveConfirmMessage(Message message) throws UnsupportedEncodingException {
        String msg = new String(message.getBody(),"UTF-8");
        log.info("接受到的队列confirm.queue的消息:{}",msg);
    }
}

在正常情况下,如果不出现问题,那么发消息和接收消息都是完全没有问题的
> 那么接下来就是如何使用springboot实现发布确认

e)回调接口

我们实现的是一个内部接口,所以需要使用 @PostConstruct注解将我们的这个配置类注入到RabbitTemplate 中

@Slf4j
@Component
//继承这个内部的接口
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
    
    //注入
    @Autowired 
    RabbitTemplate rabbitTemplate;
    
    @PostConstruct//在bean创建和属性赋值完成后进行的初始化方法
    public void init(){
        //虽然实现了内部接口,但是rabbitTemplate要调用,需要将容器里的引用指向我们实现的组件
        rabbitTemplate.setConfirmCallback(this);
    }

    /**
     * 1、发消息 交换机接受到了 回调 
     * 参数一:保存回调消息的ID以及相关信息
     * 参数二:接收到了就是true
     * 参数三:接收到了就是null
     * 2、发消息 交换机接受失败了 回调
     * 参数一:保存回调消息的ID以及相关信息
     * 参数二:false
     * 参数三:失败的原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (b==true){
            log.info("交换机收到了id为{}的消息",id);
        }else {
            log.info("交换机还未收到id为{}的消息,原因是:{}",id,s);
        }
    }
}

f)交换机确认
在我们上面的回调接口中,CorrelationData 这个参数并不是一直有的,需要修改发送者的发送方法
在这里插入图片描述
在这里插入图片描述
g)修改配置文件
在配置文件当中需要添加
spring.rabbitmq.publisher-confirm-type=correlated
有三个参数:
⚫ NONE
禁用发布确认模式,是默认值
⚫ CORRELATED
发布消息成功到交换器后会触发回调方法
⚫ SIMPLE(相当于我们之前的单个发布确认)
经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker

> 这个回调接口是针对于交换机的,交换机无论是收到还是没收到消息,都会回调,但是不能收到队列的回调,所以下面我们需要队列也能回调

h)回退消息

在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由(发送给队列),那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。

首先在配置文件中打开:

spring.rabbitmq.publisher-returns=true

然后在我们刚刚的回调接口中再实现回退借接口
在这里插入图片描述

然后重写他的方法

    //可以在当消息传递过程中不可达到目的地时,将消息返回给生产者
    //只要不可达目的地时,才回退
    //在高版本中这个方法已经过时了,这五个参数被封装成了一个对象,调用get方法即可
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        try {
            log.info("消息{},被交换机{}退回,退回原因:{},路由key:{}",
                    new String(message.getBody(),"UTF-8"),exchange,replyText,routingKey);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        RabbitTemplate.ReturnsCallback.super.returnedMessage(message, replyCode, replyText, exchange, routingKey);
    }

最后别忘了注入
在这里插入图片描述

9.2备份交换机

备用交换机的用处,一旦消息发送到交换机或队列错误,也可以走备用交换机,内容交换机的两个队列一个可以备份消息,一个可以报警,也就是说不用在生产者处进行重新发送了。
备份
交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。

a)代码架构图
在这里插入图片描述

b)修改配置类
在之前的案例中,我们已经写过confirm.exchange交换机了,我们只需要添加一个交换机,两个队列即可

    //备份交换机
    public static final String BACKUP_EXCHANGE_NAME = "backup_exchange";
    //备份队列
    public static final String BACKUP_QUEUE_NAME = "backup_queue";
    //报警队列
    public static final String WARNING_QUEUE_NAME = "warning_queue";
    @Bean
    public FanoutExchange backupExchange(){
        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }
    @Bean
    public Queue backupQueue(){
        return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
    }
    @Bean
    public Queue warningQueue(){
        return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
    }
    @Bean
    public Binding backupQueueBindingbackupExchange(@Qualifier("backupQueue") Queue backupQueue,
                                     @Qualifier("backupExchange")FanoutExchange backupExchange) {
        return BindingBuilder.bind(backupQueue).to(backupExchange);//删除类型是没有routingKey的
    }
    @Bean
    public Binding warningQueueBindingbackupExchange(@Qualifier("warningQueue") Queue warningQueue,
                                                    @Qualifier("backupExchange")FanoutExchange backupExchange) {
        return BindingBuilder.bind(warningQueue).to(backupExchange);
    }

需要加一个确认交换机转发到备份交换机的步骤。
在这里插入图片描述
c)报警消费者

/**
 * 报警消费者
 */
@Slf4j
@Component
public class WarningConsumer {
    //接受报警消息
    @RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
    public void receiveWarningMsg(Message message) throws UnsupportedEncodingException {
        String msg = new String(message.getBody(),"UTF-8");
        log.error("报警发现不可路由的消息:{}",msg);
    }
}

运行以后,那些接受失败的消息,就会通过备份交换机,到备份队列和警告队列中,其优先级高于上面的消息回退,因为消息被备份消费者消费了,所以就不会回退了

10、RabbitMQ其他知识点

10.1幂等性

a)概念
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

b)消息重复消费
消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断,故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。

c)解决思路
1、MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消息时用该 id 先判断该消息是否已消费过。
2、利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费

10.2优先级队列

a)使用场景
在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tmall商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级,否则就是默认优先级。

b)添加优先级队列
进入web页面中的添加队列
在这里插入图片描述
java代码:
配置代码

@Slf4j
@Component
public class PriorityConfig {

    public static final String EXCHANGE = "priority-exchange";

    public static final String QUEUE = "priority-queue";

    public static final String ROUTING_KEY = "priority_key";

    /**
     * 定义优先级队列
     */
    @Bean
    Queue queue() {
        Map<String, Object> args= new HashMap<>();
        args.put("x-max-priority", 10); //设置优先级,范围0-255,此处设为10,则允许优先级的范围为0-10
        return new Queue(QUEUE, false, false, false, args);
    }

    /**
     * 定义交换器
     */
    @Bean
    DirectExchange exchange() {
        return new DirectExchange(EXCHANGE);
    }


    @Bean
    Binding binding(Queue queue, DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY);
    }

}

生产者

    /**
     * 模拟发送多条数据
     */
    @GetMapping("/sendPriorityMessage")
    public void sendPriorityMessage(){
        String message = "";
        for (int i = 0; i < 10;i++){
            message = "info" + i;
            if (i == 5){ // i= 5 设置优先级为10 ,优先级也可以作为形参接受
                rabbitTemplate.convertAndSend(PriorityConfig.EXCHANGE,PriorityConfig.ROUTING_KEY,message,msg -> {
                    msg.getMessageProperties().setPriority(10);
                    return msg;
                });
            }else {
                rabbitTemplate.convertAndSend(PriorityConfig.EXCHANGE,PriorityConfig.ROUTING_KEY,message,msg -> {
                    msg.getMessageProperties().setPriority(5); 
                    return msg;
                });
            }
        }

        log.info("发出消息success");
    }

10.3惰性队列

消息正常情况下是保存在内存中的,而惰性队列的消息是保存在磁盘中的,所以消费速度较慢, 它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。、

两种模式:
队列具备两种模式:default 和 lazy。默认的为 default 模式,在 3.6.0 之前的版本无需做任何变更。lazy模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,也可以通过Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。

Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");

在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用内存是 1.2GB,而惰性队列仅仅占用 1.5MB

11.RabbitMQ集群

最开始我们介绍了如何安装及运行 RabbitMQ 服务,不过这些是单机版的,无法满足目前真实应用的要求。如果 RabbitMQ 服务器遇到内存崩溃、机器掉电或者主板故障等情况,该怎么办?单台 RabbitMQ服务器可以满足每秒 1000 条消息的吞吐量,那么如果应用需要 RabbitMQ 服务满足每秒 10 万条消息的吞吐量呢?购买昂贵的服务器来增强单机 RabbitMQ 务的性能显得捉襟见肘,搭建一个 RabbitMQ 集群才是解决实际问题的关键.

架构
请添加图片描述

11.1搭建集群

1、修改 3 台机器的主机名称
vim /etc/hostname

2.配置各个节点的 hosts,让各个节点都能互相识别对方(ip地址+主机名)
vim /etc/hosts
10.211.55.74 node1
10.211.55.75 node2
10.211.55.76 node3
在这里插入图片描述
3、以确保各个节点的 cookie 文件使用的是同一个值
在 node1 上执行远程操作命令
scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie
scp /var/lib/rabbitmq/.erlang.cookie root@node3:/var/lib/rabbitmq/.erlang.cookie

4、启动 RabbitMQ 服务,顺带启动 Erlang 虚拟机和 RbbitMQ 应用服务(在三台节点上分别执行以下命令)
rabbitmq-server -detached

5、在节点 2 执行
rabbitmqctl stop_app (rabbitmqctl stop 会将 Erlang 虚拟机关闭,rabbitmqctl stop_app 只关闭 RabbitMQ 服务)
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node1
rabbitmqctl start_app (只启动应用服务)

6、在节点 3 执行
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node1
rabbitmqctl start_app

7、集群状态
rabbitmqctl cluster_status

8、需要重新设置用户
创建账号
rabbitmqctl add_user admin 123
设置用户角色
rabbitmqctl set_user_tags admin administrator
设置用户权限
rabbitmqctl set_permissions -p “/” admin “." ".” “.*”

此时打开web监管页面,发现集群创建成功
请添加图片描述

9、解除集群节点(node2 和 node3 机器分别执行)
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
rabbitmqctl cluster_status
rabbitmqctl forget_cluster_node rabbit@node2(node1 机器上执行)

11.2镜像队列

引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他 Broker 节点之上,如果集群中的一个节点失效了,队列能自动地切换到镜像中的另一个节点上以保证服务的可用性。

如果说我们不开启备份,那么一个节点创建的队列和消息是不会和其他的共享的

打开web监管页面,点击admin,点击策略
请添加图片描述
在这里插入图片描述

配置完以后,就算整个集群只剩下一台机器了 依然能消费队列里面的消息 说明队列里面的消息被镜像队列传递到相应机器里面了

12.Federation (联合交换机)的搭建步骤

使用联邦交换机,可以解决因为距离远导致的延迟问题,就比如说下图中的深证客户,要访问北京的交换机,就会有很大的延迟。

在这里插入图片描述
1.需要保证每台节点单独运行

2.在每台机器上开启 federation 相关插件
rabbitmq-plugins enable rabbitmq_federation
rabbitmq-plugins enable rabbitmq_federation_management

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值