消息队列中间件,是分布式系统中的重要组件;主要解决异步处理、应用解耦、流量削峰等问题,从而实现高性能,高可用,可伸缩和最终一致性的架构。
使用较多的消息队列产品:RabbitMQ,RocketMQ,ActiveMQ,ZeroMQ,Kafka 等。
用户注册后,需要发送验证邮箱和手机验证码。
将注册信息写入数据库,发送验证邮件,发送手机,三个步骤全部完成后,返回给客户端。
传统:
客户端 <-> 注册信息写入数据库 -> 发送注册邮件 -> 发送注册短信
现在:
客户端 <-> 注册信息写入数据库 -> 写入消息队列 -> 异步 [发送注册邮件,发送注册短信]
场景:订单系统需要通知库存系统。
如果库存系统异常,则订单调用库存失败,导致下单失败。
原因:订单系统和库存系统耦合度太高。
传统:
用户 <-> 订单系统 - 调用库存接口 -> 库存系统
现在:
用户 <-> 订单系统 - 写入 -> 消息队列 <- 订阅 - 库存系统
订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户,下单成功。
库存系统:订阅下单的消息,获取下单信息,库存系统根据下单信息,再进行库存操作。
假如:下单的时候,库存系统不能正常运行,也不会影响下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了,实现了订单系统和库存系统的应用解耦。
所以,消息队列是典型的“生产者-消费者“模型。
生产者不断的向消息队列中生产消息,消费者不断的从队列中获取消息。
因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的入侵,这样就实现了生产者和消费者的解耦。
抢购,秒杀等业务,针对高并发的场景。
因为流量过大,暴增会导致应用挂掉,为解决这个问题,在前端加入消息队列。
用户的请求,服务器接收后,首先写入消息队列,如果超过队列的长度,就抛弃,发送一个结束的页面;而请求成功的就是进入队列的用户。
Advanced Message Queuing Protocol 是一个提供统一消息服务的应用层标准高级消息队列协议。
协议:数据在传输的过程中必须要遵守的规则。
基于此协议的客户端可以与消息中间件传递消息。
并不受产品、开发语言等条件的限制。
Java Message Server 是 Java 消息服务应用程序接口,一种规范,和 JDBC 担任的角色类似。
JMS 是一个 Java 平台中关于面向消息中间件的 API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
JMS 是定义了统一接口,统一消息操作;AMQP 通过协议统一数据交互格式。
JMS 必须是 Java 语言;AMQP 只是协议,与语言无关。
Erlang 是一种通用的面向并发的编程语言,目的是创造一种可以应对大规模并发活动的编程语言和运行环境。
最初是专门为通信应用设计的,比如控制交换机或者变换协议等,因此非常适合构建分布式,实时软并行计算系统。
Erlang 运行时环境是一个虚拟机,有点像 Java 的虚拟机,这样代码一经编译,同样可以随处运行。
RabbitMQ 由 Erlang 开发,AMQP 的最佳搭档,安装部署简单,上手门槛低。
企业级消息队列,经过大量实践考验的高可靠,大量成功的应用案例,例如阿里、网易等一线大厂都有使用。
有强大的 WEB 管理页面。
强大的社区支持,为技术进步提供动力。
支持消息持久化、支持消息确认机制、灵活的任务分发机制等,支持功能非常丰富。
集群扩展很容易,并且可以通过增加节点实现成倍的性能提升。
总结:如果希望使用一个可靠性高、功能强大、易于管理的消息队列系统那么就选择 RabbitMQ;如果想用一个性能高,但偶尔丢点数据,可以使用 Kafka 或者 ZeroMQ。
Kafka 和 ZeroMQ 的性能比 RabbitMQ 好很多。
Publisher --> Exchange --banding--> Queue --> Connection --> Consumer
|-------------------------------|
| |-------------------------| |
| | |------------------| | |
| | |Exchange --> Queue| | |
| | |------------------| | |
| | Virtual Host | |
| |-------------------------| |
| Broker |
|-------------------------------|
Broker 包含 Virtual Host
Virtual Host 包含 Exchange 和 Queue
Connection 包含多个 Channel
Broker - 消息队列服务器实体。
Virtual Host - 虚拟主机:
/
,必须在链接时指定。Exchange - 交换器(路由):用来接收生产者发送的消息并将这些消息通过路由发给服务器中的队列。
Banding - 绑定。
Queue - 消息队列:
Banding - 绑定:用于消息队列和交换机之间的关联。
Channel - 通道(信道):
Connection - 网络连接,比如一个 TCP 连接。
Publisher - 消息的生产者,也是一个向交换器发布消息的客户端应用程序。
Consumer - 消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
Message - 消息:
想要安装 RabbitMQ,必须先安装 erlang 语言环境;类似安装 tomcat,必须先安装 JDK。
查看匹配的版本:https://www.rabbitmq.com/which-erlang.html
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
systemctl start rabbitmq-server.service
systemctl status rabbitmq-server.service
systemctl restart rabbitmq-server.service
systemctl stop rabbitmq-server.service
ps -ef | grep rabbitmq
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。
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8maven.compiler.encoding>
<java.version>1.11java.version>
<maven.compiler.source>11maven.compiler.source>
<maven.compiler.target>11maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>5.7.3version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>1.7.25version>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.9version>
dependency>
dependencies>
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 提供了 6 种消息模型,但是第 6 种其实是 RPC,并不是 MQ。
在线手册:https://www.rabbitmq.com/getstarted.html
5 种消息模型,大体分为两类:
点对点模式 - P2P(Point to Point)模式:
包含三个角色:消息队列 queue,发送者 sender,接收者 receiver。
每个消息发送到一个特定的队列中,接收者从中获得消息。
队列中保留这些消息,直到他们被消费或超时。
如果希望发送的每个消息都会被成功处理,那需要 P2P。
点对点模式特点:
发布订阅模式 - publish / subscribe 模式:
发布订阅模式特点:
RabbitMQ 本身只是接收,存储和转发消息,并不会对信息进行处理;类似邮局,处理信件的应该是收件人而不是邮局。
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();
}
}
启动生产者,即可前往管理端查看队列中的信息,会有一条信息没有处理。
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。
通过刚才的案例可以看出,消息一旦被消费,消息就会立刻从队列中移除。
如果消费者接收消息后,还没执行操作就抛异常宕机导致消费失败,但是 RabbitMQ 无从得知,这样消息就丢失了。
因此,RabbitMQ 有一个 ACK 机制,当消费者获取消息后,会向 RabbitMQ 发送回执 ACK,告知消息已经被接收。
ACK - Acknowledge character 即是确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符,表示发来的数据已确认接收无误。在使用 http 请求时,http 的状态码 200 就是表示服务器执行成功。
整个过程就像快递员将包裹送到你手里,并且需要你的签字,并拍照回执。
不过这种回执 ACK 分为两种情况:
两种情况如何选择,需要看消息的重要性:
修改启动手动 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 个人吃,但是其中的某支肉串只能被一个人吃)。
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();
}
}
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);
}
}
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 个消息。
为了克服这个问题,可以使用设置为 prefetchCount = 1
的 basicQos
方法。这告诉RabbitMQ 一次不要给一个 worker 发送一条以上的消息。或者,换句话说,在 worker 处理并确认前一个消息之前,不要向它发送新消息。相反,它将把它分派到下一个不繁忙的 worker。
在消费者 1 和消费者 2 中加上 channel.basicQos(1)
:
...
// queueDeclare() 此方法有双重作用,如果队列不存在,就创建;如果队列存在,则获取
channel.queueDeclare("test_work_queue", false, false, false, null);
// 开启一次接受一条消息。可以理解为:快递一个一个送,送完一个再送下一个,速度快的送件就多
channel.basicQos(1);
...
能者多劳必须要配合手动的 ACK 机制才生效。
工作队列背后的假设是,每个任务都被准确地交付给一个工作者;“发布/订阅”模式将一个消息传递给多个消费者。
生活中的案例:众多粉丝关注一个视频主,视频主发布视频,所有粉丝都可以得到视频通知。
生产者 P 发送信息给路由 X,路由 X 将信息转发给绑定路由 X 的队列;队列将信息通过信道发送给消费者,最后消费者进行消费。整个过程,必须先创建路由。
路由在生产者程序中创建。
路由没有存储消息的能力,当生产者将信息发送给路由后,消费者还没有运行,所以没有队列,路由并不知道将信息发送给谁。
运行程序的顺序:
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();
}
}
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);
}
}
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)分发消息给不同的队列;每种类型可以对应多个消费者。
运行程序的顺序:
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();
}
}
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);
}
}
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();
}
}
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);
}
}
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);
}
}
五种消息模型,在企业中应用最广泛的就是定向匹配 topics。
Spring AMQP 是基于 Spring 框架的 AMQP 消息解决方案,提供模板化的发送和接收消息的抽象层,提供基于消息驱动的 POJO 的消息监听等,简化了对于 RabbitMQ 相关程序的开发。
依赖 pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.amqpgroupId>
<artifactId>spring-rabbitartifactId>
<version>2.0.1.RELEASEversion>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>1.7.25version>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.9version>
dependency>
dependencies>
spring-rabbitmq-producer.xml
<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"
/>
<rabbit:queue name="test_spring_queue_1"/>
<rabbit:admin connection-factory="connectionFactory"/>
<rabbit:topic-exchange name="spring_topic_exchange">
<rabbit:bindings>
<rabbit:binding pattern="msg.#" queue="test_spring_queue_1"/>
rabbit:bindings>
rabbit:topic-exchange>
<bean id="jsonMessageConverter" class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter"/>
<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
<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">
<rabbit:connection-factory
id="connectionFactory"
host="192.168.186.128"
port="5672"
username="renda"
password="123456"
virtual-host="/renda"/>
<rabbit:queue name="test_spring_queue_1"/>
<rabbit:admin connection-factory="connectionFactory"/>
<context:component-scan base-package="com.renda.listener"/>
<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);
}
}
RabbitMQ 为了保证消息的成功投递,采用通过 AMQP 协议层面提供事务机制的方案,但是采用事务会大大降低消息的吞吐量。
开启事务性能最大损失超过 250 倍。
事务效率低下原因:100 条消息,前 99 条成功,如果第 100 条失败,那么 99 条消息要全部撤销回滚。
更加高效的解决方式是采用 Confirm 模式,而 Confirm 模式则采用补发第 100 条的措施来完成 100 条消息的送达。
resources\spring\spring-rabbitmq-producer.xml
...
<rabbit:template id="rabbitTemplate"
connection-factory="connectionFactory"
exchange="spring_topic_exchange"
message-converter="jsonMessageConverter"
confirm-callback="messageConfirm"/>
<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.配置监听 -->
<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 条消息,直到消息队列为空。
Time To Live - 生存时间、还能活多久,单位毫秒。
在这个周期内,消息可以被消费者正常消费,超过这个时间,则自动删除(其实是被称为 dead message 并投入到死信队列,无法消费该消息)。
RabbitMQ 可以对消息和队列设置 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 秒之后,消息自动删除。
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 交换机的队列,称之为:“死信队列”。
消息没有被及时消费的原因:
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
<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"
/>
<rabbit:admin connection-factory="connectionFactory"/>
<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>
<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>
<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 有 3 种模式,其中 2 种是集群模式。
单一模式:即单机情况不做集群,就单独运行一个 RabbitMQ 而已。
普通模式:默认模式,以两个节点(A、B)为例来进行说明:
镜像模式 - 经典的 Mirror 镜像模式,保证数据不丢失:
另外,还有主备模式,远程模式,多活模式等等。
前置条件:准备两台 linux(192.168.186.128 和 192.168.186.129),并安装好 RabbitMQ。
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
,否则配置不生效。
cookie
必须保持一致,同步 RabbitMQ 的 cookie 文件:跨服务器拷贝 .erlang.cookie
(隐藏文件,使用 ls -all
显示)。scp /var/lib/rabbitmq/.erlang.cookie 192.168.186.129:/var/lib/rabbitmq/
修改 cookie
文件,要重启 linux 服务器 reboot
。
firewall-cmd --zone=public --add-port=4369/tcp --permanent
firewall-cmd --reload
systemctl start rabbitmq-server
[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 ...
rabbitmqctl cluster_status
搭建集群结构之后,之前创建的交换机、队列、用户都属于单一结构,在新的集群环境中是不能用的。
所以在新的集群中重新手动添加用户即可(任意节点添加,所有节点共享)。
[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
停掉,再测试,仍然可以发送和接受消息。
虽然在程序中访问 A 服务器,可以实现消息的同步,但都是 A 服务器在接收消息,A 太累;是否可以负载均衡,A 和 B 轮流接收消息,再镜像同步。
HA - High Available 高可用,Proxy - 代理。
HAProxy 是一款提供高可用性,负载均衡,并且基于 TCP 和 HTTP 应用的代理软件。
HAProxy 完全免费。
HAProxy 可以支持数以万计的并发连接。
HAProxy 可以简单又安全的整合进架构中,同时还保护 Web 服务器不被暴露到网络上。
生产者 -- 投递消息 --> HAProxy
消费者 -- 订阅消息 --> HAProxy
HAProxy ---> [MQ Node 1, MQ Node 2, MQ Node 3]
OSI - Open System Interconnection 开放式系统互联,是把网络通信的工作分为 7 层,分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
Nginx 的优点:
HAProxy 的优点:
性能上 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 服务器。
如果 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
修改映射文件 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 addr
或 ip 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 地址。
常见的网络错误:子网掩码、网关等信息要一致。
在 192.168.186.128,A 服务器上测试。
在服务器 A 执行 curl 192.168.186.130:5672
和 curl 192.168.186.66:5672
都能正常返回 AMPQ
,说明安装成功。
使用 ip addr
或 ip 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