RabbitMQ 消息队列入门

什么是 RabbitMQ

MQ(Message Queue)消息队列

消息队列中间件,是分布式系统中的重要组件;主要解决异步处理、应用解耦、流量削峰等问题,从而实现高性能,高可用,可伸缩和最终一致性的架构。

使用较多的消息队列产品:RabbitMQ,RocketMQ,ActiveMQ,ZeroMQ,Kafka 等。

异步处理

用户注册后,需要发送验证邮箱和手机验证码。

将注册信息写入数据库,发送验证邮件,发送手机,三个步骤全部完成后,返回给客户端。

传统:

客户端 <-> 注册信息写入数据库 -> 发送注册邮件 -> 发送注册短信

现在:

客户端 <-> 注册信息写入数据库 -> 写入消息队列 -> 异步 [发送注册邮件,发送注册短信]
应用解耦

场景:订单系统需要通知库存系统。

如果库存系统异常,则订单调用库存失败,导致下单失败。

原因:订单系统和库存系统耦合度太高。

传统:

用户 <-> 订单系统 - 调用库存接口 -> 库存系统

现在:

用户 <-> 订单系统 - 写入 -> 消息队列 <- 订阅 - 库存系统 

订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户,下单成功。

库存系统:订阅下单的消息,获取下单信息,库存系统根据下单信息,再进行库存操作。

假如:下单的时候,库存系统不能正常运行,也不会影响下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了,实现了订单系统和库存系统的应用解耦。

所以,消息队列是典型的“生产者-消费者“模型。

生产者不断的向消息队列中生产消息,消费者不断的从队列中获取消息。

因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的入侵,这样就实现了生产者和消费者的解耦。

流量削峰

抢购,秒杀等业务,针对高并发的场景。

因为流量过大,暴增会导致应用挂掉,为解决这个问题,在前端加入消息队列。

用户的请求,服务器接收后,首先写入消息队列,如果超过队列的长度,就抛弃,发送一个结束的页面;而请求成功的就是进入队列的用户。

背景知识介绍

AMQP 高级消息队列协议

Advanced Message Queuing Protocol 是一个提供统一消息服务的应用层标准高级消息队列协议。

协议:数据在传输的过程中必须要遵守的规则。

基于此协议的客户端可以与消息中间件传递消息。

并不受产品、开发语言等条件的限制。

JMS

Java Message Server 是 Java 消息服务应用程序接口,一种规范,和 JDBC 担任的角色类似。

JMS 是一个 Java 平台中关于面向消息中间件的 API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。

二者的联系

JMS 是定义了统一接口,统一消息操作;AMQP 通过协议统一数据交互格式。

JMS 必须是 Java 语言;AMQP 只是协议,与语言无关。

Erlang 语言

Erlang 是一种通用的面向并发的编程语言,目的是创造一种可以应对大规模并发活动的编程语言和运行环境。

最初是专门为通信应用设计的,比如控制交换机或者变换协议等,因此非常适合构建分布式,实时软并行计算系统。

Erlang 运行时环境是一个虚拟机,有点像 Java 的虚拟机,这样代码一经编译,同样可以随处运行。

为什么选择 RabbitMQ

RabbitMQ 由 Erlang 开发,AMQP 的最佳搭档,安装部署简单,上手门槛低。

企业级消息队列,经过大量实践考验的高可靠,大量成功的应用案例,例如阿里、网易等一线大厂都有使用。

有强大的 WEB 管理页面。

强大的社区支持,为技术进步提供动力。

支持消息持久化、支持消息确认机制、灵活的任务分发机制等,支持功能非常丰富。

集群扩展很容易,并且可以通过增加节点实现成倍的性能提升。

总结:如果希望使用一个可靠性高、功能强大、易于管理的消息队列系统那么就选择 RabbitMQ;如果想用一个性能高,但偶尔丢点数据,可以使用 Kafka 或者 ZeroMQ。

Kafka 和 ZeroMQ 的性能比 RabbitMQ 好很多。

RabbitMQ 各组件功能

Publisher --> Exchange --banding--> Queue --> Connection --> Consumer

|-------------------------------|
|   |-------------------------| |
|   |   |------------------|  | |
|   |   |Exchange --> Queue|  | |    
|   |   |------------------|  | |
|   |       Virtual Host      | |
|   |-------------------------| |
|              Broker           |
|-------------------------------|

Broker 包含 Virtual Host
Virtual Host 包含 Exchange 和 Queue

Connection 包含多个 Channel

Broker - 消息队列服务器实体。

Virtual Host - 虚拟主机:

  • 标识一批交换机、消息队列和相关对象,形成的整体。
  • 虚拟主机是共享相同的身份认证和加密环境的独立服务器域。
  • 每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。
  • VHost 是 AMQP 概念的基础,RabbitMQ 默认的 vhost 是 /,必须在链接时指定。

Exchange - 交换器(路由):用来接收生产者发送的消息并将这些消息通过路由发给服务器中的队列。

Banding - 绑定。

Queue - 消息队列:

  • 用来保存消息直到发送给消费者。
  • 它是消息的容器,也是消息的终点。
  • 一个消息可投入一个或多个队列。
  • 消息一直在队列里面,等待消费者连接到这个队列将其取走。

Banding - 绑定:用于消息队列和交换机之间的关联。

Channel - 通道(信道):

  • 多路复用连接中的一条独立的双向数据流通道。
  • 信道是建立在真实的 TCP 连接内的虚拟链接。
  • AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,都是通过信道完成的。
  • 因为对于操作系统来说,建立和销毁 TCP 连接都是非常昂贵的开销,所以引入了信道的概
    念,用来复用 TCP 连接。

Connection - 网络连接,比如一个 TCP 连接。

Publisher - 消息的生产者,也是一个向交换器发布消息的客户端应用程序。

Consumer - 消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。

Message - 消息:

  • 消息是不具名的,它是由消息头和消息体组成。
  • 消息体是不透明的,而消息头则是由一系列的可选属性组成,这些属性包括 routing-key(路由键)、priority(优先级)、delivery-mode(消息可能需要持久性存储[消息的路由模式])等。

使用 RabbitMQ

想要安装 RabbitMQ,必须先安装 erlang 语言环境;类似安装 tomcat,必须先安装 JDK。

查看匹配的版本:https://www.rabbitmq.com/which-erlang.html

RabbitMQ 安装启动

Erlang 下载:https://dl.bintray.com/rabbitmq-erlang/rpm/erlang

Socat 下载:http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm

RabbitMQ 下载:https://www.rabbitmq.com/install-rpm.html#downloads

安装

启动 Linux 系统(192.168.186.128),传输相关的三个 rpm 到 /opt 目录下,然后在 /opt 目录下按顺序执行安装命令:

rpm -ivh erlang-21.3.8.16-1.el7.x86_64.rpm
rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
rpm -ivh rabbitmq-server-3.8.6-1.el7.noarch.rpm
启动后台管理插件
rabbitmq-plugins enable rabbitmq_management
启动 RabbitMQ
systemctl start rabbitmq-server.service
systemctl status rabbitmq-server.service
systemctl restart rabbitmq-server.service
systemctl stop rabbitmq-server.service
查看进程
ps -ef | grep rabbitmq
测试
  1. 防火墙开放对应的端口号
firewall-cmd --zone=public --add-port=15672/tcp --permanent
firewall-cmd --zone=public --add-port=5671/tcp --permanent
firewall-cmd --zone=public --add-port=5672/tcp --permanent
firewall-cmd --zone=public --add-port=25672/tcp --permanent
firewall-cmd --reload

2)浏览器输入:http://192.168.186.128:15672

3)默认帐号和密码是 guest,而 guest 用户默认不允许远程连接

创建账号:

rabbitmqctl add_user renda 123456

设置用户角色:

rabbitmqctl set_user_tags renda administrator

设置用户权限:

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

查看当前用户和角色:

rabbitmqctl list_users

修改用户密码:

rabbitmqctl change_password renda NewPassword

管理界面介绍:

  • Overview - 概览

  • Connections - 查看链接情况

  • Channels - 信道(通道)情况

  • Exchanges - 交换机(路由)情况,默认4类7个

  • Queues - 消息队列情况

  • Admin - 管理员列表

  • RabbitMQ 提供给编程语言客户端链接的端口 - 5672;RabbitMQ 管理界面的端口 15672;RabbitMQ 集群的端口 - 25672。

RabbitMQ 快速入门

依赖
<!-- 指定编码及版本 -->
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    <java.version>1.11</java.version>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
</properties>

<dependencies>
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.7.3</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.25</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.9</version>
    </dependency>
</dependencies>
日志依赖 log4j(可选项)
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %m%n
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=rebbitmq.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l %m%n
log4j.rootLogger=debug, stdout,file
创建连接

先在 RabbitMQ 管理界面 Admin -> Virtual Hosts -> Add a new virtual host 创建虚拟主机 (Name: /renda, Description: 张人大, Tags: administrator);

然后编写连接的代码:

public class ConnectionUtil {
    
    

    public static Connection getConnection() throws  Exception{
    
    
        // 1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2.在工厂对象中设置 MQ 的连接信息(ip, port, vhost, username, password)
        factory.setHost("192.168.186.128");
        factory.setPort(5672);
        factory.setVirtualHost("/renda");
        factory.setUsername("renda");
        factory.setPassword("123456");
        // 3.通过工厂获得与 MQ 的连接
        return factory.newConnection();
    }

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = getConnection();
        System.out.println("Connection: " + connection);
        connection.close();
    }

}

RabbitMQ 模式

RabbitMQ 提供了 6 种消息模型,但是第 6 种其实是 RPC,并不是 MQ。

在线手册:https://www.rabbitmq.com/getstarted.html

5 种消息模型,大体分为两类:

  • 1 和 2 属于点对点。
  • 3、4、5 属于发布订阅模式(一对多)。

点对点模式 - P2P(Point to Point)模式:

  • 包含三个角色:消息队列 queue,发送者 sender,接收者 receiver。

  • 每个消息发送到一个特定的队列中,接收者从中获得消息。

  • 队列中保留这些消息,直到他们被消费或超时。

  • 如果希望发送的每个消息都会被成功处理,那需要 P2P。

点对点模式特点:

  • 每个消息只有一个消费者,一旦消费,消息就不在队列中了。
  • 发送者和接收者之间没有依赖性,发送者发送完成,不管接收者是否运行,都不会影响消息发送到队列中。
  1. 接收者成功接收消息之后需向对象应答成功(确认)。

发布订阅模式 - publish / subscribe 模式:

  • Pub / Sub 模式包含三个角色:交换机 exchange,发布者 publisher,订阅者 subcriber。
  • 多个发布者将消息发送交换机,系统将这些消息传递给多个订阅者。
  • 如果希望发送的消息被多个消费者处理,可采用 Pub / Sub。

发布订阅模式特点:

  • 每个消息可以有多个订阅者。
  • 发布者和订阅者之间在时间上有依赖,对于某个交换机的订阅者,必须创建一个订阅后,才能消费发布者的消息。
  1. 为了消费消息,订阅者必须保持运行状态。
简单模式

RabbitMQ 本身只是接收,存储和转发消息,并不会对信息进行处理;类似邮局,处理信件的应该是收件人而不是邮局。

生产者 P
public class Sender {
    
    

    public static void main(String[] args) throws Exception {
    
    
        String msg = "Hello, 你好 Renda";

        // 1.获得连接
        Connection connection = ConnectionUtil.getConnection();
        // 2.在连接中创建通道(信道)
        Channel channel = connection.createChannel();
        // 3.创建消息队列 (1,2,3,4,5)
        /*
            参数 1: 队列的名称
            参数 2: 队列中的数据是否持久化
            参数 3: 是否排外(是否支持扩展,当前队列只能自己用,不能给别人用)
            参数 4: 是否自动删除(当队列的连接数为 0 时,队列会销毁,不管队列是否还存保存数据)
            参数 5: 队列参数(没有参数为 null)
         */
        channel.queueDeclare("queue1", false, false, false, null);
        // 4.向指定的队列发送消息 (1,2,3,4)
        /*
            参数 1: 交换机名称,当前是简单模式,也就是 P2P 模式,没有交换机,所以名称为 ""
            参数 2: 目标队列的名称
            参数 3: 设置消息的属性(没有属性则为 null)
            参数 4: 消息的内容 (只接收字节数组)
         */
        channel.basicPublish("", "queue1", null, msg.getBytes());
        System.out.println("发送:" + msg);
        // 5.释放资源
        channel.close();
        connection.close();
    }

}

启动生产者,即可前往管理端查看队列中的信息,会有一条信息没有处理。

消费者 C
public class Receiver {
    
    

    public static void main(String[] args) throws Exception {
    
    
        // 1.获得连接
        Connection connection = ConnectionUtil.getConnection();
        // 2.获得通道(信道)
        Channel channel = connection.createChannel();

        // 3.从信道中获得消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
    
    
            // 交付处理(收件人信息,包裹上的快递标签,协议的配置,消息)
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                // body 就是从队列中获取的消息
                String s = new String(body);
                System.out.println("获取消息为:" + s);
            }
        };
        // 4.监听队列 true: 自动消息确认
        channel.basicConsume("queue1", true, consumer);
    }

}

启动消费者,前往管理端查看队列中的信息,所有信息都已经处理和确认,显示 0。

消息确认机制 ACK

通过刚才的案例可以看出,消息一旦被消费,消息就会立刻从队列中移除。

如果消费者接收消息后,还没执行操作就抛异常宕机导致消费失败,但是 RabbitMQ 无从得知,这样消息就丢失了。

因此,RabbitMQ 有一个 ACK 机制,当消费者获取消息后,会向 RabbitMQ 发送回执 ACK,告知消息已经被接收。

ACK - Acknowledge character 即是确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符,表示发来的数据已确认接收无误。在使用 http 请求时,http 的状态码 200 就是表示服务器执行成功。

整个过程就像快递员将包裹送到你手里,并且需要你的签字,并拍照回执。

不过这种回执 ACK 分为两种情况:

  • 自动 ACK - 消息接收后,消费者立刻自动发送 ACK,类似快递放在快递柜。
  • 手动 ACK - 消息接收后,不会发送 ACK,需要手动调用,类似快递必须本人签收。

两种情况如何选择,需要看消息的重要性:

  • 如果消息不太重要,丢失也没有影响,自动 ACK 会比较方便。
  • 如果消息非常重要,最好消费完成手动 ACK;如果自动 ACK 消费后,RabbitMQ 就会把消息从队列中删除,而此时消费者抛异常宕机,那么消息就永久丢失了。

修改启动手动 ACK 消息确认:

// 监听队列 false: 手动消息确认
channel.basicConsume("queue1", false, consumer);

启动生产者和消费者,前往管理端查看队列中的信息,会有一条信息没有确认(Unacked)。

手动 ACK 消息确认解决问题:

public class ReceiverAck {
    
    

    public static void main(String[] args) throws Exception {
    
    
        // 1.获得连接
        Connection connection = ConnectionUtil.getConnection();
        // 2.获得通道(信道)
        final Channel channel = connection.createChannel();

        // 3.从信道中获得消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
    
    
            // 交付处理(收件人信息,包裹上的快递标签,协议的配置,消息)
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                // body就是从队列中获取的消息
                String s = new String(body);
                System.out.println("获取消息为:" + s);
                // 手动确认(收件人信息,是否同时确认多个消息)
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 4.监听队列 false: 手动消息确认
        channel.basicConsume("queue1", false, consumer);
    }

}
工作队列模式

简单模式,一个消费者来处理消息,如果生产者生产消息过快过多,而消费者的能力有限,就会产生消息在队列中堆积(生活中的滞销)。

当运行许多消费者程序时,消息队列中的任务会被众多消费者共享,但其中某一个消息只会被一个消费者获取(100 支肉串 20 个人吃,但是其中的某支肉串只能被一个人吃)。

生产者 P
public class Sender {
    
    

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare("test_work_queue",false,false,false,null);

        for(int i = 1;i<=100;i++) {
    
    
            String msg = "Message --> " + i;
            channel.basicPublish("", "test_work_queue", null, msg.getBytes());
            System.out.println(msg);
        }

        channel.close();
        connection.close();
    }

}
消费者 1
public class Receiver1 {
    
    

    // 统计获取的信息的数量
    static int counter = 1;

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();

        // queueDeclare() 此方法有双重作用,如果队列不存在,就创建;如果队列存在,则获取
        channel.queueDeclare("test_work_queue", false, false, false, null);

        DefaultConsumer consumer = new DefaultConsumer(channel) {
    
    
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                String s = new String(body);
                System.out.println("Receiver 1: " + s + ". Total Message Count: " + counter++);
                // 模拟网络延迟 200 毫秒
                try {
    
    
                    Thread.sleep(200);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                // 手动确认(收件人信息,是否同时确认多个消息)
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 4.监听队列 false:手动消息确认
        channel.basicConsume("test_work_queue", false, consumer);
    }

}
消费者 2
public class Receiver2 {
    
    

    // 统计获取的信息的数量
    static int counter = 1;

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();

        // queueDeclare() 此方法有双重作用,如果队列不存在,就创建;如果队列存在,则获取
        channel.queueDeclare("test_work_queue", false, false, false, null);

        DefaultConsumer consumer = new DefaultConsumer(channel) {
    
    
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                String s = new String(body);
                System.out.println("Receiver 2: " + s + ". Total Message Count: " + counter++);
                // 模拟网络延迟 900 毫秒
                try {
    
    
                    Thread.sleep(900);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                // 手动确认(收件人信息,是否同时确认多个消息)
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 4.监听队列 false:手动消息确认
        channel.basicConsume("test_work_queue", false, consumer);
    }

}
能者多劳

先运行 2 个消费者,排队等候消费(取餐),再运行生产者开始生产消息(烤肉串)。

由运行结果可以看到,虽然两个消费者的消费速度不一致(线程休眠时间),但是消费的数量却是一致的,各消费 50 个消息。

  • 例如:工作中,A 编码速率高,B 编码速率低,两个人同时开发一个项目,A 10 天完成,B 30 天完成,A 完成自己的编码部分,就无所事事了,等着 B 完成就可以了,这样是不可以的,应该遵循“能者多劳”。
  • 效率高的多干点,效率低的少干点。

为了克服这个问题,可以使用设置为 prefetchCount = 1basicQos 方法。这告诉RabbitMQ 一次不要给一个 worker 发送一条以上的消息。或者,换句话说,在 worker 处理并确认前一个消息之前,不要向它发送新消息。相反,它将把它分派到下一个不繁忙的 worker。

在消费者 1 和消费者 2 中加上 channel.basicQos(1)

...
// queueDeclare() 此方法有双重作用,如果队列不存在,就创建;如果队列存在,则获取
channel.queueDeclare("test_work_queue", false, false, false, null);
// 开启一次接受一条消息。可以理解为:快递一个一个送,送完一个再送下一个,速度快的送件就多
channel.basicQos(1);
...

能者多劳必须要配合手动的 ACK 机制才生效。

如何避免消息堆积?
  • Workqueue,多个消费者监听同一个队列。
  • 接收到消息后,通过线程池,异步消费。
发布/订阅模式

工作队列背后的假设是,每个任务都被准确地交付给一个工作者;“发布/订阅”模式将一个消息传递给多个消费者。

生活中的案例:众多粉丝关注一个视频主,视频主发布视频,所有粉丝都可以得到视频通知。

生产者 P 发送信息给路由 X,路由 X 将信息转发给绑定路由 X 的队列;队列将信息通过信道发送给消费者,最后消费者进行消费。整个过程,必须先创建路由。

路由在生产者程序中创建。

路由没有存储消息的能力,当生产者将信息发送给路由后,消费者还没有运行,所以没有队列,路由并不知道将信息发送给谁。

运行程序的顺序:

  • 执行一次 MessageSender,声明了路由。
  1. 执行 MessageReceiver1 和 MessageReceiver2,绑定到路由。
  2. 再次执行 MessageSender,发送消息给路由。
生产者
public class Sender {
    
    

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明路由(路由名,路由类型)
        // fanout:不处理路由键(只需要将队列绑定到路由上,发送到路由的消息都会被转发到与该路由绑定的所有队列上)
        channel.exchangeDeclare("test_exchange_fanout", "fanout");

        String msg = "Hello,Renda";
        channel.basicPublish("test_exchange_fanout", "", null, msg.getBytes());
        System.out.println("Publisher:" + msg);

        channel.close();
        connection.close();
    }

}
消费者 1
public class Receiver1 {
    
    

    private static final String RECEIVER_QUEUE = "test_exchange_fanout_queue_1";

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(RECEIVER_QUEUE, false, false, false, null);
        // 绑定路由(关注)
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_fanout", "");

        DefaultConsumer consumer = new DefaultConsumer(channel) {
    
    
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                String s = new String(body);
                System.out.println("Subscriber 1: " + s);
            }
        };

        // 4.监听队列 true: 自动消息确认
        channel.basicConsume(RECEIVER_QUEUE, true, consumer);
    }

}
消费者 2
public class Receiver2 {
    
    

    private static final String RECEIVER_QUEUE = "test_exchange_fanout_queue_2";

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(RECEIVER_QUEUE, false, false, false, null);
        // 绑定路由(关注)
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_fanout", "");

        DefaultConsumer consumer = new DefaultConsumer(channel) {
    
    
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                String s = new String(body);
                System.out.println("Subscriber 2: " + s);
            }
        };

        // 4.监听队列 true: 自动消息确认
        channel.basicConsume(RECEIVER_QUEUE, true, consumer);
    }

}
路由模式

路由会根据类型进行定向(direct)分发消息给不同的队列;每种类型可以对应多个消费者。

运行程序的顺序:

  • 先运行一次 Sender(创建路由器)。
  • 有了路由器之后,在创建两个 Receiver1 和 Receiver2,进行队列绑定。
  • 再次运行 Sender,发出消息。
生产者
public class Sender {
    
    

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明路由 (路由名,路由类型)
        // direct:根据路由键进行定向分发消息
        channel.exchangeDeclare("test_exchange_direct", "direct");

        String msg = "Register New User: userid=S101";
        channel.basicPublish("test_exchange_direct", "insert", null, msg.getBytes());
        System.out.println(msg);

        channel.close();
        connection.close();
    }

}
消费者 1
public class Receiver1 {
    
    

    private static final String RECEIVER_QUEUE = "test_exchange_direct_queue_1";

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

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

        // 绑定路由(如果路由键的类型是 添加,删除,修改 的话,绑定到这个队列 1 上)
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_direct", "insert");
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_direct", "update");
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_direct", "delete");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
    
    
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                String s = new String(body);
                System.out.println("Cosumer 1: " + s);
            }
        };

        // 4.监听队列 true: 自动消息确认
        channel.basicConsume(RECEIVER_QUEUE, true, consumer);
    }

}
消费者 2
public class Receiver2 {
    
    

    private static final String RECEIVER_QUEUE = "test_exchange_direct_queue_2";

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

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

        // 绑定路由(如果路由键的类型是 添加,删除,修改 的话,绑定到这个队列 2 上)
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_direct", "insert");
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_direct", "update");
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_direct", "delete");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
    
    
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                String s = new String(body);
                System.out.println("Cosumer 2: " + s);
            }
        };

        // 4.监听队列 true: 自动消息确认
        channel.basicConsume(RECEIVER_QUEUE, true, consumer);
    }

}
通配符模式

通配符模式是和路由模式差不多,唯独的区别就是路由键支持模糊匹配。

匹配符号:

  • * - 只能匹配一个词(正好一个词,多一个不行,少一个也不行)。
  • # - 匹配 0 个或更多个词。

案例:

Q1 绑定了路由键 `*.orange.*`      
Q2 绑定了路由键 `*.*.rabbit``lazy.#`

quick.orange.rabbit         # Q1    Q2
lazy.orange.elephant        # Q1    Q2
quick.orange.fox            # Q1
lazy.brown.fox              # Q2
lazy.pink.rabbit            # Q2
quick.brown.fox             # 无
orange                      # 无
quick.orange.male.rabbit    # 无
生产者
public class Sender {
    
    

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明路由 (路由名,路由类型)
        // topic:模糊匹配的定向分发
        channel.exchangeDeclare("test_exchange_topic", "topic");

        String msg = "price-off promotion";
        channel.basicPublish("test_exchange_topic", "product.price", null, msg.getBytes());
        System.out.println("Provider: " + msg);

        channel.close();
        connection.close();
    }

}
消费者 1
public class Receiver1 {
    
    

    private static final String RECEIVER_QUEUE = "test_exchange_topic_queue_1";

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(RECEIVER_QUEUE, false, false, false, null);
        // 绑定路由(绑定用户相关的消息)
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_topic", "user.#");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
    
    
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                String s = new String(body);
                System.out.println("Consumer 1: " + s);
            }
        };

        // 4.监听队列 true: 自动消息确认
        channel.basicConsume(RECEIVER_QUEUE, true, consumer);
    }

}
消费者 2
public class Receiver2 {
    
    

    private static final String RECEIVER_QUEUE = "test_exchange_topic_queue_2";

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(RECEIVER_QUEUE, false, false, false, null);
        // 绑定路由(绑定用户相关的消息)
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_topic", "product.#");
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_topic", "order.#");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
    
    
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                String s = new String(body);
                System.out.println("Consumer 2: " + s);
            }
        };

        // 4.监听队列 true: 自动消息确认
        channel.basicConsume(RECEIVER_QUEUE, true, consumer);
    }

}

持久化

消息的可靠性是 RabbitMQ 的一大特色,那么 RabbitMQ 是如何避免消息丢失?

消费者的 ACK 确认机制,可以防止消费者丢失消息。

万一在消费者消费之前,RabbitMQ 服务器宕机了,那消息也会丢失。

想要将消息持久化,那么路由和队列都要持久化才可以。

生产者
public class Sender {
    
    

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明路由 (路由名,路由类型,持久化)
        // topic:模糊匹配的定向分发
        channel.exchangeDeclare("test_exchange_topic", "topic", true);

        String msg = "price-off promotion";
        // 信道持久化
        channel.basicPublish("test_exchange_topic", "product.price", MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
        System.out.println("Provider: " + msg);

        channel.close();
        connection.close();
    }

}
消费者
public class Receiver1 {
    
    

    private static final String RECEIVER_QUEUE = "test_exchange_topic_queue_1";

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列 (第二个参数为 true:支持持久化)
        channel.queueDeclare(RECEIVER_QUEUE, true, false, false, null);
        // 绑定路由(绑定用户相关的消息)
        channel.queueBind(RECEIVER_QUEUE, "test_exchange_topic", "user.#");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
    
    
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                String s = new String(body);
                System.out.println("Consumer 1: " + s);
            }
        };

        // 4.监听队列 true: 自动消息确认
        channel.basicConsume(RECEIVER_QUEUE, true, consumer);
    }

}

Spring 整合 RabbitMQ

五种消息模型,在企业中应用最广泛的就是定向匹配 topics。

Spring AMQP 是基于 Spring 框架的 AMQP 消息解决方案,提供模板化的发送和接收消息的抽象层,提供基于消息驱动的 POJO 的消息监听等,简化了对于 RabbitMQ 相关程序的开发。

生产端工程

依赖 pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.amqp</groupId>
        <artifactId>spring-rabbit</artifactId>
        <version>2.0.1.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.25</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.9</version>
    </dependency>
</dependencies>

spring-rabbitmq-producer.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/rabbit
       http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

    <!-- 1.配置连接 -->
    <rabbit:connection-factory id="connectionFactory"
                               host="192.168.186.128"
                               port="5672"
                               username="renda"
                               password="123456"
                               virtual-host="/renda"
                               publisher-confirms="true"
    />

    <!-- 2.配置队列 -->
    <rabbit:queue name="test_spring_queue_1"/>

    <!-- 3.配置 rabbitAdmin: 主要用于在 java 代码中对队列的管理,用来创建,绑定,删除队列与交换机,发送消息等 -->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!-- 4.配置交换机,topic 类型 -->
    <rabbit:topic-exchange name="spring_topic_exchange">
        <rabbit:bindings>
            <!-- 绑定队列 -->
            <rabbit:binding pattern="msg.#" queue="test_spring_queue_1"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <!-- 5.配置 json 转换的工具 -->
    <bean id="jsonMessageConverter" class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter"/>

    <!-- 6.配置 rabbitmq 的模版 -->
    <rabbit:template id="rabbitTemplate"
                     connection-factory="connectionFactory"
                     exchange="spring_topic_exchange"
                     message-converter="jsonMessageConverter"/>

</beans>

发消息 com.renda.test.Sender

public class Sender {
    
    

    public static void main(String[] args) {
    
    
        // 1.创建 spring 容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer.xml");

        // 2.从 spring 容器中获得 rabbit 模版对象
        RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);

        // 3.发消息
        Map<String, String> map = new HashMap<String, String>();
        map.put("name", "张人大");
        map.put("email", "[email protected]");
        rabbitTemplate.convertAndSend("msg.user", map);
        System.out.println("Message Sent...");

        context.close();
    }

}
消费端工程

依赖与生产者一致

spring-rabbitmq-consumer.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/rabbit
       http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!--1.配置连接-->
    <rabbit:connection-factory
            id="connectionFactory"
            host="192.168.186.128"
            port="5672"
            username="renda"
            password="123456"
            virtual-host="/renda"/>

    <!-- 2.配置队列 -->
    <rabbit:queue name="test_spring_queue_1"/>

    <!-- 3.配置 rabbitAdmin: 主要用于在 java 代码中对队列的管理,用来创建,绑定,删除队列与交换机,发送消息等 -->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!-- 4.注解扫描包 springIOC -->
    <context:component-scan base-package="com.renda.listener"/>

    <!-- 5.配置监听 -->
    <rabbit:listener-container connection-factory="connectionFactory">
        <rabbit:listener ref="consumerListener" queue-names="test_spring_queue_1"/>
    </rabbit:listener-container>

</beans>

消费者:

MessageListener 接口用于 spring 容器接收到消息后处理消息;

如果需要使用自己定义的类型来实现处理消息时,必须实现该接口,并重写 onMessage() 方法;

当 spring 容器接收消息后,会自动交由 onMessage 进行处理。

com.renda.listener.ConsumerListener

@Component
public class ConsumerListener implements MessageListener {
    
    

    /**
     * jackson 提供序列化和反序列中使用最多的类,用来转换 json 的
     */
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void onMessage(Message message) {
    
    
        // 将 message对象转换成 json
        JsonNode jsonNode = null;
        try {
    
    
            jsonNode = MAPPER.readTree(message.getBody());
            String name = jsonNode.get("name").asText();
            String email = jsonNode.get("email").asText();
            System.out.println("Message From Queue:{" + name + ", " + email + "}");
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

}

启动项目 com.renda.test.TestRunner

public class TestRunner {
    
    

    public static void main(String[] args) throws IOException {
    
    
        // 获得容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-consumer.xml");
        // 让程序一直运行,别终止
        System.in.read();
    }

}

消息成功确认机制

在实际场景下,有的生产者发送的消息是必须保证成功发送到消息队列中,需要事务机制和发布确认机制。

事务机制

AMQP 协议提供的一种保证消息成功投递的方式,通过信道开启 transactional 模式;

利用信道的三个方法来实现以事务方式发送消息,若发送失败,通过异常处理回滚事务,确保消息成功投递

  • channel.txSelect() - 开启事务

  • channel.txCommit() - 提交事务

  • channel.txRollback() - 回滚事务

Spring 已经对上面三个方法进行了封装,所以这里使用原始的代码演示。

生产者
public class Sender {
    
    

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare("test_transaction", "topic");
        // 开启事务
        channel.txSelect();
        try {
    
    
            channel.basicPublish("test_transaction", "product.price", null, "Item 1: price-off".getBytes());
            // 模拟出错
            // System.out.println(1 / 0);
            channel.basicPublish("test_transaction", "product.price", null, "Item 2: price-off".getBytes());
            // 提交事务(一起成功)
            channel.txCommit();
            System.out.println("Producer: All Messages Sent");
        } catch (Exception e) {
    
    
            System.out.println("All Messages Rollback");
            // 事务回滚(一起失败)
            channel.txRollback();
            e.printStackTrace();
        } finally {
    
    
            channel.close();
            connection.close();
        }
    }

}
消费者
public class Receiver {
    
    

    public static void main(String[] args) throws Exception {
    
    
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare("test_transaction_queue", false, false, false, null);
        channel.queueBind("test_transaction_queue", "test_transaction", "product.#");

        DefaultConsumer consumer = new DefaultConsumer(channel) {
    
    
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                String s = new String(body);
                System.out.println("Consumer: " + s);
            }
        };

        // 4.监听队列 true:自动消息确认
        channel.basicConsume("test_transaction_queue", true, consumer);
    }

}
Confirm 发布确认机制

RabbitMQ 为了保证消息的成功投递,采用通过 AMQP 协议层面提供事务机制的方案,但是采用事务会大大降低消息的吞吐量。

开启事务性能最大损失超过 250 倍。

事务效率低下原因:100 条消息,前 99 条成功,如果第 100 条失败,那么 99 条消息要全部撤销回滚。

更加高效的解决方式是采用 Confirm 模式,而 Confirm 模式则采用补发第 100 条的措施来完成 100 条消息的送达。

在 Spring 中应用

resources\spring\spring-rabbitmq-producer.xml

...
<!-- 6.配置 rabbitmq 的模版 -->
<rabbit:template id="rabbitTemplate"
                 connection-factory="connectionFactory"
                 exchange="spring_topic_exchange"
                 message-converter="jsonMessageConverter"
                 confirm-callback="messageConfirm"/>

<!-- 7.确认机制的处理类 -->
<bean id="messageConfirm" class="com.renda.confirm.MessageConfirm"/>
...

消息确认处理类 com.renda.confirm.MessageConfirm

public class MessageConfirm implements RabbitTemplate.ConfirmCallback {
    
    

    /**
     * @param correlationData 消息相关的数据对象(封装了消息的唯一 id)
     * @param b               消息是否确认成功
     * @param s               异常信息
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {
    
    
        if (b) {
    
    
            System.out.println("Successfully Confirmed Message");
        } else {
    
    
            System.out.println("Fail to Confirm Message, error: " + s);
            // 如果本条消息一定要发送到队列中,例如下订单消息,可以采用补发
            // 1.采用递归(限制递归的次数)
            // 2.redis + 定时任务(jdk 的 timer,或者定时任务框架 Quartz)
        }
    }
}

resources\log4j.properties

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %m%n

log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=rabbitmq.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l  %m%n

log4j.rootLogger=debug, stdout,file

发送消息 com.renda.test.Sender

...
// 3.发消息
Map<String, String> map = new HashMap<String, String>();
map.put("name", "张人大");
map.put("email", "[email protected]");
// 模拟发送消息失败
// rabbitTemplate.convertAndSend("fuck", "msg.user", map);
rabbitTemplate.convertAndSend("msg.user", map);
System.out.println("Message Sent...");
...

消费端限流

RabbitMQ 服务器积压了成千上万条未处理的消息,然后随便打开一个消费者客户端,就会出现这样的情况:巨量的消息瞬间全部喷涌推送过来,但是单个客户端无法同时处理这么多数据,就会被压垮崩溃。

所以,当数据量特别大的时候,对生产端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,这是用户的行为 - 是无法约束的。

应该对消费端限流,用于保持消费端的稳定。

RabbitMQ 提供了一种 QoS(Quality of Service,服务质量)服务质量保证功能;

即在非自动确认消息的前提下,如果一定数目的消息未被确认前,不再进行消费新的消息。

生产者 com.renda.test.Sender 使用循环发出多条消息:

...
for (int i = 0; i < 10; i++) {
    
    
    rabbitTemplate.convertAndSend("msg.user", map);
    System.out.println("Message Sent...");
}
...

RabbitMQ 的管理页面可以看到生产了 10 条堆积未处理的消息。

消费者进行限流处理:

resources\spring\spring-rabbitmq-consumer.xml

...
5.配置监听 -->
<!--
        prefetch="3":一次性消费的消息数量。
        会告诉 RabbitMQ 不要同时给一个消费者推送多于 N 个消息,
        一旦有 N 个消息还没有 ack,则该 consumer 将阻塞,直到消息被 ack。
    -->
<!-- acknowledge-mode: manual 手动确认-->
<rabbit:listener-container connection-factory="connectionFactory" prefetch="3" acknowledge="manual">
    <rabbit:listener ref="consumerListener" queue-names="test_spring_queue_1"/>
</rabbit:listener-container>
...

com.renda.listener.ConsumerListener

@Component
public class ConsumerListener extends AbstractAdaptableMessageListener {
    
    

    /**
     * jackson 提供序列化和反序列中使用最多的类,用来转换 json 的
     */
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
    
    
        // 将 message对象转换成 json
//        JsonNode jsonNode = MAPPER.readTree(message.getBody());
//        String name = jsonNode.get("name").asText();
//        String email = jsonNode.get("email").asText();
//        System.out.println("Message From Queue:{" + name + ", " + email + "}");

        String str = new String(message.getBody());
        System.out.println("str = " + str);

        /**
         * 手动确认消息(参数1,参数2)
         * 参数 1:RabbitMQ 想该 channel 投递的这条消息的唯一标识 ID,此 ID 是一个单调递增的正整数。
         * 参数 2:为了减少网络流量,手动确认可以被批量处理;当该参数为 true 时,则可以一次性确认小于等于 msgId 值的所有消息。
         */
        long msgId = message.getMessageProperties().getDeliveryTag();
        channel.basicAck(msgId, true);

        Thread.sleep(3000);
        System.out.println("Rest for 3 seconds and then continue for more messages...");
    }
}

每次最多只确认接收 3 条消息,直到消息队列为空。

过期时间 TTL

Time To Live - 生存时间、还能活多久,单位毫秒。

在这个周期内,消息可以被消费者正常消费,超过这个时间,则自动删除(其实是被称为 dead message 并投入到死信队列,无法消费该消息)。

RabbitMQ 可以对消息和队列设置 TTL:

  • 通过队列设置,队列中所有消息都有相同的过期时间。
  • 对消息单独设置,每条消息的 TTL 可以不同(更颗粒化)。
设置队列 TTL

RabbitMQ 管理端删除掉 test_spring_queue_1 队列。

resources\spring\spring-rabbitmq-producer.xml

<!-- 对队列中的消息设置过期时间 -->
<rabbit:queue name="test_spring_queue_1" auto-declare="true">
    <rabbit:queue-arguments>
        <entry key="x-message-ttl" value-type="long" value="5000"/>
    </rabbit:queue-arguments>
</rabbit:queue>

5 秒之后,消息自动删除。

设置消息 TTL

RabbitMQ 管理端删除掉 test_spring_queue_1 队列。

设置某条消息的 TTL,只需要在创建发送消息时指定即可。

resources\spring\spring-rabbitmq-producer.xml

<rabbit:queue name="test_spring_queue_1"/>

com.renda.test.Sender2

public class Sender2 {
    
    

    public static void main(String[] args) {
    
    
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer.xml");
        RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);

        // 创建消息配置对象
        MessageProperties messageProperties = new MessageProperties();
        // 设置消息过期时间
        messageProperties.setExpiration("6000");
        // 创建消息
        Message message = new Message("This Message will be deleted in 6000 ms".getBytes(), messageProperties);
        // 发消息
        rabbitTemplate.convertAndSend("msg.user", message);
        System.out.println("Message Sent...");

        context.close();
    }

}

如果同时设置了 queue 和 message 的 TTL 值,则只有二者中较小的才会起作用。

死信队列

DLX(Dead Letter Exchanges)死信交换机 / 死信邮箱,当消息在队列中由于某些原因没有被及时消费而变成死信(dead message)后,这些消息就会被分发到 DLX 交换机中,而绑定 DLX 交换机的队列,称之为:“死信队列”。

消息没有被及时消费的原因:

  • 消息被拒绝(basic.reject / basic.nack)并且不再重新投递 requeue=false
  • 消息超时未消费。
  • 达到最大队列长度。
my_exchange 交换机 --- 没有及时消费的消息 ---> dlx_exchange 死信交换机

my_exchange -- 路由键 dlx_ttl --> test_ttl_queue 消息过期
my_exchange -- 路由键 dlx_max --> test_max_queue 达到最大队列长度

没有及时消费的消息:[test_ttl_queue, test_max_queue]

test_ttl_queue -- 过期的消息 --> dlx_exchange
test_max_queue -- 被挤出的消息 --> dlx_exchange

dlx_exchange -- 路由键 dlx_ttl --> dlx_queue 死信队列
dlx_exchange -- 路由键 dlx_max --> dlx_queue 死信队列

resources\spring\spring-rabbitmq-producer-dlx.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/rabbit
       http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

    <!-- 配置连接 -->
    <rabbit:connection-factory id="connectionFactory"
                               host="192.168.186.128"
                               port="5672"
                               username="renda"
                               password="123456"
                               virtual-host="/renda"
                               publisher-confirms="true"
    />

    <!-- 配置 rabbitAdmin: 主要用于在 java 代码中对队列的管理,用来创建,绑定,删除队列与交换机,发送消息等 -->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!-- 配置 rabbitmq 的模版 -->
    <rabbit:template id="rabbitTemplate"
                     connection-factory="connectionFactory"
                     exchange="spring_topic_exchange"/>

    <!-- 声明死信队列 -->
    <rabbit:queue name="dlx_queue"/>

    <!-- 声明定向的死信交换机 -->
    <rabbit:direct-exchange name="dlx_exchange">
        <rabbit:bindings>
            <rabbit:binding key="dlx_ttl" queue="dlx_queue"/>
            <rabbit:binding key="dlx_max" queue="dlx_queue"/>
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <!-- 声明测试过期的消息队列 -->
    <rabbit:queue name="test_ttl_queue">
        <rabbit:queue-arguments>
            <!-- 设置队列的过期时间 TTL -->
            <entry key="x-message-ttl" value-type="long" value="10000"/>
            <!-- 消息如果超时,将消息投递给死信交换机 -->
            <entry key="x-dead-letter-exchange" value="dlx_exchange"/>
        </rabbit:queue-arguments>
    </rabbit:queue>

    <!-- 声明测试超出长度的消息队列 -->
    <rabbit:queue name="test_max_queue">
        <rabbit:queue-arguments>
            <!-- 设置队列的额定长度 (本队列最多装 2 个消息) -->
            <entry key="x-max-length" value-type="long" value="2"/>
            <!-- 消息如果超出长度,将消息投递给死信交换机 -->
            <entry key="x-dead-letter-exchange" value="dlx_exchange"/>
        </rabbit:queue-arguments>
    </rabbit:queue>

    <!-- 声明定向的测试消息的交换机 -->
    <rabbit:direct-exchange name="my_exchange">
        <rabbit:bindings>
            <rabbit:binding key="dlx_ttl" queue="test_ttl_queue"/>
            <rabbit:binding key="dlx_max" queue="test_max_queue"/>
        </rabbit:bindings>
    </rabbit:direct-exchange>

</beans>

发消息进行测试

public class SendDLX {
    
    

    public static void main(String[] args) {
    
    
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-producer-dlx.xml");
        RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);
        
        // 测试超时
        // rabbitTemplate.convertAndSend("dlx_ttl", "Overtime: Close".getBytes());

        // 测试超过最大长度
        rabbitTemplate.convertAndSend("dlx_max", "OverSize: 1".getBytes());
        rabbitTemplate.convertAndSend("dlx_max", "OverSize: 2".getBytes());
        rabbitTemplate.convertAndSend("dlx_max", "OverSize: 3".getBytes());

        System.out.println("Message Sent...");

        context.close();
    }

}

延迟队列

延迟队列 = TTL + 死信队列的合体。

死信队列只是一种特殊的队列,里面的消息仍然可以消费。

在电商开发部分中,都会涉及到延时关闭订单,此时延迟队列正好可以解决这个问题。

生产者

沿用上面死信队列案例的超时测试,超时时间改为订单关闭时间即可。

消费者

resources\spring\spring-rabbitmq-consumer.xml

...
<!-- 监听死信队列-->
<rabbit:listener-container connection-factory="connectionFactory" prefetch="3" acknowledge="manual">
    <rabbit:listener ref="consumerListener" queue-names="dlx_queue"/>
</rabbit:listener-container>
...

RabbitMQ 集群

RabbitMQ 有 3 种模式,其中 2 种是集群模式。

单一模式:即单机情况不做集群,就单独运行一个 RabbitMQ 而已。

普通模式:默认模式,以两个节点(A、B)为例来进行说明:

  • 当消息进入 A 节点的 Queue 后,Consumer 从 B 节点消费时,RabbitMQ 会在 A 和 B 之间创建临时通道进行消息传输,把 A 中的消息实体取出并经过通过交给 B 发送给 Consumer。
  • 当 A 故障后,B 就无法取到 A 节点中未消费的消息实体;如果做了消息持久化,那么得等 A 节点恢复,然后才可被消费;如果没有持久化的话,就会产生消息丢失的现象。

镜像模式 - 经典的 Mirror 镜像模式,保证数据不丢失:

  • 高可靠性解决方案,主要就是实现数据的同步,一般来讲是 2 - 3 个节点实现数据同步。
  • 对于 100% 数据可靠性解决方案,一般是采用 3 个节点。
  • 在实际工作中也是用得最多的,并且实现非常的简单,一般互联网大厂都会构建这种镜像集群模式。

另外,还有主备模式,远程模式,多活模式等等。

集群搭建

前置条件:准备两台 linux(192.168.186.128 和 192.168.186.129),并安装好 RabbitMQ。

  1. 修改映射文件 vim /etc/hosts

1 号服务器:

127.0.0.1 A   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1       A   localhost localhost.localdomain localhost6 localhost6.localdomain6

192.168.186.128 A
192.168.186.129 B

2 号服务器:

127.0.0.1 A   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1       A   localhost localhost.localdomain localhost6 localhost6.localdomain6

192.168.186.128 A
192.168.186.129 B

修改完 hosts 文件后,需要重启 Linux 服务器 reboot,否则配置不生效。

  1. 相互通信,cookie 必须保持一致,同步 RabbitMQ 的 cookie 文件:跨服务器拷贝 .erlang.cookie(隐藏文件,使用 ls -all 显示)。
scp /var/lib/rabbitmq/.erlang.cookie 192.168.186.129:/var/lib/rabbitmq/

修改 cookie 文件,要重启 linux 服务器 reboot

  1. 防火墙开放 epmd 端口 4369,启动 RabbitMQ 服务。
firewall-cmd --zone=public --add-port=4369/tcp --permanent
firewall-cmd --reload
systemctl start rabbitmq-server
  1. 加入集群节点,节点 A 加入 节点 B,或者节点 B 加入节点 A 都可以:
[root@A ~]# rabbitmqctl stop_app
Stopping rabbit application on node rabbit@A ...
[root@A ~]# rabbitmqctl join_cluster rabbit@B
Clustering node rabbit@A with rabbit@B
[root@A ~]# rabbitmqctl start_app
Starting node rabbit@A ...
  1. 查看节点状态:
rabbitmqctl cluster_status
  1. 查看管理端

搭建集群结构之后,之前创建的交换机、队列、用户都属于单一结构,在新的集群环境中是不能用的。

所以在新的集群中重新手动添加用户即可(任意节点添加,所有节点共享)。

[root@A ~]# rabbitmqctl add_user renda 123456
Adding user "renda" ...
[root@A ~]# rabbitmqctl set_user_tags renda administrator
Setting tags for user "renda" to [adminstrator] ...
[root@A ~]# rabbitmqctl set_permissions -p "/" renda ".*" ".*" ".*"
Setting permissions for user "renda" in vhost "/" ...
[root@A ~]# rabbitmqctl list_users
Listing users ...
user	tags
renda	[administrator]
guest	[administrator]

访问 http://192.168.186.128:15672 和 http://192.168.186.129:15672,两个节点共享用户。

注意:当节点脱离集群还原成单一结构后,交换机,队列和用户等数据都会重新回来。

此时,RabbitMQ 的集群搭建完毕,但是默认采用的模式为“普通模式”,可靠性不高。

镜像模式

将所有队列设置为镜像队列,即队列会被复制到各个节点,各个节点状态一致。

语法:set_policy {NAME} {PATTERN} {DEFINITION}

NAME - 策略名,可自定义

PATTERN - 队列的匹配模式(正则表达式)

  • ^ 可以使用正则表达式,比如 ^queue_ 表示对队列名称以 queue_ 开头的所有队列进行镜像,而 ^ 会匹配所有的队列。

DEFINITION - 镜像定义,包括三个部分 ha-mode, ha-params, ha-sync-mode

  • ha-mode - high available 高可用模式,指镜像队列的模式,有效值为 all/exactly/nodes;当前策略模式为 all,即复制到所有节点,包含新增节点。all 表示在集群中所有的节点上进行镜像;exactly 表示在指定个数的节点上进行镜像,节点的个数由 ha-params 指定;nodes 表示在指定的节点上进行镜像,节点名称通过 ha-params 指定。
  • ha-params - ha-mode 模式需要用到的参数。
  • ha-sync-mode - 进行队列中消息的同步方式,有效值为 automatic 和 manual。
[root@A ~]# rabbitmqctl set_policy policy_renda "^" '{"ha-mode":"all"}'
Setting policy "policy_renda" for pattern "^" to "{"ha-mode":"all"}" with priority "0" for vhost "/" ...

通过管理端 Admin -> Policies -> Add / update a policy 设置镜像策略。

设置好镜像模式后,在节点 A 增加了队列后,节点 B 也可以看到新增的队列。

在 RabbitMQ 管理界面 Admin -> Virtual Hosts -> Add a new virtual host 创建虚拟主机 /renda

使用 Spring 整合的 RabbitMQ 重新测试发送和接受消息;在其中一个节点使用命令 rabbitmqctl stop_app 停掉,再测试,仍然可以发送和接受消息。

HAProxy 实现镜像队列的负载均衡

虽然在程序中访问 A 服务器,可以实现消息的同步,但都是 A 服务器在接收消息,A 太累;是否可以负载均衡,A 和 B 轮流接收消息,再镜像同步。

HAProxy 简介

HA - High Available 高可用,Proxy - 代理。

HAProxy 是一款提供高可用性,负载均衡,并且基于 TCP 和 HTTP 应用的代理软件。

HAProxy 完全免费。

HAProxy 可以支持数以万计的并发连接。

HAProxy 可以简单又安全的整合进架构中,同时还保护 Web 服务器不被暴露到网络上。

生产者 -- 投递消息 --> HAProxy
消费者 -- 订阅消息 --> HAProxy

HAProxy ---> [MQ Node 1, MQ Node 2, MQ Node 3]
HAProxy 与 Nginx

OSI - Open System Interconnection 开放式系统互联,是把网络通信的工作分为 7 层,分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

Nginx 的优点:

  • 工作在 OSI 第 7 层,可以针对 http 应用做一些分流的策略。
  • Nginx 对网络的依赖非常小,理论上能 ping 通就就能进行负载功能,屹立至今的绝对优势。
  • Nginx 安装和配置比较简单,测试起来比较方便。
  • Nginx 不仅仅是一款优秀的负载均衡器 / 反向代理软件,它同时也是功能强大的 Web 应用服务器。

HAProxy 的优点:

  • 工作在网络 4 层和 7 层,支持 TCP 与 Http 协议。
  • 它仅仅就只是一款负载均衡软件;单纯从效率上来讲 HAProxy 更会比 Nginx 有更出色的负载均衡速度,在并发处理上也是优于 Nginx 的。
  • 支持 8 种负载均衡策略 ,支持心跳检测。

性能上 HAProxy 胜,但是功能性和便利性上 Nginx 胜。

对于 Http 协议,HAProxy 处理效率比 Nginx 高;所以,没有特殊要求的时候或者一般场景,建议使用 Haproxy 来做 Http 协议负载;如果是 Web 应用,建议使用 Nginx。

需要结合使用场景的特点来进行合理地选择。

安装和配置

HAProxy 下载:http://www.haproxy.org/download/1.8/src/haproxy-1.8.12.tar.gz

上传到第三台 Linux 服务器(192.168.186.130)中并解压:

tar -zxvf haproxy-1.8.12.tar.gz

make 时需要使用 TARGET 指定内核及版本:

[root@localhost haproxy-1.8.12]# uname -r
3.10.0-229.el7.x86_64

查看目录下的 README 文件 less /opt/haproxy-1.8.12/README 可知需要根据内核版本选择编译参数:

...
To build haproxy, you have to choose your target OS amongst the following ones
and assign it to the TARGET variable :

  - linux22     for Linux 2.2
  - linux24     for Linux 2.4 and above (default)
  - linux24e    for Linux 2.4 with support for a working epoll (> 0.21)
  - linux26     for Linux 2.6 and above
  - linux2628   for Linux 2.6.28, 3.x, and above (enables splice and tproxy)
  - solaris     for Solaris 8 or 10 (others untested)
  - freebsd     for FreeBSD 5 to 10 (others untested)
  - netbsd      for NetBSD
  - osx         for Mac OS/X
  - openbsd     for OpenBSD 5.7 and above
  - aix51       for AIX 5.1
...

进入目录,编译和安装:

cd /opt/haproxy-1.8.12/
make TARGET=linux2628 PREFIX=/usr/local/haproxy
make install PREFIX=/usr/local/haproxy

安装成功后,查看版本:

[root@localhost haproxy-1.8.12]# /usr/local/haproxy/sbin/haproxy -v
HA-Proxy version 1.8.12-8a200c7 2018/06/27
Copyright 2000-2018 Willy Tarreau <[email protected]>

配置启动文件,复制 haproxy 文件到 /usr/sbin 目录下 ,复制 haproxy 脚本,到 /etc/init.d 目录下:

cp /usr/local/haproxy/sbin/haproxy /usr/sbin/
cp /opt/haproxy-1.8.12/examples/haproxy.init /etc/init.d/haproxy
chmod 755 /etc/init.d/haproxy

创建系统账号:

useradd -r haproxy

haproxy.cfg 配置文件需要自行创建:

mkdir /etc/haproxy
vim /etc/haproxy/haproxy.cfg

添加配置信息到 haproxy.cfg:

# 全局配置
global
    # 设置日志
    log 127.0.0.1 local0 info
    # 当前工作目录
    chroot /usr/local/haproxy
    # 用户与用户组
    user haproxy
    group haproxy
    # 运行进程 ID
    uid 99
    gid 99
    # 守护进程启动
    daemon
    # 最大连接数
    maxconn 4096

# 默认配置
defaults
    # 应用全局的日志配置
    log global
    # 默认的模式 mode {tcp|http|health},TCP 是 4 层,HTTP 是 7 层,health 只返回 OK
    mode tcp
    # 日志类别 tcplog
    option tcplog
    # 不记录健康检查日志信息
    option dontlognull
    # 3 次失败则认为服务不可用
    retries 3
    # 每个进程可用的最大连接数
    maxconn 2000
    # 连接超时
    timeout connect 5s
    # 客户端超时 30 秒,ha 就会发起重新连接
    timeout client 30s
    # 服务端超时 15 秒,ha 就会发起重新连接
    timeout server 15s

# 绑定配置
listen rabbitmq_cluster
    bind 192.168.186.130:5672
    # 配置 TCP 模式
    mode tcp
    # 简单的轮询
    balance roundrobin
    # RabbitMQ 集群节点配置,每隔 5 秒对 mq 集群做检查,2 次正确证明服务可用,3 次失败证明服务不可用
    server A 192.168.186.128:5672 check inter 5000 rise 2 fall 3
    server B 192.168.186.129:5672 check inter 5000 rise 2 fall 3

# haproxy 监控页面地址
listen monitor
    bind 192.168.186.130:8100
    mode http
    option httplog
    stats enable
    # 监控页面地址 http://192.168.186.130:8100/monitor
    stats uri /monitor
    stats refresh 5s

启动 HAProxy:

service haproxy start

开放对应的防火墙端口:

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

访问监控中心:http://192.168.186.130:8100/monitor

项目发消息,只需要将服务器地址修改为 192.168.186.130 即可,其余不变。

这样,所有的请求都会交给 HAProxy,然后它会负载均衡地发给每个 RabbitMQ 服务器。

KeepAlived 搭建高可用的 HAProxy 集群

如果 HAProxy 服务器宕机,RabbitMQ 服务器就不可用了,所以对 HAProxy 也要做高可用的集群。

概述

Keepalived 是 Linux 的轻量级别的高可用热备解决方案。

Keepalived 的作用是检测服务器的状态,它根据 TCP / IP 参考模型的第三层、第四层、第五层交换机制检测每个服务节点的状态,如果有一台 web 服务器宕机,或工作出现故障,Keepalived 将检测到,并将有故障的服务器从系统中剔除,同时使用其他服务器代替该服务器的工作,当服务器工作正常后 Keepalived 自动将服务器加入到服务器群中,这些工作全部自动完成,不需要人工干涉,需要人工做的只是修复故障的服务器。

Keepalived 基于 VRRP - Virtual Router Redundancy Protocol 虚拟路由冗余协议协议;VRRP 是一种主备(主机和备用机)模式的协议,通过 VRRP 可以在网络发生故障时透明的进行设备切换而不影响主机之间的数据通信。

两台主机之间生成一个虚拟的 ip,称为漂移 ip,漂移 ip 由主服务器承担,一但主服务器宕机,备份服务器就会抢夺漂移 ip,继续工作,有效的解决了群集中的单点故障。

KeepAlived 将多台路由器设备虚拟成一个设备,对外提供统一 ip(Virtual IP)。

生产者 -- 投递消息 --> KeepAlived
消费者 -- 订阅消息 --> KeepAlived

HAProxy 1 --> 主机 1 --> KeepAlived 虚拟 IP 
HAProxy 2 --> 主机 2 --> KeepAlived 虚拟 IP
安装 KeepAlived

修改映射文件 vim /etc/hosts

3 号服务器 192.168.186.130:

127.0.0.1 C   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1       C   localhost localhost.localdomain localhost6 localhost6.localdomain6

192.168.186.128 A
192.168.186.129 B
192.168.186.130 C
192.168.186.131 D

4 号服务器 192.168.186.131:

127.0.0.1 D   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1       D   localhost localhost.localdomain localhost6 localhost6.localdomain6

192.168.186.128 A
192.168.186.129 B
192.168.186.130 C
192.168.186.131 D

修改完 hosts 文件后,需要重启 Linux 服务器 reboot,否则配置不生效。

重新启动后,需要启动 haproxy:

service haproxy start

主机 C 和主机 D 都安装 keepalived:

yum install -y keepalived

主机 C 修改配置文件(删掉内容,重新创建):

rm -rf /etc/keepalived/keepalived.conf
vim /etc/keepalived/keepalived.conf
! Configuration File for keepalived

global_defs {
    
    
    # 非常重要,标识本机的 hostname
    router_id C
}

vrrp_script chk_haproxy {
    
    
    # 执行的脚本位置
    script "/etc/keepalived/haproxy_check.sh"
    # 检测时间间隔
    interval 2
    # 如果条件成立则权重减 20
    weight -20
}

vrrp_instance VI_1 {
    
    
    # 非常重要,标识主机,备用机 131 改为 BACKUP
    state MASTER
    # 非常重要,网卡名(ifconfig 查看)
    interface ens33
    # 非常重要,自定义,虚拟路由 ID 号(主备节点要相同)
    virtual_router_id 66
    # 优先级(0-254),一般主机的大于备机
    priority 100
    # 主备信息发送间隔,两个节点必须一致,默认 1 秒
    advert_int 1
    # 认证匹配,设置认证类型和密码,MASTER 和 BACKUP 必须使用相同的密码才能正常通信
    authentication {
    
    
        auth_type PASS
        auth_pass 1111
    }
    track_script {
    
    
        # 检查 haproxy 健康状况的脚本
        chk_haproxy
    }
    # 简称 “VIP”
    virtual_ipaddress {
    
    
        # 非常重要,虚拟 ip,可以指定多个,以后连接 mq 就用这个虚拟ip
        192.168.186.66/24
    }
}
# 虚拟 ip 的详细配置
virtual_server 192.168.186.66 5672 {
    
    
    # 健康检查间隔,单位为秒
    delay_loop 6
    # lvs 调度算法 rr|wrr|lc|wlc|lblc|sh|dh
    lb_algo rr
    # 负载均衡转发规则。一般包括 DR, NAT, TUN 3 种
    lb_kind NAT
    # 转发协议,有 TCP 和 UDP 两种,一般用 TCP
    protocol TCP
        # 本机的真实 ip
        real_server 192.168.186.130 5672 {
    
    
        # 默认为 1, 失效为 0
        weight 1
    }
}

主机 C 创建执行脚本 vim /etc/keepalived/haproxy_check.sh

#!/bin/bash
COUNT=`ps -C haproxy --no-header |wc -l`
if [ $COUNT -eq 0 ];then
    /usr/local/haproxy/sbin/haproxy -f /etc/haproxy/haproxy.cfg
    sleep 2
    if [ `ps -C haproxy --no-header |wc -l` -eq 0 ];then
        killall keepalived
    fi
fi

Keepalived 组之间的心跳检查并不能察觉到 HAproxy 负载是否正常,所以需要使用此脚本。在 Keepalived 主机上,开启此脚本检测 HAproxy 是否正常工作,如正常工作,记录日志。如进程不存在,则尝试重启 HAproxy ,2 秒后检测,如果还没有,则关掉主机的 Keepalived ,此时备 Keepalived 检测到主 Keepalived 挂掉,接管 VIP,继续服务。

主机 C 给脚本文件增加执行权限:

chmod +x /etc/keepalived/haproxy_check.sh

此时,安装完毕,按照上面的步骤就可以安装第二台主机 D 了(服务器 hostname 和 ip 注意要修改)。

service keepalived start | stop | status | restart

启动 keepalived(两台都启动):

service keepalived start

查看状态:

ps -ef | grep haproxy
ps -ef | grep keepalived

查看 ip 情况 ip addrip a

启动 keepalived 前的情况:

[root@C keepalived]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:0c:29:ac:93:50 brd ff:ff:ff:ff:ff:ff
    inet 192.168.186.130/24 brd 192.168.186.255 scope global ens33
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:feac:9350/64 scope link 
       valid_lft forever preferred_lft forever

启动 keepalived 后的情况:

[root@C keepalived]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:0c:29:ac:93:50 brd ff:ff:ff:ff:ff:ff
    inet 192.168.186.130/24 brd 192.168.186.255 scope global ens33
       valid_lft forever preferred_lft forever
    inet 192.168.186.66/24 scope global secondary ens33
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:feac:9350/64 scope link 
       valid_lft forever preferred_lft forever

可以看到 ens33 网卡还多绑定了一个 IP 地址。

常见的网络错误:子网掩码、网关等信息要一致。

测试 vip 和端口一起是否能提供服务

在 192.168.186.128,A 服务器上测试。

在服务器 A 执行 curl 192.168.186.130:5672curl 192.168.186.66:5672 都能正常返回 AMPQ,说明安装成功。

测试 ip 漂移的规则

使用 ip addrip a 查看虚拟 ip。

刚开始时,C 和 D 都启动了 KeepAlived;C 是主机,所以虚拟 ip 在主机 C,表现为主机 C 显示 inet 192.168.186.66/24,而备机 D 不显示。

然后,停止主机 C 的 keepalived service keepalived stop,虚拟 ip 漂移到 D 节点,D 节点执行 ip a 可以看到 inet 192.168.186.66/24,而主机 C 却不显示。

接着,重新启动 C 节点的 Keepalived,虚拟 ip 依旧在 D 节点,并不会由于 C 的回归而回归。

最后,停止 D 的 Keepalived,虚拟 ip 再漂移回 C 节点。

测试项目发消息
消费者或生产者 -- 漂移 IP 66 --> KeepAlived 服务 --> [HAProxy 服务器C 130, HAProxy 服务器D 131]

HAProxy 服务器C 130 -- 负载均衡 --> [MQ 服务器A 128, MQ 服务器B 129]
HAProxy 服务器D 130 -- 负载均衡 --> [MQ 服务器A 128, MQ 服务器B 129]

测试单个 RabbitMQ 服务器:将服务器地址修改为 192.168.186.128,其余不变。

测试 HAProxy 实现多个 RabbitMQ 服务器负载均衡:将服务器地址修改为 192.168.186.130,其余不变。

测试 KeepAlived 实现的高可用的 HAProxy 集群:将服务器地址修改为 KeepAlived 的虚拟 IP 192.168.186.66,其余不变。

想了解更多,欢迎关注我的微信公众号:Renda_Zhang

猜你喜欢

转载自blog.csdn.net/qq_40286307/article/details/109136528