【RabbitMQ】有关于RabbitMQ的一点小笔记

是什么

RabbitMQ是一种消息中间件,是一种处于操作系统和应用程序之间的软件。它可以利用可靠的消息传递机制进行系统和系统间的通讯,并提供了消息传递和排队机制,可以在分布式系统环境下扩展进程间的通讯。
在这里插入图片描述
它实现了AMQP协议。

AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。

安装步骤

因为RabbitMQ是采用Erlang语言开发的,所以必须先安装Erlang。
这里是在centos7.x的系统安装的。
Erlang语言和RabbitMQ有版本对应的关系,可以在这个网站查看他们的按照比较。

安装步骤:

  1. 下载并解压Erlang语言安装包
wget https://packages.erlang-solutions.com/erlang-solutions-2.0-1.noarch.rpm
rpm -Uvh erlang-solutions-2.0-1.noarch.rpm
  1. 安装Erlang语言
yum install -y erlang
  1. 通过查看Erlang的版本号查看是否安装成功
erl -v
  1. 安装socat
yum install -y socat
  1. 下载并解压RabbitMQ安装包
wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.8.13/rabbitmq-server-3.8.13-1.el8.noarch.rpm
rpm -Uvh rabbitmq-server-3.8.13-1.el8.noarch.rpm

6.安装RabbitMQ

yum install rabbitmq-server -y

7.启动RabbitMQ服务

systemctl start rabbitmq-server

8.查看RabbitMQ服务状态

systemctl status rabbitmq-server

当状态为Active时,表示启动成功。

其他有关命令

# 停止服务
systemctl stop rabbitmq-server
# 开机启动服务
systemctl enable rabbitmq-server

有关端口

  • 5672:RabbitMQ的通讯端口
  • 25672:RabbitMQ的节点间的CLI通讯端口
  • 15672:RabbitMQ HTTP_API的端口,管理员用户才能访问,用于管理RabbitMQ,需要启动Management插件。
  • 1883,8883:MQTT插件启动时的端口。
  • 61613、61614:STOMP客户端插件启用的时候的端口。
  • 15674、15675:基于webscoket的STOMP端口和MOTT端口

安装Web界面

# 安装web界面的插件
rabbitmq-plugins enable rabbitmq_management
# 重启服务后生效
systemctl restart rabbitmq-server

重启之后访问http://ip:15672之后出现下图界面

RabbitMQ提供了一个默认账号:用户名和密码均为guest,只能在localhost下使用。如果需要远程访问则要新增一个账号,使用下列命令:

# 新增用户
rabbitmqctl add_user admin admin
# 设置用户分配操作权限
rabbitmqctl set_user_tags admin administrator
# 为用户添加资源权限
rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"
# 查看用户列表
rabbitmqctl list_users

简单使用

角色分类

  1. none:不能访问management plugin
  2. management:查看自己相关节点信息
  • 列出自己可以通过AMQP登入的虚拟机
  • 查看自己的虚拟机节点 virtual hosts的queues,exchanges和bindings信息
  • 查看和关闭自己的channels和connections
  • 查看有关自己的虚拟机节点virtual hosts的统计信息。包括其他用户在这个节点virtual hosts中的活动信息。
  1. Policymaker:包含management的所有权限
  • 查看和创建和删除自己的virtual hosts所属的policies和parameters信息。
  1. Monitoring:包含management的所有权限
  • 罗列出所有的virtual hosts,包括不能登录的virtual hosts。
  • 查看其他用户的connections和channels信息
  • 查看节点级别的数据如clustering和memory使用情况
  • 查看所有的virtual hosts的全局统计信息。
  1. Administrator:最高权限
  • 可以创建和删除virtual hosts
  • 可以查看,创建和删除users
  • 查看创建permisssions
  • 关闭所有用户的connections

组成部分

RabbitMQ组成部分

  • Server:又称Broker,接收客户端的连接,实现AMQP实体服务
  • Connection:管理每个到RabbitMQ的TCP网络连接
  • Channel:网络信道,几乎所有的操作都在Channel中进行,Channel是进行消息读写的通道,客户端可以建立多个Channel,每个Channel代表一个会话任务
  • Message:消息,服务与应用程序之间传送的数据,由Properties和body组成,Properties可以对消息进行修饰,比如消息的优先级,延迟等高级特性,Body则是消息体的内容.
  • Virual Host:虚拟地址,用于进行逻辑隔离,最上层的消息路由,一个虚拟主机里可以有若干个Exchange和Queue,同一个虚拟主机里面不能有相同名字的Exchange
  • Exchange:交换机,接收消息,根据路由键发送消息到绑定的队列,不具备消息存储的能力。生产者将消息发送到交换机,由交换机将消息路由到一个或多个队列中。
  • Bindings:Exchange和Queue之间的虚拟连接,binding中可以保护多个routing key
  • Routing key:是一个路由规则,虚拟机可以用它来确定如何路由一个特定消息
  • Queue:队列,也称为Message Queue消息队列,保存消息并将它们转发给消费者
  • Producer:连到RabbitMQ服务器,将消息发送到RabbitMQ服务器的队列,是消息的发送方
  • Consumer:连接到RabbitMQ则是为了消费队列中的消息,是消息的接收方

工作流程

生产者发送消息

  • 生产者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)
  • 生产者声明一个交换机,并设置相关属性,比如交换机类型、是否持久化等
  • 生产者声明一个队列并设置相关属性,比如是否排他、是否持久化、是否自动删除等
  • 生产者通过路由键将交换机和队列进行绑定
  • 生产者发送消息至RabbitMQ Broker,其中包含路由键、交换机等信息
  • 相应的交换机根据接收到的路由键查找相匹配的队列
  • 如果找到,则将从生产者发送过来的消息存入相应的队列中
  • 如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者
  • 关闭信道
  • 关闭连接

消费者接收消息

  • 消费者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)
  • 消费者向RabbitMQ Broker请求消费相应队列中的消息,可能会设置相应的回调函数,以及做一些准备工作
  • 等待RabbitMQ Broker 回应并投递相应队列中的消息,消费者接收消息。
  • 消费者确认接收到的消息
  • RabbitMQ 从队列中删除相应已经被确认的消息
  • 关闭信道
  • 关闭连接

工作模式

RabbitMQ一共有六种工作模式,分别为简单模式、工作队列模式、发布/订阅模式、路由模式、主题模式和RPC模式,RPC模式并不常用,因此我们下面只讲前面五种工作模式。

简单模式

简单模式的结构如下图所示:

这个模式有几个显著的特征:

  • 只有一个生产者,一个消费者和一个队列
  • 生产者和消费者在发送消息时,只需要指定队列名,RabbitMQ使用默认的Exchange来操作,默认的type为direct

SpringBoot导入RabbitMQ依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

eg:

package com.alone.rabbitmqdemo.simple;

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

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

/**
 * @BelongsProject: study
 * @BelongsPackage: com.alone.rabbitmq.simple
 * @Author: Alone
 * @CreateTime: 2021-03-22 14:01
 * @Description: 简单模式生产者
 */
public class Producer {

    public static void main(String[] args) {

        //所有的中间件技术都是基于TCP/IP协议基础之上构建新型的协议规范,只不过rabbitmq遵循的是amqp
        //ip port

        //1.创建连接工程
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("101.37.33.222");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        connectionFactory.setVirtualHost("/");
        Connection connection = null;
        Channel channel = null;
        try {
            //2.创建连接connection Rabbitmq为什么是基于channel去处理而不是连接?
            connection = connectionFactory.newConnection("生产者");
            //3.通过连接获取通道channel
            channel = connection.createChannel();
            //4.通过创建交换机,声明队列,绑定关系,路由key,发送消息和接收消息
            String queueName = "queue1";
            /**
             * @params1 队列名称
             * @params2 是否要持久化 durable=false 所谓持久化消息是否存盘 如果false 非持久化 true是持久化? 非持久化会存盘? 会存盘,但是会随重启服务器而消失
             * @params3 排他性,是否是独占队列
             * @params4 是否自动删除, 随着最后一个消费者消息消费完毕后是否删除队列
             * @params5 携带附加参数
             */
            channel.queueDeclare(queueName, false, false, false, null);
            //5.准备消息内容
            String message = "Hello world!";
            //6.发送消息给队列queue
            /**
             * @Param1 交换机
             * @Param2 队列,路由key
             * @Param3 消息的状态控制
             * @Param4 消息主题
             */
            channel.basicPublish("", queueName, null, message.getBytes());
            System.out.println("消息发送成功");
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        } finally {
            //7.关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
            //8.关闭连接
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

package com.alone.rabbitmqdemo.simple;

import com.rabbitmq.client.*;

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

/**
 * @BelongsProject: study
 * @BelongsPackage: com.alone.rabbitmq.simple
 * @Author: Alone
 * @CreateTime: 2021-03-22 14:01
 * @Description:  简单模式消费者
 */
public class Consumer {

    public static void main(String[] args) {
        //1.创建连接工程
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("101.37.33.222");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        connectionFactory.setVirtualHost("/");
        Connection connection = null;
        Channel channel = null;
        try {
            //2.创建连接connection
            connection = connectionFactory.newConnection("生产者");
            //3.通过连接获取通道channel
            channel = connection.createChannel();
            //4.接收消息
            String queueName = "queue1";
            channel.basicConsume(queueName, true, new DeliverCallback() {
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    System.out.println("收到消息是:" + new String(delivery.getBody(), "UTF-8"));
                }
            }, new CancelCallback() {
                @Override
                public void handle(String s) throws IOException {
                    System.out.println("发送失败了");
                }
            });
            System.out.println("开始接收消息");
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        } finally {
            //7.关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
            //8.关闭连接
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

工作模式

当生产者生产消息的速度大于消费者的消费速度时,我们可以添加一个或多个消费者来加快消费速度,这种在simple模式下增加消费者的模式,称为work模式,结构图如下图:

主要有两种模式:

  1. 轮询模式:一个消费者一条,按均分配
  2. 公平分发:根据消费者的消费能力进行公平分发,处理快的处理的多,处理慢的处理的少,按劳分配。

eg:

轮询模式

生产者

package com.alone.rabbitmqdemo.work.poll;

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

/**
 * @BelongsProject: study
 * @BelongsPackage: com.alone.rabbitmq.work.poll
 * @Author: Alone
 * @CreateTime: 2021-03-24 15:49
 * @Description:
 */
public class Producer {

    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("101.37.33.222");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("生产者");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 6: 准备发送消息的内容
            //===============================end topic模式==================================
            for (int i = 1; i <= 20; i++) {
                //消息的内容
                String msg = "学相伴:" + i;
                // 7: 发送消息给中间件rabbitmq-server
                // @params1: 交换机exchange
                // @params2: 队列名称/routingkey
                // @params3: 属性配置
                // @params4: 发送消息的内容
                channel.basicPublish("", "queue1", null, msg.getBytes());
            }
            System.out.println("消息发送成功!");
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

消费者1:

package com.alone.rabbitmqdemo.work.poll;

import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @BelongsProject: study
 * @BelongsPackage: com.alone.rabbitmq.work.poll
 * @Author: Alone
 * @CreateTime: 2021-03-24 15:50
 * @Description:
 */
public class Work1 {

    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("101.37.33.222");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("消费者-Work1");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 5: 申明队列queue存储消息
            /*
             *  如果队列不存在,则会创建
             *  Rabbitmq不允许创建两个相同的队列名称,否则会报错。
             *
             *  @params1: queue 队列的名称
             *  @params2: durable 队列是否持久化
             *  @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
             *  @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
             *  @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
             * */
            // 这里如果queue已经被创建过一次了,可以不需要定义
//            channel.queueDeclare("queue1", false, false, false, null);
            // 同一时刻,服务器只会推送一条消息给消费者
            // 6: 定义接受消息的回调
            Channel finalChannel = channel;
//            finalChannel.basicQos(1);
            finalChannel.basicConsume("queue1", true, new DeliverCallback() {
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    try {
                        System.out.println("Work1-收到消息是:" + new String(delivery.getBody(), "UTF-8"));
                        Thread.sleep(2000);
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }, new CancelCallback() {
                @Override
                public void handle(String s) throws IOException {
                }
            });
            System.out.println("Work1-开始接受消息");
            System.in.read();
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

消费者2:

package com.alone.rabbitmqdemo.work.poll;

import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @BelongsProject: study
 * @BelongsPackage: com.alone.rabbitmq.work.poll
 * @Author: Alone
 * @CreateTime: 2021-03-24 15:50
 * @Description:
 */
public class Work2 {

    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("101.37.33.222");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("消费者-Work2");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 5: 申明队列queue存储消息
            /*
             *  如果队列不存在,则会创建
             *  Rabbitmq不允许创建两个相同的队列名称,否则会报错。
             *
             *  @params1: queue 队列的名称
             *  @params2: durable 队列是否持久化
             *  @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
             *  @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
             *  @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
             * */
            // 这里如果queue已经被创建过一次了,可以不需要定义
            //channel.queueDeclare("queue1", false, true, false, null);
            // 同一时刻,服务器只会推送一条消息给消费者
            //channel.basicQos(1);
            // 6: 定义接受消息的回调
            Channel finalChannel = channel;
//            finalChannel.basicQos(1);
            finalChannel.basicConsume("queue1", true, new DeliverCallback() {
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    try {
                        System.out.println("Work2-收到消息是:" + new String(delivery.getBody(), "UTF-8"));
                        Thread.sleep(200);
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }, new CancelCallback() {
                @Override
                public void handle(String s) throws IOException {
                }
            });
            System.out.println("Work2-开始接受消息");
            System.in.read();
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

公平分发:

生产者:

package com.alone.rabbitmqdemo.work.fair;

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

/**
 * @BelongsProject: study
 * @BelongsPackage: com.alone.rabbitmq.work.poll
 * @Author: Alone
 * @CreateTime: 2021-03-24 15:49
 * @Description:
 */
public class Producer {

    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("101.37.33.222");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("生产者");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 6: 准备发送消息的内容
            //===============================end topic模式==================================
            for (int i = 1; i <= 20; i++) {
                //消息的内容
                String msg = "学相伴:" + i;
                // 7: 发送消息给中间件rabbitmq-server
                // @params1: 交换机exchange
                // @params2: 队列名称/routingkey
                // @params3: 属性配置
                // @params4: 发送消息的内容
                channel.basicPublish("", "queue1", null, msg.getBytes());
            }
            System.out.println("消息发送成功!");
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

消费者1:

package com.alone.rabbitmqdemo.work.fair;

import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @BelongsProject: study
 * @BelongsPackage: com.alone.rabbitmq.work.poll
 * @Author: Alone
 * @CreateTime: 2021-03-24 15:50
 * @Description:
 */
public class Work1 {

    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("101.37.33.222");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("消费者-Work1");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 5: 申明队列queue存储消息
            /*
             *  如果队列不存在,则会创建
             *  Rabbitmq不允许创建两个相同的队列名称,否则会报错。
             *
             *  @params1: queue 队列的名称
             *  @params2: durable 队列是否持久化
             *  @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
             *  @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
             *  @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
             * */
            // 这里如果queue已经被创建过一次了,可以不需要定义
//            channel.queueDeclare("queue1", false, false, false, null);
            // 同一时刻,服务器只会推送一条消息给消费者
            // 6: 定义接受消息的回调
            Channel finalChannel = channel;
            //一次处理多少消息?
            finalChannel.basicQos(1);
            finalChannel.basicConsume("queue1", false, new DeliverCallback() {
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    try {
                        System.out.println("Work1-收到消息是:" + new String(delivery.getBody(), "UTF-8"));
                        Thread.sleep(2000);
                        //必须要用手动应答
                        finalChannel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }, new CancelCallback() {
                @Override
                public void handle(String s) throws IOException {
                }
            });
            System.out.println("Work1-开始接受消息");
            System.in.read();
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

消费者2:

package com.alone.rabbitmqdemo.work.fair;

import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @BelongsProject: study
 * @BelongsPackage: com.alone.rabbitmq.work.poll
 * @Author: Alone
 * @CreateTime: 2021-03-24 15:50
 * @Description:
 */
public class Work2 {

    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("101.37.33.222");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("消费者-Work2");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 5: 申明队列queue存储消息
            /*
             *  如果队列不存在,则会创建
             *  Rabbitmq不允许创建两个相同的队列名称,否则会报错。
             *
             *  @params1: queue 队列的名称
             *  @params2: durable 队列是否持久化
             *  @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
             *  @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
             *  @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
             * */
            // 这里如果queue已经被创建过一次了,可以不需要定义
            //channel.queueDeclare("queue1", false, true, false, null);
            // 同一时刻,服务器只会推送一条消息给消费者
            //channel.basicQos(1);
            // 6: 定义接受消息的回调
            Channel finalChannel = channel;
            finalChannel.basicQos(1);
            finalChannel.basicConsume("queue1", false, new DeliverCallback() {
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    try {
                        System.out.println("Work2-收到消息是:" + new String(delivery.getBody(), "UTF-8"));
                        Thread.sleep(200);
                        finalChannel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }, new CancelCallback() {
                @Override
                public void handle(String s) throws IOException {
                }
            });
            System.out.println("Work2-开始接受消息");
            System.in.read();
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

公平分发需要消费者开启手动应答,关闭自动应答。
核心代码为:

channel.basicConsume("queue1", false, new DeliverCallback());
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

发布订阅模式

发布订阅模式可以将一条消息给多个消费者消费,示意图如下:

特点:

  • 在此模式下,需要指定发送到的Exchange
  • type为fanout
  • 生产者发送消息时,不需要指定队列名,Exchange会自动转发到所绑定的队列

核心代码:

//生产者中
String message = "Alone";
String exchangeName = "fanout-exchange";
String routingKey = "";
channel.basicPublish(exchangeName, routingKey, null, message.getBytes());

路由模式

特点:

  • 路由模式下Exchange的type为direct。
  • 消息的目标队列可以由生产者按照routingKey规则指定。
  • 消费者通过BindingKey绑定自己所关心的队列。
  • 一条消息队可以被多个消息者获取。
  • 只有RoutingKey与BindingKey相匹配的队列才会收到消息。

RoutingKey用于生产者指定Exchange最终将消息路由到哪个队列,BindingKey用于消费者绑定到某个队列。

示意图如下所示:

核心代码:

//生产者中
String message = "Alone";
String exchangeName = "direct-exchange";
String routingKey1 = "testKey";
String routingKey2 = "testKey2";
channel.basicPublish(exchangeName, routingKey1, null, message.getBytes());
channel.basicPublish(exchangeName, routingKey2, null, message.getBytes());

主题模式

主题模式是在路由模式的基础上,将路由键和某模式进行匹配。其中#表示匹配多个词,*表示匹配一个词,消费者可以通过某种模式的BindKey来达到订阅某个主题消息的目的。主题模式Exchange的type取值为topic。

示意图:

核心代码:

//生产者中
String message = "Alone";
String exchangeName = "topic-exchange";
String routingKey1 = "com.course.order";
String routingKey2 = "com.order.user";
channel.basicPublish(exchangeName, routingKey1, null, message.getBytes());

与SpringBoot整合

以fanout模式为例:

生产者:

  1. pom.xml中引入依赖
<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>
  1. 配置application.yml文件:
# 服务端口
server:
  port: 8080
# 配置rabbitmq服务
spring:
  rabbitmq:
    username: admin
    password: admin
    virtual-host: /
    host: 101.37.33.222
    port: 5672
  1. 定义生产者
package com.alone.rabbitmq.springbootorderrabbitmqproducer.service;


import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.UUID;

/**
 * @BelongsProject: study
 * @BelongsPackage: com.alone.rabbitmq.springbootorderrabbitmqproducer.service
 * @Author: Alone
 * @CreateTime: 2021-03-24 20:07
 * @Description:
 */
@Component
public class OrderService {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    // 1: 定义交换机
    private String exchangeName = "fanout_order_exchange";
    // 2: 路由key
    private String routeKey = "";
    public void makeOrder(Long userId, Long productId, int num) {
        // 1: 模拟用户下单
        String orderNumer = UUID.randomUUID().toString();
        // 2: 根据商品id productId 去查询商品的库存
        // int numstore = productSerivce.getProductNum(productId);
        // 3:判断库存是否充足
        // if(num >  numstore ){ return  "商品库存不足..."; }
        // 4: 下单逻辑
        // orderService.saveOrder(order);
        // 5: 下单成功要扣减库存
        // 6: 下单完成以后
        System.out.println("用户 " + userId + ",订单编号是:" + orderNumer);
        // 发送订单信息给RabbitMQ fanout
        rabbitTemplate.convertAndSend(exchangeName, routeKey, orderNumer);
    }
}

4.绑定关系

package com.alone.rabbitmq.springbootorderrabbitmqproducer.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @BelongsProject: study
 * @BelongsPackage: com.alone.rabbitmq.springbootorderrabbitmqproducer.config
 * @Author: Alone
 * @CreateTime: 2021-03-24 20:12
 * @Description:
 */
@Configuration
public class RabbitMqConfiguration {

    private static final String exchangeName = "fanout_order_exchange";

    //1.声明注册fanout模式的交换机
    @Bean
    public FanoutExchange fanoutExchange() {
    	// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
        // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
        // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
        //   return new Queue("TestDirectQueue",true,true,false);
        //一般设置一下队列的持久化就好,其余两个就是默认false
        return new FanoutExchange(exchangeName, true, false);
    }
    //2.声明队列sms.fanout.queue email.fanout.queue duanxin.fanout.queue
    @Bean
    public Queue smsQueue() {
        return new Queue("sms.fanout.queue", true);
    }

    @Bean
    public Queue duanxinQueue() {
        return new Queue("duanxin.fanout.queue", true);
    }

    @Bean
    public Queue emailQueue() {
        return new Queue("email.fanout.queue", true);
    }
    //3.完成绑定关系(队列和交换机完成绑定关系)
    @Bean
    public Binding smsBingding() {
        return BindingBuilder.bind(smsQueue()).to(fanoutExchange());
    }
    @Bean
    public Binding duanxinBingding() {
        return BindingBuilder.bind(duanxinQueue()).to(fanoutExchange());
    }
    @Bean
    public Binding emailBingding() {
        return BindingBuilder.bind(emailQueue()).to(fanoutExchange());
    }

}

5.测试:

package com.alone.rabbitmq.springbootorderrabbitmqproducer;


import com.alone.rabbitmq.springbootorderrabbitmqproducer.service.OrderService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringbootOrderRabbitmqProducerApplicationTests {

    @Autowired
    private OrderService orderService;

    @Test
    void contextLoads() {
        orderService.makeOrder("1", "1", 12);
    }
}

消费者:

邮件服务:

package com.alone.rabbitmq.springbootorderrabbitmqconsumer.service.fanout;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

/**
 * @BelongsProject: study
 * @BelongsPackage: com.alone.rabbitmq.springbootorderrabbitmqconsumer.service.fanout
 * @Author: Alone
 * @CreateTime: 2021-03-24 21:11
 * @Description:
 */
@RabbitListener(queues = {"email.fanout.queue"})
@Service
public class FanoutEmailConsumer {

    @RabbitHandler
    public void receiveMessage(String message) {
        System.out.println("email fanout ---> 接收到了订单信息是: ->" + message);
    }
}

短信服务:

package com.alone.rabbitmq.springbootorderrabbitmqconsumer.service.fanout;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

/**
 * @BelongsProject: study
 * @BelongsPackage: com.alone.rabbitmq.springbootorderrabbitmqconsumer.service.fanout
 * @Author: Alone
 * @CreateTime: 2021-03-24 21:11
 * @Description:
 */
@RabbitListener(queues = {"duanxin.fanout.queue"})
@Service
public class FanoutDuanxinConsumer {

    @RabbitHandler
    public void receiveMessage(String message) {
        System.out.println("duanxin fanout ---> 接收到了订单信息是: ->" + message);
    }
}

微信服务:

package com.alone.rabbitmq.springbootorderrabbitmqconsumer.service.fanout;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

/**
 * @BelongsProject: study
 * @BelongsPackage: com.alone.rabbitmq.springbootorderrabbitmqconsumer.service.fanout
 * @Author: Alone
 * @CreateTime: 2021-03-24 21:11
 * @Description: 消息队列消费者
 */
@RabbitListener(queues = {"sms.fanout.queue"})
@Service
public class FanoutSMSConsumer {

    @RabbitHandler
    public void receiveMessage(String message) {
        System.out.println("sms fanout ---> 接收到了订单信息是: ->" + message);
    }
}

direct模式需要在绑定的时候指定路由:

public Binding bindingDirect(){
	return BindingBuilder.bind(emailQueue()).to(directOrderExchange()).with("");
}

消费者使用注解绑定队列eg:

@Service
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "sms.topic.queue", durable = "true", autoDelete = "false"),
        exchange = @Exchange(value = "topic_order_exchange", type = ExchangeTypes.TOPIC),
        key = "com.#"
))
public class TopicSMSConsumer {

    @RabbitHandler
    public void receiveMessage(String message) {
        System.out.println("sms topic ---> 接收到了订单信息是: ->" + message);
    }
}

关于创建队列时的附加参数的使用

在创建队列时的queueDeclare()方法中的最后一个参数是可以用来传递一些队列附加参数,下面记录一下这个参数有关的使用:

设置过期时间

过期时间有两种设置方式:

  1. 通过队列属性设置,此时队列中所有消息都有相同的过期时间
@Bean
public Queue directTTLQueue() {
    //设置过期时间
    Map<String, Object> args = new HashMap<>();
    args.put("x-message-ttl", 5000);//这里一定是int类型
    return new Queue("ttl.direct.queue", true, false, false, args);
}
  1. 对消息进行单独设置,每条消息TTL可以不同
public void makeOrderTTLMessage(String userId, String productId, int num) {
        //1.根据商品id查询库存是否充足
        //2.保存订单
        String orderId = UUID.randomUUID().toString();
        System.out.println("订单生成成功:" + orderId);
        //3.通过MQ来完成消息分发
        //参数1: 交换机 参数2:路由key/queue队列名称 参数3:消息内容
        String exchangeName = "ttl_direct_exchange";
        String routingKey = "ttlmessage";
        //给消息设置过期时间
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //这里就是字符串
                message.getMessageProperties().setExpiration("5000");
                message.getMessageProperties().setContentEncoding("UTF-8");
                return message;
            }
        };
        rabbitTemplate.convertAndSend(exchangeName, routingKey, orderId, messagePostProcessor);
    }

如果上述两种方法同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就被投递到死信队列,消费者再也无法收到。

死信队列

DLX,全称为Dead-Letter-Exchange,可以称之为死信交换机。当消息在一个队列中变成死信之后,它能重新发送到另一个交换机中,这个交换机就是DLX,绑定DLX的队列就称为死信队列。

消息变成死信的原因:

  • 消息被拒绝
  • 消息过期
  • 队列达到最大长度

当队列中存在死信时,RabbitMQ就会自动将这个消息重新发布到设置的DLX上,进而被路由到死信队列。

//设置死信队列参数
args.put("x-dead-letter-exchange", "dead_direct_exchange");
args.put("x-dead-letter-routing-key", "dead"); //fanout模式不需要配置

原理

如何确保消息发送和接收

消息发送确认

  1. ConfirmCallback方法
    ConfirmCallback是一个回调接口,消息发送到Broker后触发回调,确认消息是否到达Broker服务器,也就是只确认是否正确到达Exchange中。
@Slf4j
@Component
public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback {

    /**
     * 消息从producer到broker的确认模式
     * @param correlationData 对象内部只有一个id属性,表示当前消息的唯一性
     * @param ack 消息投递到broker的状态,true代表成功
     * @param cause 投递失败的原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if(!ack) {
            log.error("消息发送异常!");
        } else {
            log.info("生产者已经收到确认:correlationData={}, ack={}, cause={}", correlationData.getId(), ack, cause);
        }
    }
}
  1. ReturnCallback方法
    通过实现ReturnCallback接口,启动消息失败返回,此接口是在交换器路由不到队列时触发回调,该方法可以不使用,因为交换器和队列是在代码里绑定的,如果消息成功投递到Broker后几乎不存在绑定队列失败,除非你代码写错了。
@Slf4j
@Component
public class ReturnCallbackService implements RabbitTemplate.ReturnsCallback {

    /**
     * 如果消息从exchange投递到queue投递失败的退回模式
     * @param returnedMessage
     */
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        // 方法包含五个参数:message-消息体,replyCode-响应code,replyText-响应内容,exchange-交换机,routingKey-队列
        log.info("returnedMessage:replyCode={},replyText={},exchange={},routingKey={},message={}",
                returnedMessage.getReplyCode(), returnedMessage.getReplyText(), returnedMessage.getExchange(), returnedMessage.getRoutingKey(), returnedMessage.getMessage());
    }
}

消息接收确认

消息应答机制是指:消费者在接收到消息并且处理该消息之后,告诉rabbitmq它已经处理了,rabbitmq可以把该消息删除了。

RabbitMQ消息确认机制(ACK)默认是自动确认的,自动确认会在消息发送给消费者后立即确认,即消息发送后立即被认为已经传送成功。但存在丢失消息的可能:1. 如果消息在接收到之前,消费者出现连接或者channel关闭。2. 如果消费端消费逻辑抛出异常,假如你用回滚了也只是保证了数据的一致性,但是消息还是丢了,也就是消费端没有处理成功这条消息,那么久相当于丢失了消息。

消息确认模式有:

  • AcknowledgeMode.NONE:自动确认
  • AcknowledgeMode.AUTO:根据情况确认
  • AcknowledgeMode.MANUAL:手动确认
    消费者收到消息后,手动使用Basic.Ack或Basic.Nack或Basic.Reject后,RabbitMQ收到这些消息后,才认为本次投递完成。
  • Basic.Ack命令:用于确认当前消息
  • Basic.Nack命令:用于否定当前消息
  • Basic.Reject命令:用于拒绝当前消息
    Nack,Reject后都有能力要求是否requeue消息或者进入死信队列。

手动确认要在消费者服务开启参数:spring.rabbitmq.listener. simple.acknowledge-mode=manual

@Slf4j
@RabbitListener(queues = HandAckConsumerConfiguration.QUEUE_NAME)
@Component
public class HandAckConsumer {

    @RabbitListener
    public void messageConsumer(String msg, Channel channel, Message message) throws IOException {
        // delivertag是一个唯一标识id,当一个消费者向RabbitMQ注册后,会建立起一个Channel
        // RabbitMQ会用basic.deliver方法向消费者推送消息,这个方法携带一个deliveryTag作为唯一标识,它是一个单调递增的正整数
        // multiple是控制是否可以进行批处理,如果为true,则一次性可以确认deliveryTag小于等于传入值的所有消息
        try {
            log.info("消费者收到消息:{}", msg);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            if(message.getMessageProperties().getRedelivered()) {
                log.error("消息已重复处理失败,拒绝再次接收...");
                // 拒绝消息
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
            } else {
                log.error("消息即将再次返回队列处理...");
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            }
        }
    }
}

持久化

为了确保消息不会丢失,还需要将队列和消息都设置为持久化的,不然服务停止后消息就丢了。

声明队列为持久化配置下面这个属性即可:

在这里插入图片描述
注意如果原先队列不是持久化的,需要先删除原来队列,再重新建队列。

声明消息为持久化采用下面的方式:

在这里插入图片描述

应用场景

异步

将一个操作的多个步骤,拆分开,提高效率,比如一个注册步骤,以前是这么做的:

使用消息队列后:

这样可以更快的返回结果,加快服务器响应速度。

解耦

削峰

对于高并发的系统来说,在访问高峰时,突发的流量就像洪水般向应用系统涌过来,尤其是一些高并发写操作,随时会导致数据库服务器瘫痪,无法继续提供服务。而引入消息队列则可以减少突发流量对应用系统的冲击。

其他

kafka是由producer决定msg发送到哪个queue,rabbitmq是由Exchange决定msg应该怎样发送到目标queue,这就是binding及对应策略。

参考资料

  1. 入门RabbitMQ消息队列,看这篇文章就够了
  2. 狂神RabbitMQ课程
  3. 2021年最新Java面试题,常见1000+面试题及答案精讲
  4. 用了 springboot + rabbitmq 消息确认机制,我感觉掉坑里了
  5. RabbitMQ学习笔记:mandatory、publisher-confirms、publisher-return属性区别
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值