中间件 - 消息队列 - RabbitMQ

0、RabbitMQ 简介

RabbitMQ 是一个开源的 AMQP 实现,服务器端用 Erlang 语言编写,支持多种客户端,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

1、安装

1.1 安装依赖环境

在 http://www.rabbitmq.com/which-erlang.html 页面查看安装rabbitmq需要安装erlang对应的版本

在 https://github.com/rabbitmq/erlang-rpm/releases 页面找到需要下载的erlang版本,erlang-*.centos.x86_64.rpm就是centos版本的。

复制下载地址后,使用wget命令下载
wget -P /home/download https://github.com/rabbitmq/erlang-rpm/releases/download/v21.2.3/erlang-21.2.3-1.el7.centos.x86_64.rpm

安装 Erlang

sudo rpm -Uvh /home/download/erlang-21.2.3-1.el7.centos.x86_64.rpm

安装 socat

sudo yum install -y socat

1.2 安装RabbitMQ

在官方下载页面找到CentOS7版本的下载链接,下载rpm安装包

wget -P /home/download https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.7.9/rabbitmq-server-3.7.9-1.el7.noarch.rpm

提示:可以在https://github.com/rabbitmq/rabbitmq-server/tags`下载历史版本`

安装RabbitMQ

sudo rpm -Uvh /home/download/rabbitmq-server-3.7.9-1.el7.noarch.rpm

1.3 启动和关闭

启动服务

sudo systemctl start rabbitmq-server

查看状态

sudo systemctl status rabbitmq-server

停止服务

sudo systemctl stop rabbitmq-server

设置开机启动

sudo systemctl enable rabbitmq-server

1.4 开启Web管理插件

开启插件

rabbitmq-plugins enable rabbitmq_management

说明:rabbitmq有一个默认的guest用户,但只能通过localhost访问,所以需要添加一个能够远程访问的用户。

添加用户

rabbitmqctl add_user admin admin

为用户分配操作权限

rabbitmqctl set_user_tags admin administrator

为用户分配资源权限

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

1.5 防火墙添加端口

RabbitMQ 服务启动后,还不能进行外部通信,需要将端口添加都防火墙

添加端口(5672 是程序访问端口,5671 是程序通过 SSL 访问的端口,15672 是Web管理控制台端口,25672 是服务器节点之间通信的端口; 另外还有 61613,61614 是当 STOMP 插件启用的时候打开,作为 STOMP 客户端端口;1883,8883 是当 MQTT 插件启用的时候打开,作为 MQTT 客户端端口使用,15674 是基于 WebSocket 的 STOMP 客户端端口,15675 是基于 WebSocket 的 MQTT 客户端端口)

sudo firewall-cmd --zone=public --add-port=4369/tcp --permanent

sudo firewall-cmd --zone=public --add-port=5672/tcp --permanent

sudo firewall-cmd --zone=public --add-port=25672/tcp --permanent

sudo firewall-cmd --zone=public --add-port=15672/tcp --permanent

重启防火墙

sudo firewall-cmd --reload

 

2、简单示例

2.1 生产者示例:

/**
 * 简单队列生产者
 * 使用RabbitMQ的默认交换器发送消息
 */
public class Producer {

    public static void main(String[] args) {
        // 1、创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2、设置连接属性
        factory.setHost("192.168.100.242");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("admin");

        Connection connection = null;
        Channel channel = null;

        try {
            // 3、从连接工厂获取连接
            connection = factory.newConnection("生产者");

            // 4、从链接中创建通道
            channel = connection.createChannel();

            /**
             * 5、声明(创建)队列
             * 如果队列不存在,才会创建
             * RabbitMQ 不允许声明两个队列名相同,属性不同的队列,否则会报错
             *
             * queueDeclare参数说明:
             * @param queue 队列名称
             * @param durable 队列是否持久化
             * @param exclusive 是否排他,即是否为私有的,如果为true,会对当前队列加锁,其它通道不能访问,并且在连接关闭时会自动删除,不受持久化和自动删除的属性控制
             * @param autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除
             * @param arguments 队列参数,设置队列的有效期、消息最大长度、队列中所有消息的生命周期等等
             */
            channel.queueDeclare("queue1", false, false, false, null);

            // 消息内容
            String message = "Hello World!";
            // 6、发送消息
            channel.basicPublish("", "queue1", 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();
                }
            }
        }
    }
}

2.2 消费者示例:

/**
 * 简单队列消费者
 */
public class Consumer {

    public static void main(String[] args) {
        // 1、创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2、设置连接属性
        factory.setHost("192.168.100.242");
        factory.setUsername("admin");
        factory.setPassword("admin");

        Connection connection = null;
        Channel channel = null;

        try {
            // 3、从连接工厂获取连接
            connection = factory.newConnection("消费者");

            // 4、从链接中创建通道
            channel = connection.createChannel();

            /**
             * 5、声明(创建)队列
             * 如果队列不存在,才会创建
             * RabbitMQ 不允许声明两个队列名相同,属性不同的队列,否则会报错
             *
             * queueDeclare参数说明:
             * @param queue 队列名称
             * @param durable 队列是否持久化
             * @param exclusive 是否排他,即是否为私有的,如果为true,会对当前队列加锁,其它通道不能访问,
             *                  并且在连接关闭时会自动删除,不受持久化和自动删除的属性控制。
             *                  一般在队列和交换器绑定时使用
             * @param autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除
             * @param arguments 队列参数,设置队列的有效期、消息最大长度、队列中所有消息的生命周期等等
             */
            channel.queueDeclare("queue1", false, false, false, null);

            // 6、定义收到消息后的回调
            DeliverCallback callback = new DeliverCallback() {
                public void handle(String consumerTag, Delivery message) throws IOException {
                    System.out.println("收到消息:" + new String(message.getBody(), "UTF-8"));
                }
            };
            // 7、监听队列
            channel.basicConsume("queue1", true, callback, new CancelCallback() {
                public void handle(String consumerTag) throws IOException {
                }
            });

            System.out.println("开始接收消息");
            System.in.read();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        } finally {
            // 8、关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }

            // 9、关闭连接
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3、RabbitMQ 的核心概念

RabbitMQ 中,有消息的生产者 Producer,消息的消费者 Consumer,消息实际存放的位置 Queue,RabbitMQ 的主机 Broker,主机中的多个虚拟主机 VirtualHost,Producer/Consumer 与服务器之间建立的连接对象 Connection 以及在 Connection 对象之上,真正客户端与服务器通信的通道 Channel,以及生产者将消息发送到交换器 Exchange 再由交换器决定将消息存放到哪个队列上。

以下是这些核心对象的分布示意图:

在上图中提到了一个新的概念 Exchange 即所谓的交换机,那么它是用来做什么的呢?交换机顾名思义是用来做数据转发的,这里也是一样,它是用来决定一条消息存储到哪个队列上。Exchange 分为 5 种:

  • 第一种是默认类型的交换机,它会根据消息属性中的 queue 属性值来决定一对一的发送到对应的 Queue 上面;
  • 第二种是 fanout 类型,发送到 fanout 类型交换机上的消息会转发给所有绑定在这个交换机上的队列;
  • 第三钟是 direct 类型,它会比对消息属性中的 routeKey 和绑定在它上面的队列的 routeKey(交换机和队列绑定时候的 key 值),如果一致那么就会发送到对应的 Queue 上;
  • 第四种是 topic 类型,它也是比对消息属性中的 routeKey 和 绑定在它上面队列的 routeKey,只不过它是模糊匹配的方式来决定,而 direct 类型的交换机是精确匹配的方式,例如消息的 RouteKey 为 com.order.create,而 topic 交换机上面绑定的队列的 routeKey分别为 com.#(匹配以 com. 开头的值)和 *.order.*(匹配中间是 order ,前后各一个单词的值),那么此时消息也就会发送到这两个队列上去,但是如果消息的 RouteKey 为 cn.order.create,那么消息只会发送到 *.order.* 上面去
  • 第五种是 headers 类型,其实它是用来比对消息的 headers 中是否存在某个属性例如 x 并且它的值正好与 exhcange 和 queue 绑定时设置的 Arguments 参数值一样

4、代码示例

4.1 基于 topic 类型交换机的生产者示例:

/**
 * Topic--生产者
 *
 * 生产者将消息发送到topic类型的交换器上,和routing的用法类似,都是通过routingKey路由,但topic类型交换器的routingKey支持通配符
 */
public class Producer {

    public static void main(String[] args) {
        // 1、创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2、设置连接属性
        factory.setHost("192.168.100.242");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("admin");

        Connection connection = null;
        Channel channel = null;

        try {
            // 3、从连接工厂获取连接
            connection = factory.newConnection("生产者");

            // 4、从链接中创建通道
            channel = connection.createChannel();

            // 路由关系如下:com.# --> queue-1     *.order.* ---> queue-2
            // 消息内容
            String message = "Hello A";
            // 发送消息到topic_test交换器上

            channel.basicPublish("topic-exchange", "com.order.create", null, message.getBytes());
            System.out.println("消息 " + message + " 已发送!");

            // 消息内容
            message = "Hello B";
            // 发送消息到topic_test交换器上
            //如果第一个参数为空的话,那么消息会被发送到默认类型的交换机上,那么第二个参数就代表队列的名称
            channel.basicPublish("topic-exchange", "com.sms.create", null, message.getBytes());
            System.out.println("消息 " + message + " 已发送!");

            // 消息内容
            message = "Hello C";
            // 发送消息到topic_test交换器上
            channel.basicPublish("topic-exchange", "cn.order.create", null, message.getBytes());
            System.out.println("消息 " + message + " 已发送!");

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

4.2 基于 topic 交换机的消费者示例

/**
 * 路由--消费者
 *
 * 消费者通过一个临时队列和交换器绑定,接收发送到交换器上的消息
 */
public class Consumer {

    private static Runnable receive = new Runnable() {
        public void run() {
            // 1、创建连接工厂
            ConnectionFactory factory = new ConnectionFactory();
            // 2、设置连接属性
            factory.setHost("192.168.100.242");
            factory.setPort(5672);
            factory.setUsername("admin");
            factory.setPassword("admin");

            Connection connection = null;
            Channel channel = null;
            final String queueName = Thread.currentThread().getName();

            try {
                // 3、从连接工厂获取连接
                connection = factory.newConnection("消费者");

                // 4、从链接中创建通道
                channel = connection.createChannel();
                // 定义消息接收回调对象
                DeliverCallback callback = new DeliverCallback() {
                    public void handle(String consumerTag, Delivery message) throws IOException {
                        System.out.println(queueName + " 收到消息:" + new String(message.getBody(), "UTF-8"));
                    }
                };
                // 监听队列
                channel.basicConsume(queueName, true, callback, new CancelCallback() {
                    public void handle(String consumerTag) throws IOException {
                    }
                });

                System.out.println(queueName + " 开始接收消息");
                System.in.read();

            } catch (IOException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            } finally {
                // 8、关闭通道
                if (channel != null && channel.isOpen()) {
                    try {
                        channel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (TimeoutException e) {
                        e.printStackTrace();
                    }
                }

                // 9、关闭连接
                if (connection != null && connection.isOpen()) {
                    try {
                        connection.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };

    public static void main(String[] args) {
        new Thread(receive, "queue-1").start();
        new Thread(receive, "queue-2").start();
        new Thread(receive, "queue-3").start();
    }

}

4.3 RabbitMQ 的发布订阅的实现

通过 channel 声明一个 fanout 类型的 exchange 并且从 channel 中拿到一个临时的队列,那么所有发送到这个 exchange 上的数据都会发送到这个临时队列中,这个临时队列会在连接断开的时候被删掉,所以实现发布订阅的机制的关键就在于消费 fanout 类型的交换机绑定的队列。

消费者示例如下:

                // 代码定义交换器
                channel.exchangeDeclare("ps_test", "fanout");
                //  还可以定义一个临时队列,连接关闭后会自动删除,此队列是一个排他队列
                String queueName = channel.queueDeclare().getQueue();
                // 将队列和交换器绑定
                channel.queueBind(queueName, "ps_test", "");

                // 定义消息接收回调对象
                DeliverCallback callback = new DeliverCallback() {
                    public void handle(String consumerTag, Delivery message) throws IOException {
                        System.out.println(clientName + " 收到消息:" + new String(message.getBody(), "UTF-8"));
                    }
                };
                // 监听队列
                channel.basicConsume(queueName, true, callback, new CancelCallback() {
                    public void handle(String consumerTag) throws IOException {
                    }
                });

4.4 消息预抓取

所谓的消息预抓取是针对多个消费者而言的,即当服务器队列上的消息大量堆积的时候,一个消费者已经无法跟上生产者的速度,因此会部署多个消费者来计算消息的处理,而在多个消费者处理的过程中,有个叫 Qos(服务质量)的设置可以使得服务器预抓取指定数量的数据并且一次性的发送给对应的消费者,这个 Qos 的设置在队列消息大量堆积的时候很有帮助,因为它减少了与 Consumer 的通信次数,但是如果服务器端的消息并没有堆积到一定程序的话,那么 Qos 就没有必要进行设置,下面是使用演示(通过 channel 的 basicQos 的方法来设置 Qos 的值):

            // 3、从连接工厂获取连接
            connection = factory.newConnection("消费者");

            // 4、从链接中创建通道
            channel = connection.createChannel();

            /**
             * 5、声明(创建)队列
             * 如果队列不存在,才会创建
             * RabbitMQ 不允许声明两个队列名相同,属性不同的队列,否则会报错
             *
             * queueDeclare参数说明:
             * @param queue 队列名称
             * @param durable 队列是否持久化
             * @param exclusive 是否排他,即是否为私有的,如果为true,会对当前队列加锁,其它通道不能访问,
             *                  并且在连接关闭时会自动删除,不受持久化和自动删除的属性控制。
             *                  一般在队列和交换器绑定时使用
             * @param autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除
             * @param arguments 队列参数,设置队列的有效期、消息最大长度、队列中所有消息的生命周期等等
             */
            channel.queueDeclare("queue1", false, false, false, null);

            // 同一时刻,服务器只会发送一条消息给消费者
            channel.basicQos(10);

5、RabbitMQ 集群:

RabbitMQ 多个节点之间也是通过共享元数据(包括队列元数据、交换机元数据、绑定关系元数据以及虚拟主机元数据)的方式来提供集群服务的。

5.1 RabbitMQ 集群配置(多机单节点)

(1)首先准备好三台单机版的 RabbitMQ 服务器

192.168.100.241

192.168.100.242

192.168.100.243

(2)修改其中一台主机例如 192.168.100.241 上面的 /etc/hosts 文件,在里面添加三个主机的 IP 地址和主机名信息:

192.168.100.241 node1
192.168.100.242 node2
192.168.100.243 node3

(3)将这个 hosts 文件复制到另外两台主机上面,这样来保证各服务器之间可以通过主机名来通信,命令中的root是目标机器的用户名,命令执行后,可能会提示需要输入密码,输入对应用户的密码就行了。

sudo scp /etc/hosts root@192.168.100.242:/etc/
sudo scp /etc/hosts root@192.168.100.243:/etc/

(4)将其中一台主机上面的 /var/lib/rabbitmq/.erlang.cookie 拷贝到另外两台机器上,这是保证三台服务器可以组成集群的必要条件

scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/
scp /var/lib/rabbitmq/.erlang.cookie root@node3:/var/lib/rabbitmq/

(5)开启每台主机的相应端口的防火墙设置或者直接关闭防火墙设置

sudo firewall-cmd --zone=public --add-port=4369/tcp --permanent
sudo firewall-cmd --zone=public --add-port=5672/tcp --permanent
sudo firewall-cmd --zone=public --add-port=25672/tcp --permanent
sudo firewall-cmd --zone=public --add-port=15672/tcp --permanent

(6)重启防火墙使得设置生效

sudo firewall-cmd --reload

(7)启动每台机器的 RabbitMQ 服务

sudo systemctl start rabbitmq-server

(8)将 192.168.100.242,192.168.100.243 加入到 192.168.100.241 形成集群

# 停止RabbitMQ 应用
rabbitmqctl stop_app
# 重置RabbitMQ 设置
rabbitmqctl reset
# 加入到集群
rabbitmqctl join_cluster rabbit@node1 --ram
# 启动RabbitMQ 应用
rabbitmqctl start_app

(9)通过 rabbitmqctl cluster_status 命令查看集群的启动状态,如果可以看到

running_nodes,[rabbit@node1,rabbit@node2,rabbit@node3] 

表示集群启动成功,在管理界面可以更直观的看到集群信息

5.2 RabbitMQ 集群配置(单机多节点)

(1)首先准备一台部署好 RabbitMQ 的服务器 192.168.100.241

(2)在启动前,先修改RabbitMQ 的默认节点名(非必要),在/etc/rabbitmq/rabbitmq-env.conf增加以下内容

# RabbitMQ 默认节点名,默认是rabbit
NODENAME=rabbit1

(3)RabbitMQ 默认是使用服务的启动的,单机多节点时需要改为手动启动,先停止运行中的RabbitMQ 服务

sudo systemctl stop rabbitmq-server

(4)启动第一个节点

rabbitmq-server -detached

(5)启动第二个节点

RABBITMQ_NODE_PORT=5673 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]" RABBITMQ_NODENAME=rabbit2 rabbitmq-server -detached

(6)启动第三个节点

RABBITMQ_NODE_PORT=5674 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15674}]" RABBITMQ_NODENAME=rabbit3 rabbitmq-server -detached

(7)将rabbit2加入到集群

# 停止 rabbit2 的应用
rabbitmqctl -n rabbit2 stop_app
# 重置 rabbit2 的设置
rabbitmqctl -n rabbit2 reset
# rabbit2 节点加入到 rabbit1的集群中
rabbitmqctl -n rabbit2 join_cluster rabbit1 --ram
# 启动 rabbit2 节点
rabbitmqctl -n rabbit2 start_app

(8)将rabbit3加入到集群

# 停止 rabbit3 的应用
rabbitmqctl -n rabbit3 stop_app
# 重置 rabbit3 的设置
rabbitmqctl -n rabbit3 reset
# rabbit3 节点加入到 rabbit1的集群中
rabbitmqctl -n rabbit3 join_cluster rabbit1 --ram
# 启动 rabbit3 节点
rabbitmqctl -n rabbit3 start_app

(9)查看集群状态,看到{running_nodes,[rabbit3@node1,rabbit2@node1,rabbit1@node1]}说明节点已启动成功

rabbitmqctl cluster_status

(10)防火墙设置:打开对应端口的防火墙或者直接关闭整个防火墙

sudo firewall-cmd --zone=public --add-port=4369/tcp --permanent
sudo firewall-cmd --zone=public --add-port=5672/tcp --permanent
sudo firewall-cmd --zone=public --add-port=25672/tcp --permanent
sudo firewall-cmd --zone=public --add-port=15672/tcp --permanent
sudo firewall-cmd --zone=public --add-port=5673/tcp --permanent
sudo firewall-cmd --zone=public --add-port=25673/tcp --permanent
sudo firewall-cmd --zone=public --add-port=15673/tcp --permanent
sudo firewall-cmd --zone=public --add-port=5674/tcp --permanent
sudo firewall-cmd --zone=public --add-port=25674/tcp --permanent
sudo firewall-cmd --zone=public --add-port=15674/tcp --permanent

(11)重启防火墙设置

sudo firewall-cmd --reload

6、虚拟主机

6.1 虚拟主机介绍

每一个 RabbitMQ 服务器都能创建虚拟的消息服务器,我们称之为虚拟主机 (virtual host) ,简称为 vhost。

每一个 vhost 本质上是一个独立的小型 RabbitMQ 服务器,拥有自己独立的队列、交换器及绑定关系等,井且它拥有自己独立的权限。

vhost 就像是虚拟机与物理服务器一样,它们在各个实例间提供逻辑上的分离,为不同程序安全保密地运行数据,它既能将同一个RabbitMQ 中的众多客户区分开,又可以避免队列和交换器等命名冲突。

vhost 之间是绝对隔离的,无法将 vhostl 中的交换器与 vhost2 中的队列进行绑定,这样既保证了安全性,又可以确保可移植性。
如果在使用 RabbitMQ 达到一定规模的时候,建议用户对业务功能、场景进行归类区分,并为之分配独立的 vhost。

 

6.2 虚拟主机创建

使用管理员用户登录Web管理界面。

(1)创建虚拟主机 v1

在 Admin -> Virtual Hosts 页面添加一个名为 v1 的Virtual Hosts。

(2)创建虚拟主机的用户并设置角色

此时还需要为此vhost分配用户,添加一个新用户:在 Admin -> Users 页面添加一个名为 order-user 的用户,并设置为 management 角色。

(3)给创建的用户设置访问虚拟主机的权限,包括可以访问的队列/交换机、以及对队列/交换机的读写权限

从 Admin 进入 order-user 的用户设置界面,在 Permissions 中,为用户分配vhost为/v1,并为每种权限设置需要匹配的目标名称的正则表达式。

下图表示如果需要对队列/交换机进行相应操作需要有的 configure,write 和 read 权限,比如创建队列时,会调用queue.declare方法,此时会使用到configure权限,会校验队列名是否与configure的表达式匹配 ;比如队列绑定交换器时,会调用queue.bind方法,此时会用到write 和 read权限,会检验队列名是否与write的表达式匹配,交换器名是否与read的表达式匹配。

(4)vhost可以限制最大连接数和最大队列数,并且可以设置vhost下的用户资源权限和Topic权限,具体权限见下方说明。

在 Admin -> Limits 页面可以设置vhost的最大连接数和最大队列数,达到限制后,继续创建,将会报错。

 

6.3 虚拟主机 Demo

/**
 * vhost和权限应用示例
 * <p>
 * 说明:先阅读文档中的使用示例,创建号用户名和vhost,分配好权限。
 * http://code.dongnaoedu.com/MQ/rabbitmq/rabbitmq/blob/master/java/src/main/java/com/study/rabbitmq/a133/cluster/readme.md#vhost使用示例
 * <p>
 * 另外需要自己在管理界面创建queue2队列和test交换器
 */
public class VirtualHostsDemo {

    public static void main(String[] args) {
        // 1、创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2、设置连接属性,这里就是新创建的用户的用户名和密码
        factory.setUsername("order-user");
        factory.setPassword("order-user");
        //这是在上一步中创建的虚拟主机,表示使用 order-user 这个用户连接到 v1 这个虚拟主机上
        factory.setVirtualHost("v1");

        Connection connection = null;
        Channel prducerChannel = null;
        Channel consumerChannel = null;

        // 3、设置每个节点的链接地址和端口,连接到集群中
        Address[] addresses = new Address[]{
                new Address("192.168.100.243", 5672),
                new Address("192.168.100.242", 5672),
                new Address("192.168.100.241", 5672)
        };

        try {
            // 4、从连接工厂获取连接
            connection = factory.newConnection(addresses, "消费者");

            // 5、从链接中创建通道
            prducerChannel = connection.createChannel();

            //定义一个 fanout 类型的 Exchange
            prducerChannel.exchangeDeclare("test-exchange", "fanout");
            //声明一个队列 queue1
            prducerChannel.queueDeclare("queue1", false, false, false, null);
            //将 queue1 与 test-exchange 进行绑定,routeKey 为 xxoo
            prducerChannel.queueBind("queue1", "test-exchange", "xxoo");
            // 消息内容
            String message = "Hello A";
            prducerChannel.basicPublish("test-exchange", "c1", null, message.getBytes());

            consumerChannel = connection.createChannel();
            // 创建一个消费者对象
            Consumer consumer = new DefaultConsumer(consumerChannel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    System.out.println("收到消息:" + new String(body, "UTF-8"));
                }
            };
            consumerChannel.basicConsume("queue1", true, consumer);

            System.out.println("等待接收消息");
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        } finally {
            // 9、关闭通道
            if (prducerChannel != null && prducerChannel.isOpen()) {
                try {
                    prducerChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }

            // 10、关闭连接
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

7 RabbitMQ 高可用架构:镜像队列

RabbitMQ 集群服务器之间虽然可以通过同步元数据的方式来保证负载均衡,但是队列始终只存在于某一台服务器上,那么如果这台服务器宕机之后,它上面所有的队列都不可用,这种情况下 RabbitMQ 提供了一种所谓 镜像队列 的方式来提供高可用,即当通过配置来使得当一条消息发送到集群中的某台机器的某个队列后,其他一台或者多台机器对这个队列进行同步存储。

可以在RabbitMQ 的 web 管理控制台去配置镜像队列的同步策略:如下图所示,其中

  • Virtual Host 表示策略生效的虚拟主机
  • Name是配置这个策略的名字;
  • Pattern 是配置策略生效的队列的模糊匹配;
  • Apply to 是配置策略生效的对象,包括队列和交换机;
  • Priority 表示策略的优先级,数值越大优先级越高;
  • Definition 表示一些参数配置,其中 ha-mode 有三种选择,第一种是 all 表示集群中所有主机都同步保存队列数据之后才返回消息发送成功,第二种是 exactly 表示集群中多少台主机都同步成功则表示消息发送成功,第三种是 nodes 表示指定的节点都同步成功表示消息发送成功;而 HA sync mode 表示队列中消息的同步方式,有效值为 automatic 和 manual,默认是 automatic;

以下是高可用架构下的生产者/消费者示例的关键部分,包括了服务器宕机自动重连,设置尝试恢复连接的时间间隔以及添加重连监听器:

        // 3、设置每个节点的链接地址和端口
        Address[] addresses = new Address[]{
                new Address("192.168.100.242", 5672),
                new Address("192.168.100.241", 5672)
        };

        try {
            // 开启/关闭连接自动恢复,默认是开启状态
            factory.setAutomaticRecoveryEnabled(true);

            // 设置每100毫秒尝试恢复一次,默认是5秒:com.rabbitmq.client.ConnectionFactory.DEFAULT_NETWORK_RECOVERY_INTERVAL
            /*什么时候会触发连接恢复?https://www.rabbitmq.com/api-guide.html#recovery-triggers


            如果启用了自动连接恢复,将由以下事件触发:

                连接的I/O循环中抛出IOExceiption
                读取Socket套接字超时
                检测不到服务器心跳
                在连接的I/O循环中引发任何其他异常*/
            factory.setNetworkRecoveryInterval(100);

            factory.setTopologyRecoveryEnabled(false);

            // 4、使用连接集合里面的地址获取连接
            connection = factory.newConnection(addresses, "生产者");

            // 添加重连监听器
            ((Recoverable) connection).addRecoveryListener(new RecoveryListener() {
                /**
                 * 重连成功后的回调
                 * @param recoverable
                 */
                public void handleRecovery(Recoverable recoverable) {
                    System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SS").format(new Date()) + " 已重新建立连接!");
                }

                /**
                 * 开始重连时的回调
                 * @param recoverable
                 */
                public void handleRecoveryStarted(Recoverable recoverable) {
                    System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SS").format(new Date()) + " 开始尝试重连!");
                }
            });

需要注意的是,以上是服务器突然宕机的情况,发生在客户端已经和服务器端建立过连接;但如果客户端第一次连接失败,不会自动恢复连接。需要我们自己负责重试连接、记录失败的尝试、实现重试次数的限制等等

8、集群管理命令

  • rabbitmqctl status:查看节点状态
  • rabbitmqctl stop [pid_file]:停止运行 RabbitMQ 的 Erlang 虚拟机和 RabbitMQ 服务应用。如果指定了 pid_file,还需要等待指定进程的结束,pid_file 是通过调用 rabbitmq-server 命令启动 RabbitMQ 服务时创建的,默认情况下存放于 Mnesia 目录中。如果使用 rabbitmq-server -detach 后缀的命令来启动 RabbitMQ 服务则不会创建 pid_file 
  • rabbitmqctl stop_app:停止 RabbitMQ 服务应用,但是 Erlang 虚拟机还是处于运行状态。此命令的执行优先于其他管理操作,比如 rabbitmqctl reset
  • rabbitmqctl start_app:启动 RabbitMQ 应用。此命令典型的用途是在执行了其他管理操作之后,重新启动之前停止的 RabbitMQ 应用,比如 rabbitmqctl reset 
  • rabbitmqctl reset:将 RabbitMQ 节点重置还原到最初状态,包括从原来所在的集群中删除此节点,从管理数据库中删除所有的配置数据,如已配置的用户、vhost 等,以及删除所有的持久化消息。执行 rabbitmqctl reset 命令前必须停止 RabbitMQ 应用
  • rabbitmqctl force_reset:强制将 RabbitMQ 节点重置还原到最初状态。此命令不论当前管理数据库的状态和集群配置是什么,都会无条件地重置节点,只能在数据库或集群配置已损坏的情况下使用。 
  • rabbitmqctl [-n nodename] join_cluster {cluster_node} [--ram]:将节点加入到指定集群中,在这个命令执行前需要停止 RabbitMQ 应用并重置节点。命令中的 -n nodename 表示指定需要操作的目标节点,例如 rabbit@node1;cluster_node 表示需要加入的集群节点名称,例如 rabbit@node2;--ram 表示集群节点类型,有两种类型:ram|disc,默认为 disc,表示内存节点,即所有的元数据都存储在磁盘中,而 ram 表示所有的元数据都存储在内存中。如果是单节点形成的伪集群,可以将节点类型配置为 disc 来保证宕机后可以恢复集群信息;如果是多个节点形成的集群,可以将节点类型设置为 ram。
  • rabbitmqctl cluster_status:查看集群状态 
  • rabbitmqctl change_cluster_node_type {disc|ram}:修改集群节点的类型,使用此命令之前要停止 RabbitMQ 应用。例如原先 A B 节点组成集群,然后 A 节点的 RabbitMQ 服务停止了,此时集群中只有 B 节点;现在有个 C 节点也添加到 B 节点所在的集群中,然后 B 节点又因为某些原因离开了当前集群,此时如果 A 节点的 RabbitMQ 服务又启动起来,当它尝试连接到 B 节点形成集群时,由于 B 节点已经不在当前集群了,因此此时无法跟 C 形成集群,这种情况下就可以使用 rabbitmqctl change_cluster_node_type A C  的方式来重新组成集群
  • rabbitmqctl forget_cluster_node [--offline]:将节点从集群中删除,允许离线执行。
  • rabbitmqctl update_cluster_nodes {clusternode}:在集群中的节点应用启动前咨询 clusternode 节点的最新信息,并更新相应的集群信息。这个和 join_cluster 不同,它不会加入集群。
  • rabbitmqctl force_boot:确保节点可以启动,即使他不是最后一个关闭的节点。该命令的应用场景为:当 RabbitMQ 的服务器集群整体关闭之后,本来按道理来说最后关闭的节点应该是保存最全信息的节点,所以重启服务器集群的话应该从最后关闭的节点开始重启,但是有的时候我们并不能知道最后关闭的节点是什么,因此就可以使用该命令来确保节点可以启动。RabbitMQ 本身不能保证数据完全不丢失,还需要配合生产者的重发机制。
  • rabbitmqctl set_cluster_name {name}:设置集群名称。集群名称在客户端连接时会通报给客户端,集群名称默认是集群中第一个节点的名称,通过这个命令可以重新设置。

 

9、RabbitMQ 持久化机制

9.1 消息、队列、交换机持久化

RabbitMQ 的持久化分为队列持久化、消息持久化和交换机持久化。不管是持久化消息还是非持久化的消息都可以被写入到磁盘中。

当消息被定义为持久化消息时,生产者将消息发送到队列中后,内存中会有消息数据,同时也会写入到磁盘来保证服务器重启后消息数据依然存在。

非持久化消息在被发送到服务器后不会主动的持久化到本地,只有当服务器的内存不够用时,才会将消息放到磁盘中,当内存够用后,再次从磁盘中读取消息到内存中并删除磁盘中的消息数据

队列的持久化是在定义队列时的 durable 参数来实现的,durable 为 true 时,队列才持久化

消息持久化是通过消息的属性 deliveryMode 来设置是否持久化的,在发送消息时通过 basicPublish 的参数传入。MessageProperties.PERSISTENT_TEXT_PLAIN 表示是持久化的消息

 而交换机的持久化也是在定义的的时候通过设置 durable 参数来指定的:

9.2 持久化带来的内存、磁盘管理问题 

当内存使用超过配置的阈值或者磁盘剩余空间低于配置的阈值时,RabbitMQ 会暂时阻塞客户端的连接,并停止接收从客户端发来的消息,以此来避免服务崩溃,客户端和服务器端的心跳检测也会失效。

(1)内存阈值

当出现内存告警时,可以通过管理命令临时调整内存阈值大小:

rabbitmqctl set_vm_memory_high_watermark <fraction>
rabbitmqctl set_vm_memory_high_watermark absolute <value>

fraction 为内存阈值,RabbitMQ 默认值为 0.4,即表示当 RabbitMQ 的内存使用超过 40% 时,就会产生告警并且阻塞所有生产者连接。通过此命令来修改的阈值在 Broker 重启后将会失效,通过修改配置文件 /etc/rabbitmq/rabbitmq.conf 中的

# 建议取值范围为 0.4-0.66,最多不要超过 0.7
vm_memory_high_watermark.relative = 0.5  
vm_memory_high_watermark.absolute = 1GB

的方式设置的阈值则不会在重启后失效,但需要重启 Broker 才会生效。

(2)内存换页

除了提供内存阈值的控制外,还有一个内存换页的概念,即在某个节点快达到内存阈值并阻塞生产者之前,它会尝试将队列中的消息换页到磁盘中以释放内存空间。持久化消息和非持久话消息都会被转储到磁盘中,但是持久化的消息本来就在磁盘有一分副本,因此这里直接会将持久化的消息从内存中清除。

默认情况下,在内存达到内存阈值的 50 % 时会进行内存换页的动作,也就是说如果内存总大小是 1G,那么默认的内存阈值为 0.4G(此时服务器将阻塞生产者继续发送消息),当内存使用超过 0.4*0.5=0.2G 时就会进行内存换页(此时服务器将会把内存中的数据转储到磁盘中)。

可以通过配置文件中的 vm_memory_high_watermark_paging_ratio 项来修改此值:

# 如果这个值超过 1 那么相当于禁止内存换页功能
vm_memory_high_watermark_paging_ratio = 0.75

(3)磁盘阈值

当磁盘剩余空间低于确定的阈值时,RabbitMQ 同样会阻塞生产者,这样可以避免因非持久化的消息持续换页而耗尽磁盘空间导致服务崩溃。

默认情况下,磁盘阈值为 50 M,表示当磁盘剩余空间低于 50 M 时会阻塞生产者并停止内存中消息的换页动作。

这个阈值的设置爱可以减小,但不能完全消除因磁盘耗尽而导致服务崩溃的可能性。比如在两次磁盘空间检测期间内,磁盘空间从大于 50 M 被耗尽到 0M。一个相对谨慎的做法是将磁盘阈值设置为与操作系统所显示的内存大小一致。

可以通过命令来临时调整磁盘阈值:

# disk_limit 为固定大小,单位可以是 MB,GB
rabbitmqctl set_disk_free_limit <disk_limit>
# fraction 是相对值,即相对内存大小,建议的取值为 1.0-2.0 即内存如果是 8G,这里配置的是 1.0,那么剩余空间阈值为 8G
rabbitmqctl set_disk_free_limit mem_relative <fraction>

 也可以通过修改配置文件来使得配置永久生效:

disk_free_limit.relative=1.0
disk_free_limit.absolute=8GB

10、消息可靠性

消息的可靠性分为三种:

  • 发送可靠性:确保消息成功发送到 Broker
  • 存储可靠性:Broker 对消息持久化,确保消息不会丢失
  • 消费可靠性:确保消息成功被消费

10.1 发送可靠性

一般消息发送可靠性可以分为三个层级:

  • 最多一次,消息可能会丢失,但绝对不会重复
  • 最少一次,消息绝对不会丢失,但可能会重复
  • 恰好一次,每条消息肯定会被传输一次且仅传输一次

为了保证消息可以被可靠发送,那么我们就要选择最少一次的层级。消息生产者需要开启事务机制或者 publisher confirm 机制以确保消息可以可靠传输到 RabbitMQ 中;消息生产者需要配合使用 mandatory 参数或者备份交换器来确保消息能够从交换器路由到队列中,进而能够保存下来而不会被丢弃。

我们选择使用 publisher confirm 的模式来确保消息的可靠性(注意这种消息发布确认机制在不同的 API 中可能需要不同的开启方式,SpringBoot 中开启方式是配置 RabbitMQ 的 publisher-confirms=true): 

            // 进入confirm模式, 每次发送消息,rabbtiqm处理之后会返回一个对应的回执消息
            AMQP.Confirm.SelectOk selectOk = channel.confirmSelect();
            // 增加监听器
            ArrayList<String> queues = new ArrayList<>();
            channel.addConfirmListener(new ConfirmListener() {
                @Override
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                    // deliveryTag 同一个channel中此条消息的编号 。
                    // 业务..
                    System.out.println("受理成功 " + queues.get((int) deliveryTag) + " " + multiple);
                }

                @Override
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                    // 失败重发
                    // queues.get((int) deliveryTag)
                    System.out.println("受理失败 " + deliveryTag);
                }
            });
            // 定义fanout类型的交换器
            channel.exchangeDeclare("ps_test", "fanout");

            for (int i = 0; i < 10; i++) {
                // 消息内容
                String message = "Hello Confirm " + i;
                queues.add(message);
                // 发送消息到ps_test交换器上
                AMQP.BasicProperties basicProperties = new AMQP.BasicProperties();
                channel.basicPublish("ps_test", "", basicProperties, message.getBytes());
                System.out.println("消息 " + message + " 已发送!");
            }

以下是消息可靠性发送的流转图:首先从业务DB将业务取出来并插入到消息 DB 表来记录消息的发送情况;接着将消息发送到 MQ 服务器;MQ 服务器会返回确认收到的消息;生产者收到确认消息后将更新消息 DB 中的消息发送状态为已发送,如果生产者没有收到服务器的确认,那么将不会更新消息 DB 中的状态,此时可以使用一个定时任务定时抓取消息 DB 中的数据来进行消息的重发。

10.2 消息消费可靠性

消费者在消费消息的同时,需要将 autoAck 设置为 false,然后通过手动确认的方式去确认已经正确消费消息,以免在消费端引起不必要的消息丢失。 

消息消费的可靠性可以在 channel.basicConsume() 方法中来实现,通过定义 DefaultConsumer 的匿名实现类并且实现 handleDelivery 方法来进行消息消费的确认。正常消费的话可以使用 channel.basicAck() 方法来告知服务器;异常消费的话则通过 channel.basicNack() 方法来告知服务器,而 basicNack 方法中最后一个参数 requeue 则表示是否告知服务器将消费失败的消息进行重新发送,如果设置为 true 则消息会被不断的发送,如果设置为 false,那么消息可能会被丢弃或者放入到死信队列中。所谓的死信队列就是专门用来存储消费失败的消息,通过在声明正常队列时设置 x-dead-letter-exchange 来设置死信队列,设置了死信队列的队列消息在没有收到消息确认并且不再重复发送的话,会将消息放到死信队列中,这样我们可以再启动一个消费者专门来消费死信队列中的消息来监控系统的运转。

                connection = factory.newConnection("消费者");
                // ###死信队列相关:专门用来存储 出错 出异常的数据
                channel = connection.createChannel();
                // 1、 创建一个exchange
                channel.exchangeDeclare("dlq_exchange", "fanout");
                // 2、 创建一个queue,和exchange绑定起来
                channel.queueDeclare("dlq_queue1", false, false, false, null);
                channel.queueBind("dlq_queue1", "dlq_exchange", "");
                // ######死信队列结束


                // 4、从链接中创建通道
                channel = connection.createChannel();
                // 代码定义交换器
                channel.exchangeDeclare("ps_test", "fanout");
                //  还可以定义一个临时队列,连接关闭后会自动删除,此队列是一个排他队列
                String queueName = "queue1";
                // 队列中有死信产生时,消息会转发到交换器 dlq_exchange。
                Map<String, Object> args = new HashMap<String, Object>();
                args.put("x-dead-letter-exchange", "dlq_exchange");
                channel.queueDeclare(queueName, false, false, false, args);
                // 将队列和交换器绑定
                channel.queueBind(queueName, "ps_test", "");

                // 监听队列
                Channel finalChannel = channel;
                channel.basicConsume(queueName, false, "消费者-手动回执",
                        new DefaultConsumer(finalChannel) {
                            @Override
                            public void handleDelivery(String consumerTag,
                                                       Envelope envelope,
                                                       AMQP.BasicProperties properties,
                                                       byte[] body)
                                    throws IOException {
                                try {
                                    System.out.println("收到消息: " + new String(body));
                                    // TODO 业务处理
                                    long deliveryTag = envelope.getDeliveryTag();
                                    // 模拟业务处理耗时
                                    Thread.sleep(1000L);
                                    // 正常消费
                                    // finalChannel.basicAck(deliveryTag, false);
                                    // 异常消费
                                    finalChannel.basicNack(envelope.getDeliveryTag(), false, false);
                                } catch (InterruptedException e) {
                                    // 异常消费, requeue参数 true重发,false不重发(丢弃或者移到DLQ死信队列)
                                    // finalChannel.basicNack(envelope.getDeliveryTag(), false, false);
                                    e.printStackTrace();
                                }
                            }
                        });

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值