RabbitMQ学习

1.RabbitMQ核心模式

2.RabbitMQ安装

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.启动rabbitmq服务

添加开机启动 RabbitMQ 服务
 chkconfig rabbitmq-server on
启动服务
 /sbin/service rabbitmq-server start 
查看服务状态
 /sbin/service rabbitmq-server status

3.开启 web 管理插件

rabbitmq-plugins enable rabbitmq_management

4.添加用户,新增权限

创建账号
rabbitmqctl add_user admin 123
设置用户角色
rabbitmqctl set_user_tags admin administrator
设置用户权限
set_permissions [-p <vhostpath>] <user> <conf> <write> <read>
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
用户 user_admin 具有/vhost1 这个 virtual host 中所有资源的配置、写、读权限
查看当前用户和角色
 rabbitmqctl list_users

5.使用新建用户登录(ecs云服务器关闭防火墙,新增安全组策略)

systemctl stop firewalld

3.创建简单队列模式的消息生产者和消费者

1.添加pom依赖

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

创建获取channel信道工具类

package com.study.rabbitmq.utils;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class RabbitMqUtils {

    //获取连接的channel
    public static Channel getChannel() throws Exception{
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        factory.setHost("47.97.166.96");
        factory.setUsername("admin");
        factory.setPassword("123");

        //创建连接
        Connection connection = factory.newConnection();

        //获取信道
        Channel channel = connection.createChannel();
        return channel;
    }

}

2.创建生产者

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

/**
 * 生产者,发消息
 */
public class Producer {

    //队列名称
    public static final String QUEUE_NAME="hello";
    
    //发消息
    public static void main(String[] args) throws Exception{
        
        //获取信道
        Channel channel = RabbitMqUtils.getChannel();

        //创建队列
        /**
         * 1.队列名称
         * 2.消息是否持久化,默认存储在内存中
         * 3.该队列是否只供一个消费者消费,是否进行消息共享
         * 4.是否自动删除,最后一个消费者断开后,队列是否自动删除
         * 5.其他参数
         */
        channel.queueDeclare(QUEUE_NAME,true,false,false,null);

        //发消息
        String msg = "hello word";

        channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
        System.out.println("send success");

    }

}

3.创建消费者


import com.rabbitmq.client.*;

/**
 * 消费者
 */
public class Consumer {

    //队列名称
    public static final String QUEUE_NAME="hello";

    //消费消息
    public static void main(String[] args) throws Exception{

        //获取信道
        Channel channel = RabbitMqUtils.getChannel();

        //申明接受消息处理函数
        DeliverCallback deliverCallback = (var1,  message) -> {
            System.out.println("消息:"+message.getBody());
        };

        //取消消息回调
        CancelCallback cancelCallback = (var1) ->{
            System.out.println("消息消费被中断");
        };

        /**
         *消费消息
         * 1.队列
         * 2.是否自动ack应答
         * 3.为成功消费的回调
         * 4.消费者取消消费的回调
         */
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    }

}

4.测试,启动生产者发送消息

5.启动消费者

4.Work Queues工作队列

  • 即多个消费者(工作线程)同时消费消息,RabbitMQ轮训分发消息给多个工作线程,保证每个消息只被消费一次,避免消息的重复消费。

一个工作线程大量发送消息,两个工作线程轮训接受消息测试

1.创建两个工作线程(消费者)

package com.study.rabbitmq.workeQueue;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.study.rabbitmq.utils.RabbitMqUtils;

/**
 * 工作线程01(消费者01)
 */
public class Worker01 {

    //队列名称
    public static final String QUEUE_NAME="hello";

    //接受消息
    public static void main(String[] args) throws Exception{

        Channel channel = RabbitMqUtils.getChannel();

        //申明接受消息
        DeliverCallback deliverCallback = (var1, message) -> {
            System.out.println("Worker01接受的消息:"+ new String(message.getBody()));
        };

        //取消消息回调
        CancelCallback cancelCallback = (var1) ->{
            System.out.println(var1+"消息消费被中断");
        };

        /**
         *消费消息
         * 1.队列
         * 2.是否自动ack应答
         * 3.为成功消费的回调
         * 4.消费者取消消费的回调
         */
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);

    }

}

package com.study.rabbitmq.workeQueue;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.study.rabbitmq.utils.RabbitMqUtils;

/**
 * 工作线程02(消费者02)
 */
public class Worker02 {

    //队列名称
    public static final String QUEUE_NAME="hello";

    //接受消息
    public static void main(String[] args) throws Exception{

        Channel channel = RabbitMqUtils.getChannel();

        //申明接受消息
        DeliverCallback deliverCallback = (var1, message) -> {
            System.out.println("Worker02接受的消息:"+ new String(message.getBody()));
        };

        //取消消息回调
        CancelCallback cancelCallback = (var1) ->{
            System.out.println(var1+"消息消费被中断");
        };

        /**
         *消费消息
         * 1.队列
         * 2.是否自动ack应答
         * 3.为成功消费的回调
         * 4.消费者取消消费的回调
         */
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);

    }

}

2.创建生产者,发送大量消息

package com.study.rabbitmq.workeQueue;

import com.rabbitmq.client.Channel;
import com.study.rabbitmq.utils.RabbitMqUtils;

/**
 * 生产者发送大量消息
 */
public class SendMsg01 {

    //队列名称
    public static final String QUEUE_NAME="hello";

    //发送大量消息
    public static void main(String[] args) throws Exception{

        Channel channel = RabbitMqUtils.getChannel();

        //创建队列
        /**
         * 1.队列名称
         * 2.消息是否持久化,默认存储在内存中
         * 3.该队列是否只供一个消费者消费,是否进行消息共享
         * 4.是否自动删除,最后一个消费者断开后,队列是否自动删除
         * 5.其他参数
         */
        channel.queueDeclare(QUEUE_NAME,true,false,false,null);

        for(int i=0;i<10;i++){
            String msg = "消息:"+i;
            channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
        }


    }

}

3.启动两个工作线程和生产者,观察两个工作线程的控制台,轮流消费消息打印

  • Worker01

  • Worker02

5.消息应答机制

  • RabbitMQ一旦向消费者传递一条消息,便会立即将其标记为删除,若消费者在处理消息的过程中消费者挂掉了,就会丢失正在处理的消息,以及后续发送给该消费者的其它消息,也会丢失。为了保证消息发送过程中不丢失,rabbitmq引入的消息应答机制,即消费者在接收以及处理了消息后,告诉mq已经处理完毕,mq再将该消息删除。

  • 消息应答的方式

  • .自动应答(不可靠,尽量避免使用)

  • .手动应答:

1.Channel.basicAck(用于肯定确认) 
 RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了

2.Channel.basicNack(用于否定确认) 

3.Channel.basicReject(用于否定确认) 
与 Channel.basicNack 相比少一个参数(批量处理参数),不处理该消息了直接拒绝,可以将其丢弃了

Multiple解释:

手动应答可以批量应答并且减少网络拥堵。

若为true,则会应答当前tag上以及channel上所有未应答的消息。
若为false,则只会应答当前tag上的消息。

消息的自动重新入队

  • 若消费者由于某些原因导致连接断开(通道关闭),导致消息为发送ACK确认,RabbitMQ将了解到消息未完全处理,就会将消息重新排队,若其他消费者可以处理,则会重新将消息分发给其他消费者,确保消息不丢失。

测试代码,改为手动ACK(默认自动ACK)

1.创建消息生产者

package com.study.rabbitmq.autoack;

import com.rabbitmq.client.Channel;
import com.study.rabbitmq.utils.RabbitMqUtils;

/**
 * 消息生产者
 */
public class SendTask01 {

    public static final String TASK_QUEUE_NAME = "ack_queue";

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

        //声明队列
        channel.queueDeclare(TASK_QUEUE_NAME,false,false,false,null);

        for(int i=0;i<5;i++){
            System.out.println("生产者发出消息:"+i);
            String msg = "消息:"+i;
            channel.basicPublish("",TASK_QUEUE_NAME,null,msg.getBytes());
        }

    }

}


2.创建消费者01,消费消息前睡眠1秒,采用手动ack

package com.study.rabbitmq.autoack;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.study.rabbitmq.utils.RabbitMqUtils;

/**
 * 工作线程(消费者)
 */
public class Consumer01 {

    public static final String TASK_QUEUE_NAME = "ack_queue";

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

        //申明接受消息
        DeliverCallback deliverCallback = (var1, message) -> {
            //睡眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println("Worker01接受的消息:"+ new String(message.getBody()));
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };

        //取消消息回调
        CancelCallback cancelCallback = (var1) ->{
            System.out.println(var1+"消息消费被中断");
        };

        /**
         *消费消息
         * 1.队列
         * 2.是否自动ack应答
         * 3.为成功消费的回调
         * 4.消费者取消消费的回调
         */
        boolean autoack = false;
        channel.basicConsume(TASK_QUEUE_NAME,autoack,deliverCallback,cancelCallback);
    }

}

3.创建消费者02,消费消息前睡眠10秒,采用手动ack

package com.study.rabbitmq.autoack;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.study.rabbitmq.utils.RabbitMqUtils;

/**
 * 工作线程(消费者)
 * 消息在手动应答时不丢失,放回队列重新消费
 */
public class Consumer02 {

    public static final String TASK_QUEUE_NAME = "ack_queue";

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

        //申明接受消息
        DeliverCallback deliverCallback = (var1, message) -> {
            //睡眠10秒
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
            }
            System.out.println("Worker02接受的消息:"+ new String(message.getBody()));
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };

        //取消消息回调
        CancelCallback cancelCallback = (var1) ->{
            System.out.println(var1+"消息消费被中断");
        };

        /**
         *消费消息
         * 1.队列
         * 2.是否自动ack应答
         * 3.为成功消费的回调
         * 4.消费者取消消费的回调
         */
        //手动ack确认
        boolean autoack = false;
        channel.basicConsume(TASK_QUEUE_NAME,autoack,deliverCallback,cancelCallback);
    }

}

4.测试

  • 生产者向队列发送5条消息,工作线程1迅速消费完消息0,2,4,工作线程2处理消息1时将工作线程停掉,此时工作线程由于是手动ack未向MQ发送ack,此时可以看见:

此时工作线程2未ack的两个消息将会被重新放入队列,由工作线程1消费。

此时可见本该由工作线程2处理的消息1和消息3,由于线程的终止导致MQ未收到ack,交由工作线程1继续执行。

RabbitMQ持久化

  • 在RabbitMQ服务停掉后,如何保证生产者发送过来的消息不丢失,保证持久化需要保证队列和消息都标记为持久化

1.队列持久化

//创建队列
        /**
         * 1.队列名称
         * 2.消息是否持久化,默认存储在内存中
         * 3.该队列是否只供一个消费者消费,是否进行消息共享
         * 4.是否自动删除,最后一个消费者断开后,队列是否自动删除
         * 5.其他参数
         */
        boolean durable = true;
        channel.queueDeclare(QUEUE_NAME,durable,false,false,null);

2.消息持久化

  • 要想让消息实现持久化需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN 添
    加这个属性。
        channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());

3.消息不公平分发

  • 前文的消息分发采用的是轮训分发,但是当某个消费者处理消息很快,另一个很慢时,轮询的方式会造成浪费大量的时间,为避免这种情况,可以设置参数channel.basicQos(1);
//消息不公平分发,设置消费者channel属性
channel.basicQos(1);//0:默认的轮询分发;1:不公平分发;大于等于2:预取值

4.预取值(即上文消费者设置的不公平分发的参数值)

  • 可以通过预取值来指定工作线程(消费者)的处理的消息的数量。该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量,RabbitMQ将停止在通道上传递更多消息。例如,假设在通道上有未确认的消息5、6、7,8,并且通道的预取计数设置为4,此时RabbitMQ将不会在该通道上再传递任何消息,除非至少有一个未应答的消息被ack。

发布确认原理

  • 如何保证生产者发布的消息能够正确的到达MQ?生产者将信道设置成confirm模式,信道一旦进入confirm模式,该信道上发布的所以消息都会被指派一个唯一的Id(从1开始),一旦消息被投放到指定的队列后,broker就会发送一条确认消息给生产者,包含消息的唯一id,使得生产者能够得知消息已经正确的到达队列的,如果给消息设置了持久化,那么确认消息将会在消息保存到磁盘之后发出

1.信道开启发布确认

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

2.单个消息的确认发布

  • 他是一种同步的确认发布方式,即发布一个消息后只有等他确认发布了,后续消息才能继续发布,发布速度特别慢
/**
     * 单个消息发布确认
     */
    public static void publishMessageConfirmOne() throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        //队列的声明
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName,true,false,false,null);

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

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

        //发送消息
        for(int i=0;i<1000;i++){
            String msg = "消息:"+i;
            channel.basicPublish("",queueName,null,msg.getBytes());
            //单个消息确认
            boolean b = channel.waitForConfirms();
            if (b){
                System.out.println("消息发送成功!");
            }
        }
        //结束时间
        long endrtime = System.currentTimeMillis();
        System.out.println("发布1000条消息确认耗时:"+(endrtime-statrtime)+" ms");
    }

3.批量确认发布

  • 批量发布确认能提高吞吐量,但是当消息发布出现问题,不能知道是哪个消息,必须将整个处理过程保存在内存中,以记录问题消息从而重新发布,这种方法还是同步的。
/**
     * 批量发布确认
     */
    public static void publishMessageConfirmBatch() throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        //队列的声明
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName,true,false,false,null);

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

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

        //批量发送消息,批量确认
        for(int i=0;i<1000;i++){
            String msg = "消息:"+i;
            channel.basicPublish("",queueName,null,msg.getBytes());
            if(i%100 == 0){
                //批量消息确认,每100条确认一次
                boolean b = channel.waitForConfirms();
            }
        }

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

测试结果:

4.异步确认发布(监听器实现)

  • 通过回调来实现消息的可靠传递,来保证消息发布成功,不必发一条消息的等待确认在发送下一条消息。
/**
     * 批量发布确认
     */
    public static void publishMessageConfirmAsync() throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        //队列的声明
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName,true,false,false,null);

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

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

        //消息确认成功的回调函数
        /**
         * param1:消息编号
         * param2:是否为批量确认
         */
        ConfirmCallback ackCallback = (var1,var3) -> {
            System.out.println("确认的消息编号:"+var1);
        };
        //消息确认失败的回调函数
        /**
         * param1:消息编号
         * param2:是否为批量确认
         */
        ConfirmCallback noAckCallback = (var1,var3) -> {
            System.out.println("未确认的消息编号:"+var1);
        };
        //准备消息的监听器,监听成功和失败的消息
        channel.addConfirmListener(ackCallback,noAckCallback);

        //批量发送消息,异步确认
        for(int i=0;i<1000;i++){
            String msg = "消息:"+i;
            channel.basicPublish("",queueName,null,msg.getBytes());
        }


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

测试结果:

5.异步未确认消息的处理

  • 可将未确认的消息存放在一个基于内存且能被发布线程访问的队列中,如ConcurrentLinkedQueue,这个队列可在监听器的线程和发送消息线程之间进行消息的传递。

可在消息发送时将所有消息记录到队列中,在监听器的消息确认回调方法中聪队列里删除已经确认的消息,队列剩下的消息就是未确认的消息。

/**
     * 批量发布确认
     */
    public static void publishMessageConfirmAsync() throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        //队列的声明
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName,true,false,false,null);

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

        /**
         * 记录消息的map
         * key:消息序号
         * value:消息内容
         */
        ConcurrentSkipListMap<Long, String> concurrentSkipListMap = new ConcurrentSkipListMap<Long, String>();

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

        //消息确认成功的回调函数
        /**
         * param1:消息编号
         * param2:是否为批量确认
         */
        ConfirmCallback ackCallback = (var1,var3) -> {
            if(var3){   //是否批量确认
                //删除已经确认的消息,剩下的就是未确认的消息
                ConcurrentNavigableMap<Long, String> confirmed = concurrentSkipListMap.headMap(var1);
                confirmed.clear();
            }else {
                concurrentSkipListMap.remove(var1);
            }

            System.out.println("确认的消息编号:"+var1);
        };
        //消息确认失败的回调函数
        /**
         * param1:消息编号
         * param2:是否为批量确认
         */
        ConfirmCallback noAckCallback = (var1,var3) -> {
            System.out.println("未确认的消息编号:"+var1);
        };
        //准备消息的监听器,监听成功和失败的消息
        channel.addConfirmListener(ackCallback,noAckCallback);

        //批量发送消息,异步确认
        for(int i=0;i<1000;i++){
            String msg = "消息:"+i;
            channel.basicPublish("",queueName,null,msg.getBytes());
            //记录所有的消息,消息的总和
            concurrentSkipListMap.put(channel.getNextPublishSeqNo(),msg);
        }


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

Exchange交换机

概念:

  • 消息生产者通常不会直接将消息传送到队列上,实际上生产者甚至都不知道消息被传递到哪些队列中,生产者只能将消息发送到交换机,再由交换机将消息推送到其绑定的队列上,交换机必须明确的知道如何处理这些消息,是该放入特定队列还是多个队列还是丢弃,这由交换机的类型确定

交换机类型:

  • 直接(direct)
  • 主题(topic)
  • 标题(headers)
  • 扇出(fanout)

绑定(binding):

  • binding是交换机和队列之间的桥梁,指定交换机和那些队列进行绑定。

FanOut交换机(发布订阅,routingkey相同):

  • 它是将接收到的所有消息广播给它所知道的所有队列中。

代码测试:

消费者01

package com.study.rabbitmq.fanout;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.study.rabbitmq.utils.RabbitMqUtils;

/**
 * 消息接收方01
 */
public class Receive01 {

    //交换机名称
    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,"");

        //申明接受消息
        DeliverCallback deliverCallback = (var1, message) -> {
            System.out.println("01接受的消息:"+ new String(message.getBody()));
        };

        //取消消息回调
        CancelCallback cancelCallback = (var1) ->{
            System.out.println(var1+"消息消费被中断");
        };

        /**
         *消费消息
         * 1.队列
         * 2.是否自动ack应答
         * 3.为成功消费的回调
         * 4.消费者取消消费的回调
         */
        channel.basicConsume(queue,true,deliverCallback,cancelCallback);

    }

}

消费者02

package com.study.rabbitmq.fanout;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.study.rabbitmq.utils.RabbitMqUtils;


public class Receive02 {
    //交换机名称
    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,"");

        //申明接受消息
        DeliverCallback deliverCallback = (var1, message) -> {
            System.out.println("02接受的消息:"+ new String(message.getBody()));
        };

        //取消消息回调
        CancelCallback cancelCallback = (var1) ->{
            System.out.println(var1+"消息消费被中断");
        };

        /**
         *消费消息
         * 1.队列
         * 2.是否自动ack应答
         * 3.为成功消费的回调
         * 4.消费者取消消费的回调
         */
        channel.basicConsume(queue,true,deliverCallback,cancelCallback);

    }
}

消息生产者

package com.study.rabbitmq.fanout;

import com.rabbitmq.client.Channel;
import com.study.rabbitmq.utils.RabbitMqUtils;

/**
 * 生产者
 */
public class Send02 {

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

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

        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");

        //发送消息
        for(int i=0;i<5;i++){
            String msg = "消息:"+i;
            channel.basicPublish(EXCHANGE_NAME,"",null,msg.getBytes());
            System.out.println("发送消息:"+msg);
        }

    }

}

启动测试,两个消费者的两个临时队列绑定到同一个交换机,且routingkey为“”,生产者发送到交换机的消息两个队列都能收到:

Direct直接交换机(routingkey不同):

Topics主题交换机:

  • topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单
    词列表,以点号分隔开。这些单词可以是任意单词,比如说:"stock.usd.nyse", "nyse.vmw",
    "quick.orange.rabbit".这种类型的。

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

例子:

Q1:绑定中间带 orange 带 3 个单词的字符串(*.orange.*);
Q2:最后一个单词是 rabbit 的 3 个单词(*.*.rabbit)以及第一个单词是 lazy 的多个单词(lazy.#)

quick.orange.rabbit -> 被队列 Q1Q2 接收到
lazy.orange.elephant -> 被队列 Q1Q2 接收到
quick.orange.fox -> 被队列 Q1 接收到
lazy.brown.fox -> 被队列 Q2 接收到
lazy.pink.rabbit -> 虽然满足两个绑定但只被队列 Q2 接收一次
quick.brown.fox -> 不匹配任何绑定不会被任何队列接收到会被丢弃
quick.orange.male.rabbit -> 是四个单词不匹配任何绑定会被丢弃
lazy.orange.male.rabbit -> 是四个单词但匹配 Q2

死信队列

  • 生产者将消息投递到broker或者直接到queue里,consumer从中取出消息进行消费,但是由于某些原因导致queue里的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信消息。

死信消息的来源:

  • 消息TTL(存活时间)过期
  • 队列达到最大长度,队列已满,无法添加数据到mq中
  • 消息被拒绝(basic.reject或basic.noack)并且reque=false

测试:

1.创建消费者C1,构建交换机,队列,绑定关系。

package com.study.rabbitmq.deadQueue;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
import com.study.rabbitmq.utils.RabbitMqUtils;

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

/**
 * 正常队列消费者C1
 */
public class ConsumerC1 {

    //普通交换机
    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();
        //申明死信交换机和普通交换机
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        //声明普通队列
        Map<String, Object> arguments = new HashMap<>();
        //过期时间(ms),也可在消息生产方设置消息过期时间
//        arguments.put("x-message-ttl",100000);
        //设置正常对列的死信交换机
        arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
        //设置正常对列的死信交换机的routingkey
        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");

        DeliverCallback deliverCallback = (consumerTag,message) ->{
            System.out.println("消费者C1接受消息:"+new String(message.getBody()));
        };

        //接受消息
        channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,consumerTag -> {});
    }


}

2.创建消息生产者

package com.study.rabbitmq.deadQueue;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.study.rabbitmq.utils.RabbitMqUtils;

/**
 * 消息生产者
 */
public class Producter {

    //普通交换机
    public static final String NORMAL_EXCHANGE = "normal_exchange";

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

        //发送死信消息,设置TTL时间(ms)
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();

        for (int i=0;i<10;i++){
            String msg = "发送消息:"+i;
            channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties,msg.getBytes());
        }
    }

}

3.先启动消费者C1,再停掉C1,再启动生产者发送10条消息,前10秒消息被发送到普通队列,10秒后普通队列的消息变为死信消息,被转移到死信队列中。

4.创建死信队列消费者C2

package com.study.rabbitmq.deadQueue;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.study.rabbitmq.utils.RabbitMqUtils;

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

/**
 * 死信队列消费者C2
 */
public class ConsumerC2 {

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

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        DeliverCallback deliverCallback = (consumerTag,message) ->{
            System.out.println("消费者C2接受消息:"+new String(message.getBody()));
        };

        //接受消息
        channel.basicConsume(DEAD_QUEUE,true,deliverCallback,consumerTag -> {});
    }


}

队列达到最长度属性设置:

//声明普通队列
        Map<String, Object> arguments = new HashMap<>();
        //设置正常对列的死信交换机
        //arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
        //设置正常对列的死信交换机的routingkey
        //arguments.put("x-dead-letter-routing-key","lisi");
        //设置队列的最大长度
        arguments.put("x-max-length",5);

        channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);

延迟队列

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

使用场景:

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

优化:

  • 添加一个不设置TTL的队列,消息的过期时间由生产者发送消息时指定。

缺陷:

  • 在这种情况下QC,MQ只会检查第一个消息是否过期,如果过期则丢到死信队列,当第一个消息的过期时间很长,第二个消息的过期时间很短时,第二个消息并不会优先得到执行

解决办法:安装RabbitMQ延迟队列插件

1.官网下载 https://www.rabbitmq.com/community-plugins.html,
下载rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录

2.进入 RabbitMQ 的安装目录下的 plgins 目录,启动插件。
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

此时可以看见:

创建交换机时指定交换机类型为 x-delayed-message,该类型的消息支持消息的延时投递,消息投递后并不会立即投递到队列中,而是存储在 mnesia(一个分布式数据系统)表中,当到达投递时间时,才投递到目标队列中。

//自定义交换机 我们在这里定义的是一个延迟交换机
@Bean
public CustomExchange delayedExchange() { 
    Map<String, Object> args = new HashMap<>();
    //自定义交换机的类型
    args.put("x-delayed-type", "direct");
    return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false,args);
}
posted @ 2023-05-17 21:33  StudyHardWork  阅读(10)  评论(0编辑  收藏  举报