RabbitMQ简易原理及使用

黑马程序员RabbitMQ全套教程,rabbitmq消息中间件到实战_哔哩哔哩_bilibili

尚硅谷RabbitMQ教程丨快速掌握MQ消息中间件_哔哩哔哩_bilibili

安装:CentOS8安装RabbitMQ 3.8.9_wcybaonier的博客-CSDN博客

// config改为conf,rabbitmq以后缀识别,config和conf两个配置文件级别和配置方式不同
vi /etc/rabbitmq/rabbitmq.conf 

loopback_users = none //conf文件内容,管理界面可以远程登录

 docker安装:

docker pull rabbitmq:management

docker run -d --name rabbitmq --publish 5671:5671 \
--publish 5672:5672 --publish 4369:4369 --publish 25672:25672 --publish 15671:15671 --publish 15672:15672 \
rabbitmq:management

4369 -- erlang发现口 5672 --client端通信口

15672 -- 管理界面ui端口 25672 -- server间内部通信口

目录

MQ概述

MQ 的优势和劣势

MQ 的优势

MQ 的劣势

使用 MQ 需要满足什么条件呢? 

常见的 MQ 产品 

 RabbitMQ 简介

 概念解析:

MQ中各种工作模式的原生Java API示例

简单模式

 Work Queues工作队列

Publish/Subsrcibe发布订阅

Routing路由模式

Topics 通配符模式

springboot整合

RabbitMQ 高级特性

confirm&return

Consumer Ack

消息可靠性总结

 消费端限流

TTL

死信队列

延迟队列

Rabbitmq 插件实现延迟队列

优先级队列

惰性队列

日志与监控 

RabbitMQ日志

web管控台监控

rabbitmqctl管理和监控

消息追踪

RabbitMQ应用问题

1. 消息投递可靠性保障

2. 消息幂等性保障

3.消费端消息不丢失

RabbitMQ集群模式

RabbitMQ集群搭建——黑马版本

3.1 集群方案的原理

3.2 单机多实例部署

3.3 集群管理

3.4 RabbitMQ镜像集群配置

3.5 负载均衡-HAProxy

RabbitMQ 集群——尚硅谷版本

1.搭建步骤

2.镜像队列

Haproxy+Keepalive 实现高可用负载均衡

 整体架构图

Haproxy 实现负载均衡

搭建步骤

Keepalived 实现双机(主备)热备

搭建步骤

Federation Exchange

搭建步骤

Federation Queue

Shovel

搭建步骤


MQ概述

MQ全称 Message Queue(消息队列),是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。 ⚫ MQ,消息队列,存储消息的中间件

⚫ 分布式系统通信两种方式:直接远程调用(RPC) 和 借助第三方完成间接通信

⚫ 发送方称为生产者,接收方称为消费者

MQ 的优势和劣势

MQ 的优势

  • 应用解耦:提高系统容错性和可维护性
  • 异步提速:提升用户体验和系统吞吐量(单位时间内处理请求的数目)。
  • 削峰填谷(提高系统稳定性):使用了 MQ 之后,限制消费消息的速度为1000,这样一来,高峰期产生的数据势必会被积压在 MQ 中,高峰 就被“削”掉了,但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000,直到消费完积压的消息,这就叫做“填谷”。

MQ 的劣势

 ⚫ 系统可用性降低 系统引入的外部依赖越多,系统稳定性越差。一旦 MQ 宕机,就会对业务造成影响。如何保证MQ的高可用?

⚫ 系统复杂度提高 MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用。如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性?

⚫ 一致性问题 A 系统处理完业务,通过 MQ 给B、C、D三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理失败。如何保证消息数据处理的一致性?

使用 MQ 需要满足什么条件呢? 

①生产者不需要从消费者处获得反馈。引入消息队列之前的直接调用,其接口的返回值应该为空,这才让明明下层的动作还没做,上层却当成动作做完了继续往后走,即所谓异步成为了可能。

② 容许短暂的不一致性。

③ 确实是用了有效果。即解耦、提速、削峰这些方面的收益,超过加入MQ,管理MQ这些成本。

常见的 MQ 产品 

 RabbitMQ 简介

AMQP,即 Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议 的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中 间件不同产品,不同的开发语言等条件的限制。2006年,AMQP 规范发布。类比HTTP。

 概念解析:

⚫ Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker

⚫ Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多 个vhost,每个用户在自己的 vhost 创建 exchange/queue 等。

⚫ Connection:publisher/consumer 和 broker 之间的 TCP 连接。

⚫ Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线 程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销

⚫ Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)

  1. Direct:处理路由键,需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键为“green”,则只有路由键为“green”的消息才被转发,不会转发路由键为"red",只会转发路由键为“green”。

  2. Topic:将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词,符号“*”只能匹配一个词。

  3. Fanout:不处理路由键。你只需要简单的将队列绑定到交换机上。一个发送到该类型交换机的消息都会被广播到与该交换机绑定的所有队列上。

  4. Headers:不处理路由键,而是根据发送的消息内容中的 headers 属性进行匹配。在绑定 Queue 与 Exchange 时指定一组键值对;当消息发送到 RabbitMQ 时会取到该消息的 headers 与 Exchange 绑定时指定的键值对进行匹配;如果完全匹配则消息会路由到该队列,否则不会路由到该队列

在这四种类型里,Direct 类型的 Exchange 投递消息是最快的。其他的 Exchange,MQ 还得花时间计算投递的位置。

⚫ Queue:消息最终被送到这里等待 consumer 取走

⚫ Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存 到 exchange 中的查询表中,用于 message 的分发依据

RabbitMQ 提供了 6种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing 路由模式、Topics 主题模式、RPC 远程调用模式 

RabbitMQ Tutorials — RabbitMQ

JMS 即 Java 消息服务(JavaMessage Service)应用程序接口,是一个 Java 平台中关于面向消息中间件的API。

⚫ JMS 是 JavaEE 规范中的一种,类比JDBC

⚫ 很多消息中间件都实现了JMS规范,例如:ActiveMQ。RabbitMQ 官方没有提供 JMS 的实现包,但是开源社区有


MQ中各种工作模式的原生Java API示例

简单模式

package com.exe.producer;

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

import java.nio.charset.StandardCharsets;

public class Producer_HelloWorld {

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setVirtualHost("hcxvh");
        factory.setUsername("hcx");
        factory.setPassword("123456");
        try (
                Connection connection = factory.newConnection();
                Channel channel = connection.createChannel();
        ) {
            /*
             * (String queue, boolean durable, boolean exclusive,
             * 队列名称          是否持久化     是否独占,即一个消费者监听,当Connection关闭时是否删除队列
             * boolean autoDelete, Map<String, Object> arguments)
             * 当没有Consumer时是否删除           参数
             * */
            channel.queueDeclare("Hello_World", true, false, false, null);
            /*
             * (String exchange, String routingKey,
             *  交换机,简单模式下使用默认    路由名称
             * BasicProperties props, byte[] body)
             * 配置信息              消息体,字节数组
             * */
            channel.basicPublish("", "Hello_World",
                    null, "hello rabbitmq!".getBytes(StandardCharsets.UTF_8));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package com.exe.consumer;

import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer_HelloWorld {
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setVirtualHost("hcxvh");
        factory.setUsername("hcx");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        /*
         * (String queue, boolean durable, boolean exclusive,
         * 队列名称          是否持久化     是否独占,即一个消费者监听,当Connection关闭时是否删除队列
         * boolean autoDelete, Map<String, Object> arguments)
         * 当没有Consumer时是否删除           参数
         * */
        channel.queueDeclare("Hello_World", true, false, false, null);
        /*
         * (String exchange, String routingKey,
         *  交换机,简单模式下使用默认    路由名称
         * BasicProperties props, byte[] body)
         * 配置信息              消息体,字节数组
         * */
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumerTag: " + consumerTag);
                System.out.println("Exchange: " + envelope.getExchange());
                System.out.println("RoutingKey: " + envelope.getRoutingKey());
                System.out.println("properties: " + properties);
                System.out.println("body: " + new String(body));
            }
        };
        channel.basicConsume("Hello_World", true, consumer);
    }
}

 Work Queues工作队列

package com.exe.producer;

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

import java.nio.charset.StandardCharsets;

public class Producer_WorkQueue {

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setVirtualHost("hcxvh");
        factory.setUsername("hcx");
        factory.setPassword("123456");
        try (
                Connection connection = factory.newConnection();
                Channel channel = connection.createChannel();
        ) {
            /*
             * (String queue, boolean durable, boolean exclusive,
             * 队列名称          是否持久化     是否独占,即一个消费者监听,当Connection关闭时是否删除队列
             * boolean autoDelete, Map<String, Object> arguments)
             * 当没有Consumer时是否删除           参数
             * */
            channel.queueDeclare("Hello_World", true, false, false, null);
            /*
             * (String exchange, String routingKey,
             *  交换机,简单模式下使用默认    路由名称
             * BasicProperties props, byte[] body)
             * 配置信息              消息体,字节数组
             * */
            for (int i = 0; i < 10; i++) {
                String body = i + " hello rabbitmq!";
                channel.basicPublish("", "Hello_World",
                        null, body.getBytes(StandardCharsets.UTF_8));
            }


        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 先启动两个consumer实例,可以看见两者顺序消费

package com.exe.consumer;

import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer_HelloWorld {
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setVirtualHost("hcxvh");
        factory.setUsername("hcx");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        /*
         * (String queue, boolean durable, boolean exclusive,
         * 队列名称          是否持久化     是否独占,即一个消费者监听,当Connection关闭时是否删除队列
         * boolean autoDelete, Map<String, Object> arguments)
         * 当没有Consumer时是否删除           参数
         * */
        channel.queueDeclare("Hello_World", true, false, false, null);
        /*
         * (String exchange, String routingKey,
         *  交换机,简单模式下使用默认    路由名称
         * BasicProperties props, byte[] body)
         * 配置信息              消息体,字节数组
         * */
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
/*                System.out.println("consumerTag: " + consumerTag);
                System.out.println("Exchange: " + envelope.getExchange());
                System.out.println("RoutingKey: " + envelope.getRoutingKey());
                System.out.println("properties: " + properties);*/
                System.out.println("body: " + new String(body));
            }
        };
        channel.basicConsume("Hello_World", true, consumer);
    }
}

小结:

1. 在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争的关系。

2. Work Queues 对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。例如:短信服务部署多个, 只需要有一个节点成功发送即可


Publish/Subsrcibe发布订阅

package com.exe.producer;

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

import java.nio.charset.StandardCharsets;

public class Producer_Pub {

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setVirtualHost("hcxvh");
        factory.setUsername("hcx");
        factory.setPassword("123456");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()
        ) {

            /*
             * (String queue, boolean durable, boolean exclusive,
             * 队列名称          是否持久化     是否独占,即一个消费者监听,当Connection关闭时是否删除队列
             * boolean autoDelete, Map<String, Object> arguments)
             * 当没有Consumer时是否删除           参数
             * */
            channel.queueDeclare("queue1", true, false, false, null);
            channel.queueDeclare("queue2", true, false, false, null);

            /*
            (String exchange, BuiltinExchangeType type, boolean durable,
            boolean autoDelete, Map<String, Object> arguments)
            */
            channel.exchangeDeclare("exchange1", BuiltinExchangeType.FANOUT, //广播模式
                    true, false, null);
            /*
             * (String queue, String exchange, String routingKey)
             *                                 交换机类型为fanout时,为""
             * */
            channel.queueBind("queue1", "exchange1", "");
            channel.queueBind("queue2", "exchange1", "");
            /*
             * (String exchange, String routingKey,
             *  交换机,简单模式下使用默认    路由名称
             * BasicProperties props, byte[] body)
             * 配置信息              消息体,字节数组
             * */
            for (int i = 0; i < 10; i++) {
                String body = i + " : rabbitmq pubsub";
                channel.basicPublish("exchange1", "",
                        null, body.getBytes(StandardCharsets.UTF_8));
            }


        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

package com.exe.consumer;

import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer_Sub {
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setVirtualHost("hcxvh");
        factory.setUsername("hcx");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        /*
         * (String exchange, String routingKey,
         *  交换机,简单模式下使用默认    路由名称
         * BasicProperties props, byte[] body)
         * 配置信息              消息体,字节数组
         * */
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
//                System.out.println("consumerTag: " + consumerTag);
//                System.out.println("Exchange: " + envelope.getExchange());
//                System.out.println("RoutingKey: " + envelope.getRoutingKey());
//                System.out.println("properties: " + properties);
                System.out.println("body: " + new String(body));
            }
        };
        channel.basicConsume("queue2", true, consumer); // 监听队列
    }
}

小结:

1. 交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到。

2. 发布订阅模式与工作队列模式的区别:

  •  工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机
  • 发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用 默认交换机)
  • 发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队 列绑 定到默认的交换机

Routing路由模式

package com.exe.producer;

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

import java.nio.charset.StandardCharsets;
import java.util.Random;

public class Producer_Routing {

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setVirtualHost("hcxvh");
        factory.setUsername("hcx");
        factory.setPassword("123456");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()
        ) {

            /*
             * (String queue, boolean durable, boolean exclusive,
             * 队列名称          是否持久化     是否独占,即一个消费者监听,当Connection关闭时是否删除队列
             * boolean autoDelete, Map<String, Object> arguments)
             * 当没有Consumer时是否删除           参数
             * */
            String queue1 = "directQueue1";
            String queue2 = "directQueue2";
            channel.queueDeclare(queue1, true, false, false, null);
            channel.queueDeclare(queue2, true, false, false, null);

            /*
            (String exchange, BuiltinExchangeType type, boolean durable,
            boolean autoDelete, Map<String, Object> arguments)
            */
            String exchangeName = "routingExchange";
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT,//定向发送
                    true, false, null);
            /*
             * (String queue, String exchange, String routingKey)
             * */

            channel.queueBind(queue1, exchangeName, "error");

            channel.queueBind(queue2, exchangeName, "info");
            channel.queueBind(queue2, exchangeName, "error");
            channel.queueBind(queue2, exchangeName, "waring");
            /*
             * (String exchange, String routingKey,
             *  交换机,简单模式下使用默认    路由名称
             * BasicProperties props, byte[] body)
             * 配置信息              消息体,字节数组
             * */
            String[] str = {"info", "waring", "error"};
            Random random = new Random();
            for (int i = 0; i < 10; i++) {
                String s = str[random.nextInt(3)];
                String body = s + " : rabbitmq Routing";
                channel.basicPublish(exchangeName, s,
                        null, body.getBytes(StandardCharsets.UTF_8));
            }


        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package com.exe.consumer;

import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer_Routing {
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setVirtualHost("hcxvh");
        factory.setUsername("hcx");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        /*
         * (String exchange, String routingKey,
         *  交换机,简单模式下使用默认    路由名称
         * BasicProperties props, byte[] body)
         * 配置信息              消息体,字节数组
         * */
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
//                System.out.println("consumerTag: " + consumerTag);
//                System.out.println("Exchange: " + envelope.getExchange());
//                System.out.println("RoutingKey: " + envelope.getRoutingKey());
//                System.out.println("properties: " + properties);
                System.out.println("body: " + new String(body));
            }
        };
        String queue1 = "directQueue1";
        String queue2 = "directQueue2";
        channel.basicConsume(queue2, true, consumer); //创建多个实例消费不同队列
    }
}

Routing 模式要求队列在绑定交换机时要指定 routing key,消息会转发到符合 routing key 的队列。 

Topics 通配符模式

⚫ Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型 Exchange 可以让队列在绑定 Routing key 的时候使用通配符!

⚫ Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert

⚫ 通配符规则:# 匹配零个或多个词,* 匹配不多不少恰好1个词,例如:item.# 能够匹配 item.insert.abc 或者 item.insert,item.* 只能匹配 item.insert

package com.exe.producer;

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

import java.nio.charset.StandardCharsets;
import java.util.Random;

public class Producer_Topic {

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setVirtualHost("hcxvh");
        factory.setUsername("hcx");
        factory.setPassword("123456");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()
        ) {

            /*
             * (String queue, boolean durable, boolean exclusive,
             * 队列名称          是否持久化     是否独占,即一个消费者监听,当Connection关闭时是否删除队列
             * boolean autoDelete, Map<String, Object> arguments)
             * 当没有Consumer时是否删除           参数
             * */
            String queue1 = "topicQueue1";
            String queue2 = "topicQueue2";
            channel.queueDeclare(queue1, true, false, false, null);
            channel.queueDeclare(queue2, true, false, false, null);

            /*
            (String exchange, BuiltinExchangeType type, boolean durable,
            boolean autoDelete, Map<String, Object> arguments)
            */
            String exchangeName = "TopicExchange";
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC,//通配符匹配发送
                    true, false, null);
            /*
             * (String queue, String exchange, String routingKey)
             * */
            // *匹配一个单词    #匹配一个或多个单词
            channel.queueBind(queue1, exchangeName, "#.error");

            channel.queueBind(queue2, exchangeName, "order.*");

            /*
             * (String exchange, String routingKey,
             *  交换机,简单模式下使用默认    路由名称
             * BasicProperties props, byte[] body)
             * 配置信息              消息体,字节数组
             * */
            String[] str = {"order.info", "order.waring", "order.error", "other.error"
                    , "z.x.y.error", "order.d.d"};
            Random random = new Random();
            for (int i = 0; i < 20; i++) {
                String s = str[random.nextInt(6)];
                String body = s + " : rabbitmq Topic";
                channel.basicPublish(exchangeName, s,
                        null, body.getBytes(StandardCharsets.UTF_8));
            }


        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package com.exe.consumer;

import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer_Topic {
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setVirtualHost("hcxvh");
        factory.setUsername("hcx");
        factory.setPassword("123456");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        /*
         * (String exchange, String routingKey,
         *  交换机,简单模式下使用默认    路由名称
         * BasicProperties props, byte[] body)
         * 配置信息              消息体,字节数组
         * */
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
//                System.out.println("consumerTag: " + consumerTag);
//                System.out.println("Exchange: " + envelope.getExchange());
//                System.out.println("RoutingKey: " + envelope.getRoutingKey());
//                System.out.println("properties: " + properties);
                System.out.println("body: " + new String(body));
            }
        };
        String queue1 = "topicQueue1";
        String queue2 = "topicQueue2";
        channel.basicConsume(queue1, true, consumer);
    }
}

springboot整合

spring:
  rabbitmq:
    username: hcx
    password: 123456
    virtual-host: hcxvh
    port: 5672
    host: 192.168.88.128
package com.exe.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {
    public static final String EXCHANGE_NAME = "boot_topic_exchange";
    public static final String QUEUE_NAME = "boot_topic_queue";

    @Bean("bootExchange")
    public Exchange exchange() {
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).build();
    }

    @Bean("bootQueue")
    public Queue queue() {
        return QueueBuilder.durable(QUEUE_NAME).build();
    }
    @Bean
    public Binding bindingQueueToExchange(@Qualifier("bootQueue") Queue queue,
                                          @Qualifier("bootExchange") Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
    }
}
package com.exe;

import com.exe.config.RabbitMQConfig;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
public class Producer_Topic_Boot {
    @Resource
    private RabbitTemplate rabbitTemplate;

    @Test
    public void send() {
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.momo",
                "hello_boot");
    }
}
package com.exe.consumer;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class Consumer_Topic_Boot {
    @RabbitListener(queues = "boot_topic_queue")
    public void getMessage(Message message) {
        System.out.println(message);
    }
}

RabbitMQ 高级特性

confirm&return

在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提 供了两种方式用来控制消息的投递可靠性模式。

⚫ confirm 确认模式        application.yml配置文件中加入

spring:
    rabbitmq:
        publisher-confirm-type: simple


simple:如果消息成功到达Broker后一样会触发ConfirmCalllBack回调,
发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法
等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,
🚨注意:waitForConfirmsOrDie方法如果返回false则会关闭channel信道,则接下来无法发送消息到broker
none :表示禁用发布确认模式,默认值,使用此模式之后,不管消息有没有发送到Broker都不会触发ConfirmCallback回调。
correlated :表示消息成功到达Broker后触发 ConfirmCalllBack 回调

⚫ return 退回模式

spring:
  rabbitmq:
    publisher-returns: true

rabbitmq 整个消息投递的路径为: producer--->rabbitmq broker--->exchange--->queue--->consumer

  • 重回队列就是为了对没有处理成功的消息,把消息重新投递给broker!

  • 实际应用中一般都不开启重回队列。

⚫ 消息从 producer 到 exchange 则会返回一个 confirmCallback 。

⚫ 消息从 exchange-->queue 投递失败则会返回一个 returnCallback 。

发布确认

生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始)。

一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了, 如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。

confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息, 生产者应用程序同样可以在回调方法中处理该 nack 消息。

异步确认消息
public static void publishMessageAsync() throws Exception {
    try (Channel channel = RabbitMqUtils.getChannel()) {
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName, false, false, false, null);
        //开启发布确认
        channel.confirmSelect();
        /**
         * 线程安全有序的一个哈希表,适用于高并发的情况
         * 1.轻松的将序号与消息进行关联
         * 2.轻松批量删除条目 只要给到序列号
         * 3.支持并发访问
         */
        ConcurrentSkipListMap<Long, String> outstandingConfirms = new
                ConcurrentSkipListMap<>();
        /**
         * 确认收到消息的一个回调
         * 1.消息序列号
         * 2.true 可以确认小于等于当前序列号的消息
         * false 确认当前序列号消息
         */
        ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
            if (multiple) {
                //返回的是小于等于当前序列号的未确认消息 是一个 map
                ConcurrentNavigableMap<Long, String> confirmed =
                        outstandingConfirms.headMap(sequenceNumber, true);
                //清除该部分未确认消息
                confirmed.clear();
            } else {
                //只清除当前序列号的消息
                outstandingConfirms.remove(sequenceNumber);
            }
        };
        ConfirmCallback nackCallback = (sequenceNumber, multiple) ->
        {
            String message = outstandingConfirms.get(sequenceNumber);
            System.out.println("发布的消息" + message + "未被确认,序列号" + sequenceNumber);
        };
        /**
         * 添加一个异步确认的监听器
         * 1.确认收到消息的回调
         * 2.未收到消息的回调
         */
        channel.addConfirmListener(ackCallback, null);
        long begin = System.currentTimeMillis();
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String message = "消息" + i;
            /**
             * channel.getNextPublishSeqNo()获取下一个消息的序列号
             * 通过序列号与消息体进行一个关联
             * 全部都是未确认的消息体
             */
            outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
            channel.basicPublish("", queueName, null, message.getBytes());
            "ms");
        }
    }
    long end = System.currentTimeMillis();
    System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时" + (end - begin) +
}

confirm 模式

@Test
public void messageConfirm() {
    rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
        @Override          //  配置信息                     是否成功接收      错误原因
        public void confirm(CorrelationData correlationData, boolean b, String s) {
            if (b) {
                System.out.println("true");
            } else {
                System.out.println("false");
                System.out.println(s);
            }
        }
    });
    send(); //发送消息的test方法
}

return 模式

@Test
public void messageReturn() {
    // 设置exchange处理模式:如果没有路由到queue时,默认false:即丢弃;true:返回消息发送方ReturnsCallback ,新版本不设置也可以返回
//        rabbitTemplate.setMandatory(true);
    rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
        /*
        private final Message message;
        private final int replyCode;
        private final String replyText;
        private final String exchange;
        private final String routingKey;*/
        @Override
        public void returnedMessage(ReturnedMessage returnedMessage) {
            log.error(returnedMessage.getExchange());
            log.error(String.valueOf((returnedMessage.getReplyCode())));
            log.error(new String(returnedMessage.getMessage().getBody()));
        }
    });
    send();
}

备份交换机 

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

.withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);

mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?

谁优先级高,答案是备份交换机优先级高。 


➢ 使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。

➢ 使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到 queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。并执行回调函数returnedMessage。

➢ 在RabbitMQ中也提供了事务机制,但是性能较差,此处不做讲解。

使用channel下列方法,完成事务控制:

txSelect(), 用于将当前channel设置成transaction模式

txCommit(),用于提交事务

txRollback(),用于回滚事务


Consumer Ack

ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。

  • 消费端进行消费的时候,如果由于业务异常我们可以进行日志的记录,然后进行补偿!(也可以加上最大努力次数的尝试)

  • 如果由于服务器宕机等严重问题,那我们就需要手动进行ack保证消费端的消费成功!

有三种确认方式:

• 自动确认:acknowledge="none"

• 手动确认:acknowledge="manual"

• 根据异常情况确认:acknowledge="auto",(这种方式使用麻烦,不作讲解)

其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。

如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。

@RabbitListener(queues = "boot_topic_queue")
public void onMessage(Message message, Channel channel) throws IOException {
    long deliveryTag = message.getMessageProperties().getDeliveryTag();
    try {
        log.debug("处理逻辑");
        int i = 10 / 0;
        // RabbitMQ的ack机制中,第二个参数返回true,表示需要将这条消息投递给其他的消费者重新消费
        channel.basicAck(deliveryTag, false);
    } catch (Exception e) {
        // 第三个参数true,表示这个消息会重新进入队列
        channel.basicNack(deliveryTag, false, true);
    }
}

➢ 在配置文件中设置acknowledge属性,设置ack方式 none:自动确认,manual:手动确认

    listener:
      simple:
        acknowledge-mode: manual

➢ 如果在消费端没有出现异常,则调用channel.basicAck(deliveryTag,false);方法确认签收消息

➢ 如果出现异常,则在catch中调用 basicNack或 basicReject,拒绝消息,让MQ重新发送消息。

消息可靠性总结

1. 持久化

• exchange要持久化

• queue要持久化

• message要持久化

2. 生产方确认Confirm

3. 消费方确认Ack

4. Broker高可用 


 消费端限流

消费端的确认模式一定为手动确认。acknowledge="manual"

Caused by: org.springframework.amqp.AmqpException: No method found for class [B_sayyy的博客-CSDN博客 springboot + rabbitMQ 消费端限流限流_八月大哥的博客-CSDN博客

package com.exe.consumer;

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j
@RabbitListener(queues = "boot_topic_queue")
@Component
public class Consumer_Topic_Boot {
    //    @RabbitListener(queues = "boot_topic_queue")
    public void getMessage(Message message) {
        System.out.println(message);
    }

    @RabbitHandler(isDefault = true)
    public void onMessage(Message message, Channel channel) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            Thread.sleep(1000);
            log.debug("处理逻辑");
            // RabbitMQ的ack机制中,第二个参数返回true,表示需要将这条消息投递给其他的消费者重新消费
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            // 第三个参数true,表示这个消息会重新进入队列
            channel.basicNack(deliveryTag, false, true);
        }
    }
}

假设我们有个场景,首先,我们有个rabbitMQ服务器上有上万条消息未消费,然后我们随便打开一个消费者客户端,会出现:巨量的消息瞬间推送过来,但是我们的消费端无法同时处理这么多数据。

这时就会导致你的服务崩溃。其他情况也会出现问题,比如你的生产者与消费者能力不匹配,在高并发的情况下生产端产生大量消息,消费端无法消费那么多消息。

  • rabbitMQ提供了一种qos(服务质量保证)的功能,即非自动确认消息的前提下,如果有一定数目的消息(通过consumer或者Channel设置qos)未被确认,不进行新的消费。

void basicQOS(unit prefetchSize,ushort prefetchCount,Boolean global)方法。

  • prefetchSize:0 单条消息的大小限制。0就是不限制,一般都是不限制。

  • prefetchCount: 设置一个固定的值,告诉rabbitMQ不要同时给一个消费者推送多余N个消息,即一旦有N个消息还没有ack,则consumer将block掉,直到有消息ack

  • global:truefalse 是否将上面的设置用于channel,也是就是说上面设置的限制是用于channel级别的还是consumer的级别的。


TTL

全称 Time To Live(存活时间/过期时间)。

➢ 当消息到达存活时间后,还没有被消费,会被自动清除。

➢ RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。

➢ 消息过期后,只有消息在队列顶端,才会判断其是否过期(否则过期消息不会被移除)。 

➢ 设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。

➢ 设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断 这一消息是否过期。

➢ 如果两者都进行了设置,以时间短的为准。

如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队 列中),而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者 之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间;另外,还需 要注意的一点是,如果不设置 TTL,表示消息永远不会过期,如果将 TTL 设置为 0,则表示除非此时可以 直接投递该消息到消费者,否则该消息将会被丢弃。

@Bean("bootQueueTTL")
public Queue queueTTL() {
    return QueueBuilder.durable(QUEUE_TTL_NAME).ttl(10000).build();
}
@Test
public void messageTTL() {
    rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.momo",
            "hello_boot", new MessagePostProcessor() {
                @Override
                public Message postProcessMessage(Message message) throws AmqpException {
                    message.getMessageProperties().setExpiration(String.valueOf(1000));
                    return message;
                }
            });
}

死信队列

死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以 被重新发送到另一个交换机,这个交换机就是DLX。

消息成为死信的三种情况(消息无法被消费):

1. 队列消息长度到达限制;

2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;

3. 原队列存在消息过期设置,消息到达超时时间未被消费; 

队列绑定死信交换机: 给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key

死信队列小结

1. 死信交换机和死信队列和普通的没有区别

2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队 列

// 普通队列绑定死信交换机和出现死信时的routingkey
@Bean("bootQueue")
public Queue queue() {
    return QueueBuilder.durable(QUEUE_NAME)
            .deadLetterExchange(EXCHANGE_DLX_NAME)
            .deadLetterRoutingKey("dlx.momo") 
            .maxLength(10)
            .build();
}
// 死信队列,和普通队列没有区别
@Bean("dlxQueue")
public Queue queueDLX() {
    return QueueBuilder.durable(QUEUE_DLX_NAME).build();
}
// 死信交换机,和普通交换机没有区别
@Bean("dlxExchange")
public Exchange exchangeDLX() {
    return ExchangeBuilder.topicExchange(EXCHANGE_DLX_NAME).build();
}
// 绑定死信队列和死信交换机
@Bean
public Binding bindingDLX(@Qualifier("dlxQueue") Queue queue,
                          @Qualifier("dlxExchange") Exchange exchange) {
    return BindingBuilder.bind(queue).to(exchange).with("dlx.#").noargs();
}
@Test
public void messageDLX() {
    // 1. 消息过期且未被消费
    rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.momo",
            "onmessage", message -> {
                message.getMessageProperties().setExpiration(String.valueOf(1000));
                return message;
            }
            );
    // 2.消息队列长度到达限制
    for (int i = 0; i < 15; i++) {
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.momo",
                "onmessage", message -> {
                    message.getMessageProperties().setExpiration(String.valueOf(1000));
                    return message;
                });
    }

}
package com.exe.consumer;

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j
@RabbitListener(queues = "boot_topic_queue")
@Component
public class Consumer_Topic_Boot {
    //    @RabbitListener(queues = "boot_topic_queue")
    public void getMessage(Message message) {
        System.out.println(message);
    }

    @RabbitHandler(isDefault = true)
    public void onMessage(Message message, Channel channel) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            Thread.sleep(1000);
            log.debug("处理逻辑");
            int l=10/0;
            // RabbitMQ的ack机制中,第二个参数返回true,表示需要将这条消息投递给其他的消费者重新消费
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            // 情况3:消息被拒收,且未重返队列
            // 第三个参数true,表示这个消息会重新进入队列,不重新进入该队列则会进入死信队列
            channel.basicNack(deliveryTag, false, false);
        }
    }
}

延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。

需求:

1. 下单后,30分钟未支付,取消订单,回滚库存。

2. 新用户注册成功7天后,发送短信问候。

实现方式:

1. 定时器

2. 延迟队列

@Bean("bootQueue")
public Queue queue() {
    return QueueBuilder.durable(QUEUE_NAME)
            .deadLetterExchange(EXCHANGE_DLX_NAME)
            .deadLetterRoutingKey("dlx.momo")
            .maxLength(10)
            .ttl(1000) // 延迟消息到达死信队列
            .build();
}
@RabbitListener(queues = "dlx_queue") //消费死信队列中的消息

1. 延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费。 2. RabbitMQ没有提供延迟队列功能,但是可以使用 : TTL + DLX 来实现延迟队列效果。

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

当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz 或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景

Rabbitmq 插件实现延迟队列

上文中提到的问题,确实是一个问题,如果不能实现在消息粒度上的 TTL,并使其在设置的TTL 时间 及时死亡,就无法设计成一个通用的延时队列。

那如何解决呢,接下来我们就去解决该问题。

1. 安装延时队列插件

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

进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ

cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins 
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

 在我们自定义的交换机中,这是一种新的交换类型,该类型消息支持延迟投递机制 消息传递后并 不会立即投递到目标队列中,而是存储在 mnesia(一个分布式数据系统)表中,当达到投递时间时,才投递到目标队列中。

@Configuration
public class DelayedQueueConfig {
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
    @Bean
    public Queue delayedQueue() {
        return new Queue(DELAYED_QUEUE_NAME);
    }
    //自定义交换机 我们在这里定义的是一个延迟交换机
    @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);
    }
    @Bean
    public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,
                                       @Qualifier("delayedExchange") CustomExchange
                                               delayedExchange) {
        return
                BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@GetMapping("sendDelayMsg/{message}/{delayTime}")
public void sendMsg(@PathVariable String message,@PathVariable Integer delayTime) {
        rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, message,
        correlationData ->{
        correlationData.getMessageProperties().setDelay(delayTime);
        return correlationData;
        });
        log.info(" 当 前 时 间 : {}, 发 送 一 条 延 迟 {} 毫秒的信息给队列 delayed.queue:{}",
         new Date(),delayTime, message);
}
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receiveDelayedQueue(Message message){
    String msg = newString(message.getBody());
    log.info("当前时间:{},收到延时队列的消息:{}", new Date().toString(), msg);
}

优先级队列

public class Producer {
    private static final String QUEUE_NAME="hello";
    public static void main(String[] args) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel();) {
            //给消息赋予一个 priority 属性
                 AMQP.BasicProperties properties=new AMQP.BasicProperties().builder().priority(5).build();
                 for (int i = 1; i <11; i++) {
                    String message = "info"+i;
                    if(i==5){
                        channel.basicPublish("", QUEUE_NAME, properties, message.getBytes());
                    }else{
                        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
                    }
                    System.out.println("发送消息完成:" + message);
                }
        }
    }
}

public class Consumer {
    private static final String QUEUE_NAME="hello";
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        //设置队列的最大优先级 最大可以设置到 255 官网推荐 1-10 如果设置太高比较吃内存和 CPU
        Map<String, Object> params = new HashMap();
        params.put("x-max-priority", 10);
        channel.queueDeclare(QUEUE_NAME, true, false, false, params);
        System.out.println("消费者启动等待消费..............");
        DeliverCallback deliverCallback=(consumerTag, delivery)->{
            String receivedMessage = new String(delivery.getBody());
            System.out.println("接收到消息:"+receivedMessage);
        };
        channel.basicConsume(QUEUE_NAME,true,
                            deliverCallback,(consumerTag)->{
                            System.out.println("消费者无法消费消息时调用,如队列被删除");
                });
    }
}

惰性队列

RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。

惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。

当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。

默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中, 这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。

当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的 时间,也会阻塞队列的操作,进而无法接收新的消息。

虽然 RabbitMQ 的开发者们一直在升级相关的算法, 但是效果始终不太理想,尤其是在消息量特别大的时候。

两种模式

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

Map args = new HashMap(); args.put("x-queue-mode", "lazy"); 
channel.queueDeclare("myqueue", false, false, false, args);


日志与监控 

RabbitMQ日志

RabbitMQ默认日志存放路径: /var/log/rabbitmq/rabbit@xxx.log

之前安装时修改过路径:"/data/rabbitmq/log/rabbit@localhost.log" //localhost是主机名

日志包含了RabbitMQ的版本号、Erlang的版本号、RabbitMQ服务节点名称、cookie的hash值、 RabbitMQ配置文件地址、内存限制、磁盘限制、默认账户guest的创建以及权限配置等等。

web管控台监控

RabbitMQ Managementhttp://192.168.88.128:15672/#/15672:为默认访问端口

rabbitmqctl管理和监控

查看队列 # rabbitmqctl list_queues

查看exchanges # rabbitmqctl list_exchanges

查看用户 # rabbitmqctl list_users

查看连接 # rabbitmqctl list_connections

查看消费者信息 # rabbitmqctl list_consumers

查看环境变量 # rabbitmqctl environment

查看未被确认的队列 # rabbitmqctl list_queues name messages_unacknowledged

查看单个队列的内存使用 # rabbitmqctl list_queues name memory

查看准备就绪的队列 # rabbitmqctl list_queues name messages_ready


消息追踪

在使用任何消息中间件的过程中,难免会出现某条消息异常丢失的情况。

对于RabbitMQ而言,可能是因为生产者或消费者与RabbitMQ断开了连接,而它们与RabbitMQ又采用了不同的确认机制;也有可能是因为交换器与队列之间不同的转发策略;甚至是交换器并没有与任何队列进行绑定,生产者 又不感知或者没有采取相应的措施;另外RabbitMQ本身的集群策略也可能导致消息的丢失。

这个时候就需要有一个较好的机制跟踪记录消息的投递过程,以此协助开发和运维人员进行问题的定位。 在RabbitMQ中可以使用Firehose和rabbitmq_tracing插件功能来实现消息追踪。

消息追踪-Firehose

firehose的机制是将生产者投递给rabbitmq的消息,rabbitmq投递给消费者的消息按照指定的格式 发送到默认的exchange上。这个默认的exchange的名称为amq.rabbitmq.trace,它是一个topic类 型的exchange。发送到这个exchange上的消息的routing key为 publish.exchangename 和 deliver.queuename。其中exchangename和queuename为实际exchange和queue的名称,分别对应生产者投递到exchange的消息,和消费者从queue上获取的消息。

注意:打开 trace 会影响消息写入功能,适当打开后请关闭。

rabbitmqctl trace_on:开启Firehose命令

rabbitmqctl trace_off:关闭Firehose命令

消息追踪-rabbitmq_tracing

rabbitmq_tracing和Firehose在实现上如出一辙,只不过rabbitmq_tracing的方式比Firehose多了一 层GUI的包装,更容易使用和管理。 启用插件:rabbitmq-plugins enable rabbitmq_tracing

创建traces

 消息匹配格式

实际创建队列并绑定到amq.rabbitmq.trace中

 

发送符合格式的消息都会写到log文件中

RabbitMQ应用问题

1. 消息投递可靠性保障

•消息入库

消息入库,顾名思义就是将要发送的消息保存到数据库中。

首先发送消息前先将消息保存到数据库中,有一个状态字段status=0,表示生产端将消息发送给了RabbitMQ但还没收到确认;在生产端收到确认后将status设为1,表示RabbitMQ已收到消息。这里有可能会出现上面说的两种情况,所以生产端这边开一个定时器,定时检索消息表,将status=0并且超过固定时间后(可能消息刚发出去还没来得及确认这边定时器刚好检索到这条status=0的消息,所以给个时间)还没收到确认的消息取出重发(第二种情况下这里会造成消息重复,消费者端要做幂等性),可能重发还会失败,所以可以做一个最大重发次数,超过就做另外的处理。

• 消息补偿机制

Producer:发送消息Q1和发送延迟消息Q3

Consumer:接收消息Q1并发送确认消息Q2

定时检查服务:

第9步中:比对producer的db和消息确认的mdb,调用producer重发db中多的那些数据(即未发送成功或未被消费者成功确认的消息) 

回调检查服务:

第6步中:监听到确认消息Q2,将消息写入数据库MDB

第8步中:监听到延迟消息Q3,比对MDB中的消息,出现重复即代表该消息已被消费

2. 消息幂等性保障

• 乐观锁解决方案

幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任 意多次执行对资源本身所产生的影响均与一次执行的影响相同。

在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果。

MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费 者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消 息时用该 id 先判断该消息是否已消费过。

在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性, 这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:

a. 唯一 ID+指纹码机制,利用数据库主键去重

b.利用 redis 的原子性去实现 

  • 唯一ID+指纹码机制
    • 指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存在数据库中,
    • 优势就是实现简单,就一个拼接,然后查询判断是否重复;
    • 劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
  • Redis 原子性利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费
    • 使用Redis进行幂等时需要考虑的问题?

      • 是否进行数据库落库,落库后数据和缓存如何做到保证幂等(Redis  和数据库如何同时成功同时失败)?

      • 如果不进行落库,都放在Redis中如何这是Redis和数据库的同步策略?还有放在缓存中就能百分之百的成功吗?

3.消费端消息不丢失

既然已经可以让生产端100%可靠性投递到RabbitMQ了,那接下来就改看看消费端的了,如何让消费端不丢失消息。

默认情况下,以下3种情况会导致消息丢失:

  • 在RabbitMQ将消息发出后,消费端还没接收到消息之前,发生网络故障,消费端与RabbitMQ断开连接,此时消息会丢失;

  • 在RabbitMQ将消息发出后,消费端还没接收到消息之前,消费端挂了,此时消息会丢失;

  • 消费端正确接收到消息,但在处理消息的过程中发生异常或宕机了,消息也会丢失。

其实,上述3中情况导致消息丢失归根结底是因为RabbitMQ的自动ack机制,即默认RabbitMQ在消息发出后就立即将这条消息删除,而不管消费端是否接收到,是否处理完,导致消费端消息丢失时RabbitMQ自己又没有这条消息了。

所以就需要将自动ack机制改为手动ack机制。

DeliverCallback deliverCallback = (consumerTag, delivery) -> {
    try {
        //接收到消息,做处理
        //手动确认
        channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
    } catch (Exception e) {
        //出错处理,这里可以让消息重回队列重新发送或直接丢弃消息
    }
};
//第二个参数autoAck设为false表示关闭自动确认机制,需手动确认
channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> {});

RabbitMQ集群模式

  1. 主备模式:实现rabbitMQ高可用集群,一般在并发量和数据不大的情况下,这种模式好用简单。又称warren模式。(区别于主从模式,主从模式主节点提供写操作,从节点提供读操作,主备模式从节点不提供任何读写操作,只做备份)如果主节点宕机备份从节点会自动切换成主节点,提供服务。

  2. 集群模式:经典方式就是Mirror模式,保证100%数据不丢失,实现起来也是比较简单。

  • 镜像队列,是rabbitMQ数据高可用的解决方案,主要是实现数据同步,一般来说是由2-3节点实现数据同步,(对于100%消息可靠性解决方案一般是3个节点)

多活模式:这种模式也是实现异地数据复制的主流模式,因为shovel模式配置相对复杂,所以一般来说实现异地集群都是使用这种双活,多活的模式,这种模式需要依赖rabbitMQ的federation插件,可以实现持续可靠的AMQP数据。

rabbitMQ部署架构采用双中心模式(多中心)在两套(或多套)数据中心各部署一套rabbitMQ集群,各中心的rabbitMQ服务需要为提供正常的消息业务外,中心之间还需要实现部分队列消息共享。

多活架构如下:

federation插件是一个不需要构建Cluster,而在Brokers之间传输消息的高性能插件,federation可以在brokers或者cluster之间传输消息,连接的双方可以使用不同的users或者virtual host双方也可以使用不同版本的erlang或者rabbitMQ版本。federation插件可以使用AMQP协议作为通讯协议,可以接受不连续的传输。

Federation Exchanges,可以看成Downstream从Upstream主动拉取消息,但
并不是拉取所有消息,必须是在Downstream上已经明确定义Bindings关系的
Exchange,也就是有实际的物理Queue来接收消息,才会从Upstream拉取消息
到Downstream。

使用AMQP协议实施代理间通信,Downstream 会将绑定关系组合在一起, 绑定/解除绑定命令将发送到Upstream交换机。

因此,Federation Exchange只接收具有订阅的消息。

HAProxy是一款提供高可用性、负载均衡以及基于TCP (第四层)和HTTP
(第七层)应用的代理软件,支持虚拟主机,它是免费、快速并且可靠的一种解决
方案。

HAProxy特别适用于那些负载特大的web站点,这些站点通常又需要会
话保持或七层处理。HAProxy运行在时下的硬件上,完全可以支持数以万计的
并发连接。

并且它的运行模式使得它可以很简单安全的整合进您当前的架构中
同时可以保护你的web服务器不被暴露到网络上。

HAProxy性能为何这么好?

  1. 单进程、事件驱动模型显著降低了.上下文切换的开销及内存占用.

  2. 在任何可用的情况下,单缓冲(single buffering)机制能以不复制任何数据的方式完成读写操作,这会节约大量的CPU时钟周期及内存带宽

  3. 借助于Linux 2.6 (>= 2.6.27.19). 上的splice()系统调用,HAProxy可以实现零复制转发(Zero-copy forwarding),在Linux 3.5及以上的OS中还可以实现心零复制启动(zero-starting)

  4. 内存分配器在固定大小的内存池中可实现即时内存分配,这能够显著减少创建一个会话的时长

  5. 树型存储:侧重于使用作者多年前开发的弹性二叉树,实现了以O(log(N))的低开销来保持计时器命令、保持运行队列命令及管理轮询及最少连接队列

keepAlive

KeepAlived软件主要是通过VRRP协议实现高可用功能的。VRRP是
Virtual Router RedundancyProtocol(虚拟路由器冗余协议)的缩写,
VRRP出现的目的就是为了解决静态路由单点故障问题的,它能够保证当
个别节点宕机时,整个网络可以不间断地运行所以,Keepalived - -方面
具有配置管理LVS的功能,同时还具有对LVS下面节点进行健康检查的功
能,另一方面也可实现系统网络服务的高可用功能

keepAlive的作用:

  1. 管理LVS负载均衡软件

  2. 实现LVS集群节点的健康检查中

  3. 作为系统网络服务的高可用性(failover)

Keepalived如何实现高可用

Keepalived高可用服务对之间的故障切换转移,是通过VRRP (Virtual Router
Redundancy Protocol ,虚拟路由器冗余协议)来实现的。

在Keepalived服务正常工作时,主Master节点会不断地向备节点发送( 多播的方式)心跳消息,用以告诉备Backup节点自己还活着,当主Master节点发生故障时,就无法发送心跳消息,备节点也就因此无法继续检测到来自主Master节点的心跳了,于是调用自身的接管程序,接管主Master节点的IP资源及服务。

而当主Master节点恢复时备Backup节点又会释放主节点故障时自身接管的IP资源及服务,恢复到原来的备用角色。


RabbitMQ集群搭建——黑马版本

摘要:实际生产应用中都会采用消息队列的集群方案,如果选择RabbitMQ那么有必要了解下它的集群方案原理

一般来说,如果只是为了学习RabbitMQ或者验证业务工程的正确性那么在本地环境或者测试环境上使用其单实例部署就可以了,但是出于MQ中间件本身的可靠性、并发性、吞吐量和消息堆积能力等问题的考虑,在生产环境上一般都会考虑使用RabbitMQ的集群方案。

3.1 集群方案的原理

RabbitMQ这款消息队列中间件产品本身是基于Erlang编写,Erlang语言天生具备分布式特性(通过同步Erlang集群各节点的magic cookie来实现)。因此,RabbitMQ天然支持Clustering。这使得RabbitMQ本身不需要像ActiveMQ、Kafka那样通过ZooKeeper分别来实现HA方案和保存集群的元数据。集群是保证可靠性的一种方式,同时可以通过水平扩展以达到增加消息吞吐量能力的目的。

3.2 单机多实例部署

由于某些因素的限制,有时候你不得不在一台机器上去搭建一个rabbitmq集群,这个有点类似zookeeper的单机版。真实生成环境还是要配成多机集群的。有关怎么配置多机集群的可以参考其他的资料,这里主要论述如何在单机中配置多个rabbitmq实例。

主要参考官方文档:Clustering Guide — RabbitMQ

首先确保RabbitMQ运行没有问题

[root@super ~]# rabbitmqctl status
Status of node rabbit@super ...
[{pid,10232},
 {running_applications,
     [{rabbitmq_management,"RabbitMQ Management Console","3.6.5"},
      {rabbitmq_web_dispatch,"RabbitMQ Web Dispatcher","3.6.5"},
      {webmachine,"webmachine","1.10.3"},
      {mochiweb,"MochiMedia Web Server","2.13.1"},
      {rabbitmq_management_agent,"RabbitMQ Management Agent","3.6.5"},
      {rabbit,"RabbitMQ","3.6.5"},
      {os_mon,"CPO  CXC 138 46","2.4"},
      {syntax_tools,"Syntax tools","1.7"},
      {inets,"INETS  CXC 138 49","6.2"},
      {amqp_client,"RabbitMQ AMQP Client","3.6.5"},
      {rabbit_common,[],"3.6.5"},
      {ssl,"Erlang/OTP SSL application","7.3"},
      {public_key,"Public key infrastructure","1.1.1"},
      {asn1,"The Erlang ASN1 compiler version 4.0.2","4.0.2"},
      {ranch,"Socket acceptor pool for TCP protocols.","1.2.1"},
      {mnesia,"MNESIA  CXC 138 12","4.13.3"},
      {compiler,"ERTS  CXC 138 10","6.0.3"},
      {crypto,"CRYPTO","3.6.3"},
      {xmerl,"XML parser","1.3.10"},
      {sasl,"SASL  CXC 138 11","2.7"},
      {stdlib,"ERTS  CXC 138 10","2.8"},
      {kernel,"ERTS  CXC 138 10","4.2"}]},
 {os,{unix,linux}},
 {erlang_version,
     "Erlang/OTP 18 [erts-7.3] [source] [64-bit] [async-threads:64] [hipe] [kernel-poll:true]\n"},
 {memory,
     [{total,56066752},
      {connection_readers,0},
      {connection_writers,0},
      {connection_channels,0},
      {connection_other,2680},
      {queue_procs,268248},
      {queue_slave_procs,0},
      {plugins,1131936},
      {other_proc,18144280},
      {mnesia,125304},
      {mgmt_db,921312},
      {msg_index,69440},
      {other_ets,1413664},
      {binary,755736},
      {code,27824046},
      {atom,1000601},
      {other_system,4409505}]},
 {alarms,[]},
 {listeners,[{clustering,25672,"::"},{amqp,5672,"::"}]},
 {vm_memory_high_watermark,0.4},
 {vm_memory_limit,411294105},
 {disk_free_limit,50000000},
 {disk_free,13270233088},
 {file_descriptors,
     [{total_limit,924},{total_used,6},{sockets_limit,829},{sockets_used,0}]},
 {processes,[{limit,1048576},{used,262}]},
 {run_queue,0},
 {uptime,43651},
 {kernel,{net_ticktime,60}}]

停止rabbitmq服务

[root@super sbin]# service rabbitmq-server stop
Stopping rabbitmq-server: rabbitmq-server.
​

启动第一个节点:

[root@super sbin]# RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=rabbit1 rabbitmq-server start
​
              RabbitMQ 3.6.5. Copyright (C) 2007-2016 Pivotal Software, Inc.
  ##  ##      Licensed under the MPL.  See http://www.rabbitmq.com/
  ##  ##
  ##########  Logs: /var/log/rabbitmq/rabbit1.log
  ######  ##        /var/log/rabbitmq/rabbit1-sasl.log
  ##########
              Starting broker...
 completed with 6 plugins.

启动第二个节点:

web管理插件端口占用,所以还要指定其web插件占用的端口号。

[root@super ~]# RABBITMQ_NODE_PORT=5674 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15674}]" RABBITMQ_NODENAME=rabbit2 rabbitmq-server start
​
              RabbitMQ 3.6.5. Copyright (C) 2007-2016 Pivotal Software, Inc.
  ##  ##      Licensed under the MPL.  See http://www.rabbitmq.com/
  ##  ##
  ##########  Logs: /var/log/rabbitmq/rabbit2.log
  ######  ##        /var/log/rabbitmq/rabbit2-sasl.log
  ##########
              Starting broker...
 completed with 6 plugins.
​

结束命令:

rabbitmqctl -n rabbit1 stop
rabbitmqctl -n rabbit2 stop

rabbit1操作作为主节点:

[root@super ~]# rabbitmqctl -n rabbit1 stop_app  
Stopping node rabbit1@super ...
[root@super ~]# rabbitmqctl -n rabbit1 reset     
Resetting node rabbit1@super ...
[root@super ~]# rabbitmqctl -n rabbit1 start_app
Starting node rabbit1@super ...
[root@super ~]# 

rabbit2操作为从节点:

[root@super ~]# rabbitmqctl -n rabbit2 stop_app
Stopping node rabbit2@super ...
[root@super ~]# rabbitmqctl -n rabbit2 reset
Resetting node rabbit2@super ...
[root@super ~]# rabbitmqctl -n rabbit2 join_cluster rabbit1@'super' ###''内是主机名换成自己的
Clustering node rabbit2@super with rabbit1@super ...
[root@super ~]# rabbitmqctl -n rabbit2 start_app
Starting node rabbit2@super ...
​

查看集群状态:

[root@super ~]# rabbitmqctl cluster_status -n rabbit1
Cluster status of node rabbit1@super ...
[{nodes,[{disc,[rabbit1@super,rabbit2@super]}]},
 {running_nodes,[rabbit2@super,rabbit1@super]},
 {cluster_name,<<"rabbit1@super">>},
 {partitions,[]},
 {alarms,[{rabbit2@super,[]},{rabbit1@super,[]}]}]

web监控:

3.3 集群管理

rabbitmqctl join_cluster {cluster_node} [–ram] 将节点加入指定集群中。在这个命令执行前需要停止RabbitMQ应用并重置节点。

rabbitmqctl cluster_status 显示集群的状态。

rabbitmqctl change_cluster_node_type {disc|ram} 修改集群节点的类型。在这个命令执行前需要停止RabbitMQ应用。

rabbitmqctl forget_cluster_node [–offline] 将节点从集群中删除,允许离线执行。

rabbitmqctl update_cluster_nodes {clusternode}

在集群中的节点应用启动前咨询clusternode节点的最新信息,并更新相应的集群信息。这个和join_cluster不同,它不加入集群。考虑这样一种情况,节点A和节点B都在集群中,当节点A离线了,节点C又和节点B组成了一个集群,然后节点B又离开了集群,当A醒来的时候,它会尝试联系节点B,但是这样会失败,因为节点B已经不在集群中了。

rabbitmqctl cancel_sync_queue [-p vhost] {queue} 取消队列queue同步镜像的操作。

rabbitmqctl set_cluster_name {name} 设置集群名称。集群名称在客户端连接时会通报给客户端。Federation和Shovel插件也会有用到集群名称的地方。集群名称默认是集群中第一个节点的名称,通过这个命令可以重新设置。

3.4 RabbitMQ镜像集群配置

上面已经完成RabbitMQ默认集群模式,但并不保证队列的高可用性,尽管交换机、绑定这些可以复制到集群里的任何一个节点,但是队列内容不会复制。虽然该模式解决一项目组节点压力,但队列节点宕机直接导致该队列无法应用,只能等待重启,所以要想在队列节点宕机或故障也能正常应用,就要复制队列内容到集群里的每个节点,必须要创建镜像队列。

镜像队列是基于普通的集群模式的,然后再添加一些策略,所以你还是得先配置普通集群,然后才能设置镜像队列,我们就以上面的集群接着做。

设置的镜像队列可以通过开启的网页的管理端Admin->Policies,也可以通过命令。

rabbitmqctl set_policy my_ha "^" '{"ha-mode":"all"}'

  • Name:策略名称

  • Pattern:匹配的规则,如果是匹配所有的队列,是^.

  • Definition:使用ha-mode模式中的all,也就是同步所有匹配的队列。问号链接帮助文档。

3.5 负载均衡-HAProxy

HAProxy提供高可用性、负载均衡以及基于TCP和HTTP应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案,包括Twitter,Reddit,StackOverflow,GitHub在内的多家知名互联网公司在使用。HAProxy实现了一种事件驱动、单一进程模型,此模型支持非常大的并发连接数。

3.5.1 安装HAProxy

//下载依赖包
yum install gcc vim wget
//上传haproxy源码包
//解压
tar -zxvf haproxy-1.6.5.tar.gz -C /usr/local
//进入目录、进行编译、安装
cd /usr/local/haproxy-1.6.5
make TARGET=linux31 PREFIX=/usr/local/haproxy
make install PREFIX=/usr/local/haproxy
mkdir /etc/haproxy
//赋权
groupadd -r -g 149 haproxy
useradd -g haproxy -r -s /sbin/nologin -u 149 haproxy
//创建haproxy配置文件
mkdir /etc/haproxy
vim /etc/haproxy/haproxy.cfg

3.5.2 配置HAProxy

配置文件路径:/etc/haproxy/haproxy.cfg

#logging options
global
    log 127.0.0.1 local0 info
    maxconn 5120
    chroot /usr/local/haproxy
    uid 99
    gid 99
    daemon
    quiet
    nbproc 20
    pidfile /var/run/haproxy.pid
​
defaults
    log global
    
    mode tcp
​
    option tcplog
    option dontlognull
    retries 3
    option redispatch
    maxconn 2000
    contimeout 5s
   
     clitimeout 60s
​
     srvtimeout 15s 
#front-end IP for consumers and producters
​
listen rabbitmq_cluster
    bind 0.0.0.0:5672 //对外提供服务的端口
    
    mode tcp
    #balance url_param userid
    #balance url_param session_id check_post 64
    #balance hdr(User-Agent)
    #balance hdr(host)
    #balance hdr(Host) use_domain_only
    #balance rdp-cookie
    #balance leastconn
    #balance source //ip
    
    balance roundrobin
    
        server node1 127.0.0.1:5673 check inter 5000 rise 2 fall 2
        server node2 127.0.0.1:5674 check inter 5000 rise 2 fall 2
​
listen stats
    bind 172.16.98.133:8100 //管理后台
    mode http
    option httplog
    stats enable
    stats uri /rabbitmq-stats
    stats refresh 5s

启动HAproxy负载

/usr/local/haproxy/sbin/haproxy -f /etc/haproxy/haproxy.cfg
//查看haproxy进程状态
ps -ef | grep haproxy
​
访问如下地址对mq节点进行监控
http://172.16.98.133:8100/rabbitmq-stats

代码中访问mq集群地址,则变为访问haproxy地址:5672


RabbitMQ 集群——尚硅谷版本

1.搭建步骤

  • 1.修改 3 台机器的主机名称
    • vim /etc/hostname
  • 2.配置各个节点的 hosts 文件,让各个节点都能互相识别对方
    • vim /etc/hosts 10.211.55.74 node1 10.211.55.75 node2 10.211.55.76 node3
  • 3.以确保各个节点的 cookie 文件使用的是同一个值 在 node1 上执行远程操作命令
    • scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie
    • scp /var/lib/rabbitmq/.erlang.cookie root@node3:/var/lib/rabbitmq/.erlang.cookie
  • 4.启动 RabbitMQ 服务,顺带启动 Erlang 虚拟机和 RbbitMQ 应用服务(在三台节点上分别执行以 下命令)
    • rabbitmq-server -detached
  • 5.在节点 2 执行
    • rabbitmqctl stop_app (rabbitmqctl stop 会将Erlang 虚拟机关闭,rabbitmqctl stop_app 只关闭 RabbitMQ 服务)
    • rabbitmqctl reset
    • rabbitmqctl join_cluster rabbit@node1
    • rabbitmqctl start_app(只启动应用服务)
  • 6.在节点 3 执行
    • rabbitmqctl stop_app
    • rabbitmqctl reset
    • rabbitmqctl join_cluster rabbit@node2
    • rabbitmqctl start_app
  • 7.集群状态 rabbitmqctl cluster_status
  • 8.需要重新设置用户
    • 创建账号 rabbitmqctl add_user admin 123
    • 设置用户角色 rabbitmqctl set_user_tags admin administrator
    • 设置用户权限 rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
  • 9.解除集群节点(node2 和 node3 机器分别执行)
    • rabbitmqctl stop_app
    • rabbitmqctl reset
    • rabbitmqctl start_app
    • rabbitmqctl cluster_status
    • rabbitmqctl forget_cluster_node rabbit@node2(node1 机器上执行)

2.镜像队列

如果 RabbitMQ 集群中只有一个 Broker 节点,那么该节点的失效将导致整体服务的临时性不可用,并且也可能会导致消息的丢失。可以将所有消息都设置为持久化,并且对应队列的durable属性也设置为true,但是这样仍然无法避免由于缓存导致的问题:因为消息在发送之后和被写入磁盘井执行刷盘动作之间存在一 个短暂却会产生问题的时间窗。通过 publisherconfirm 机制能够确保客户端知道哪些消息己经存入磁盘。

尽管如此,一般不希望遇到因单点故障导致的服务不可用。引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他 Broker 节点之上,如果集群中的一个节点失效了,队列能自动地切换到镜像中的另一个节点上以保证服务的可用性。


Haproxy+Keepalive 实现高可用负载均衡

 整体架构图

 

Haproxy 实现负载均衡

HAProxy 提供高可用性、负载均衡及基于TCPHTTP 应用的代理,支持虚拟主机,它是免费、快速并 且可靠的一种解决方案,包括 Twitter,Reddit,StackOverflow,GitHub 在内的多家知名互联网公司在使用。 HAProxy 实现了一种事件驱动、单一进程模型,此模型支持非常大的井发连接数。

扩展 nginx,lvs,haproxy 之间的区别: (总结)Nginx/LVS/HAProxy负载均衡软件的优缺点详解 (ha97.com)

搭建步骤

  • 1.下载 haproxy(在 node1 和 node2)
    • yum -y install haproxy
  • 2.修改 node1 和 node2 的 haproxy.cfg
    • vim /etc/haproxy/haproxy.cfg 需要修改红色 IP 为当前机器 IP
  • 3.在两台节点启动 haproxy
    • haproxy -f /etc/haproxy/haproxy.cfg
    • ps -ef | grep haproxy
  • 4.访问地址 http://10.211.55.71:8888/stats 

Keepalived 实现双机(主备)热备

试想如果前面配置的 HAProxy 主机突然宕机或者网卡失效,那么虽然 RbbitMQ 集群没有任何故障但是 对于外界的客户端来说所有的连接都会被断开结果将是灾难性的为了确保负载均衡服务的可靠性同样显得 十分重要,这里就要引入 Keepalived 它能够通过自身健康检查、资源接管功能做高可用(双机热备),实现 故障转移.

搭建步骤

  • 1.下载 keepalived
    • yum -y install keepalived
  • 2.节点 node1 配置文件
    • vim /etc/keepalived/keepalived.conf 把资料里面的 keepalived.conf 修改之后替换
  • 3.节点 node2 配置文件
    • 需要修改global_defs 的 router_id,如:nodeB
    • 其次要修改 vrrp_instance_VI 中 state 为"BACKUP";
    • 最后要将priority 设置为小于 100 的值
  • 4.添加 haproxy_chk.sh (为了防止 HAProxy 服务挂掉之后 Keepalived 还在正常工作而没有切换到 Backup 上,所以这里需要编写一个脚本来检测 HAProxy 服务的状态,当 HAProxy 服务挂掉之后该脚本会自动重启 HAProxy 的服务,如果不成功则关闭 Keepalived 服务,这样便可以切换到 Backup 继续工作)
    • vim /etc/keepalived/haproxy_chk.sh(可以直接上传文件)
    • 修改权限 chmod 777 /etc/keepalived/haproxy_chk.sh
    • #!/bin/bash
      START_HAPROXY="/usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg" #haproxy启动命令
      LOG_FILE="/usr/local/keepalived/log/haproxy-check.log" # 日志文件
      HAPS=`ps -C haproxy --no-header |wc -l` # 检测haproxy的状态,0代表未启动,1已经启动
      date "+%Y-%m-%d %H:%M:%S" >> $LOG_FILE #在日志文件当中记录检测时间
      echo "check haproxy status" >> $LOG_FILE # 记录haproxy的状态
      if [ $HAPS -eq 0 ];then #执行haproxy判断
        echo $START_HAPROXY >> $LOG_FILE #记录启动命令
        /usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg #启动haproxy
        sleep 3
        if [ `ps -C haproxy --no-header |wc -l` -eq 0 ];then
          echo "start haproxy failed, killall keepalived" >> $LOG_FILE
          killall keepalived
          service keepalived stop
        fi
      fi

  • 5.启动 keepalive 命令(node1 和 node2 启动) systemctl start keepalived
  • 6. 观察 Keepalived 的日志 tail -f /var/log/messages -n 200
  • 7.观察最新添加的 vip ip add show
  • 8. node1 模拟 keepalived 关闭状态 systemctl stop keepalived
  • 9. 使用 vip 地址来访问 rabbitmq 集群

Federation Exchange

(broker 北京),(broker 深圳)彼此之间相距甚远,网络延迟是一个不得不面对的问题。有一个在北京 的业务(Client 北京) 需要连接(broker 北京),向其中的交换器 exchangeA 发送消息,此时的网络延迟很小, (Client 北京)可以迅速将消息发送至 exchangeA 中,就算在开启了 publisherconfirm 机制或者事务机制的情 况下,也可以迅速收到确认信息。

此时又有个在深圳的业务(Client 深圳)需要向 exchangeA 发送消息, 那 么(Client 深圳) (broker 北京)之间有很大的网络延迟,(Client 深圳) 将发送消息至 exchangeA 会经历一定的延迟,尤其是在开启了 publisherconfirm 机制或者事务机制的情况下,(Client 深圳) 会等待很长的延迟 时间来接收(broker 北京)的确认信息,进而必然造成这条发送线程的性能降低,甚至造成一定程度上的阻 塞。

将业务(Client 深圳)部署到北京的机房可以解决这个问题,但是如果(Client 深圳)调用的另些服务都部 署在深圳,那么又会引发新的时延问题,总不见得将所有业务全部部署在一个机房,那么容灾又何以实现? 这里 使用 Federation 插件就可以很好地解决这个问题.

搭建步骤

  • 1.需要保证每台节点单独运行
  • 2.在每台机器上开启 federation 相关插件
    • rabbitmq-plugins enable
    • rabbitmq_federation
    • rabbitmq-plugins enable
    • rabbitmq_federation_management
  • 3.原理图(先运行 consumer 在 node2 创建 fed_exchange)
  • 4.在 downstream(node2)配置 upstream(node1) ,添加 policy

Federation Queue

联邦队列可以在多个 Broker 节点(或者集群)之间为单个队列提供均衡负载的功能。一个联邦队列可以 连接一个或者多个上游队列(upstream queue),并从这些上游队列中获取消息以满足本地消费者消费消息 的需求。

Shovel

Federation 具备的数据转发功能类似,Shovel 够可靠、持续地从一个 Broker 中的队列(作为源端,即 source)拉取数据并转发至另一个 Broker 中的交换器(作为目的端,即 destination)。作为源端的队列和作为 目的端的交换器可以同时位于同一个 Broker,也可以位于不同的 Broker 上。Shovel 可以翻译为"铲子",是 一种比较形象的比喻,这个"铲子"可以将消息从一方"铲子"另一方。Shovel 行为就像优秀的客户端应用程 序能够负责连接源和目的地、负责消息的读写及负责连接失败问题的处理。

搭建步骤

  • 1.开启插件(需要的机器都开启)
    • rabbitmq-plugins enable rabbitmq_shovel rabbitmq-plugins enable
    • rabbitmq_shovel_management
  • 2.原理图(在源头发送的消息直接回进入到目的地队列)
  • 3.添加 shovel 源和目的地
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值