一、简介
是⼀个开源的AMQP实现,服务器端用Erlang语⾔编 写,支持多种客户端,如:Python、Ruby、.NET、 Java、C、用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不错
- 作用:
- 异步:模块服务及时响应,不重要操作可投递队列,队列执行
- 削峰:高并发/大量请求涌入,队列化,慢慢处理
- 解耦:模块之间的调用,不进行强关联
- 缺点:使用Erlang开发,阅读和修改源码难度大
- 拓展:
- AMQP:是一种协议,类似与HTTP协议,各大MQ厂商基于此协议开发MQ消息产品,有跨语言跨平台的特性,支持长周期消息传递、支持事务(跨消息队列)
- JMS:是针对Java编写的API协议,类似于JDBC,只适用于java/jvm语言,不能够跨语言
- MQTT:消息队列遥测传输(Message Queueing Telemetry Transport),一种协议,内存占用低,不支持长周期存储和转发,不允许分段消息(很难发送长消息),不安全建立连接,仅适用于性能结算不高的小型设备,例如扫地机器人,智能家具等,主要用于物联网(IOT)
二、核心概念
- Broker
- RabbitMQ的服务端程序,可以认为⼀个mq节点就是⼀个broker
- Producer
- 生产者,创建消息Message,然后发布到RabbitMQ中
- Consumer
- 消费者,消费队列里面的消息
- Message
- 生产消费的内容,有消息头和消息体,也包括多个属性配置,比如routingKey路由键
- Queue
- 队列,是RabbitMQ 的内部对象,用于存储消息,消息都只能存储在队列中
- Channel
- 信道,⼀条支持多路复用的通道,独立的双向数据流通道,可以发布、订阅、接收消息。
- 信道是建立在真实的TCP连接内的虚拟连接,复用TCP连接的通道
- Connection
- 连接,是RabbitMQ的socket链接,它封装了socket协议相关部分逻辑,⼀个连接上可以有多个channel进行通信
- Exchange
- 交换机,⽣产者将消息发送到 Exchange,交换器将消息路由到⼀个或者多个队列中,里面有多个类型,队列和交换机是多对多的关系。
- RoutingKey
- 路由键,生产者将消息发给交换器的时候,⼀般会指定⼀个
- RoutingKey,用来指定这个消息的路由规则,最大长度255字节
- Binding
- 绑定,通过绑定将交换器与队列关联起来,在绑定的时候⼀般会指定⼀个绑定键 (BindingKey),这样RabbitMQ 就知道如何正确地将消息路由到队列了
- Virtual host 虚拟主机
- 用于不同业务模块的逻辑隔离,⼀个Virtual Host里面可以有若干个Exchange和Queue,同⼀个VirtualHost里面不能有相同名称的Exchange或Queue,默认是 /
三、docker 安装
#拉取rabbitMq镜像
docker pull rabbitmq:management
#启动容器
docker run -d --hostname rabbit_host1 --name rabbitMq
-e RABBITMQ_DEFAULT_USER=admin
-e RABBITMQ_DEFAULT_PASS=111111
-p 15672:15672 -p 5672:5672 rabbitmq:management
端口介绍:
- 4369 erlang 发现口
- 5672 client 端通信口
- 15672 管理界⾯ ui 端口
- 25672 server 间内部通信口
四、rabbitmq 简易使用
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.6.2</version>
</dependency>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.10.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
- 简单生产消费
/**
* 消息投递方
*/
@Slf4j
public class Send {
private static final String QUEUE_NAME = "simple";
public static void main(String[] args) {
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//获取一个连接
try(Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel()) {
/**
* 队列名称
* 持久化配置:mq重启后还在
* 是否独占:只能有⼀个消费者监听队列;当connection关闭是否删除队列,⼀般是false,发布订阅是独占
* ⾃动删除: 当没有消费者的时候,⾃动删除掉,⼀般是false
* 其他参数
*
* 队列不存在则会⾃动创建,如果存在则不会覆盖,所以此时的时候需要注意属性
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "你好";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
log.info("发送消息成功:队列={},消息={}", QUEUE_NAME, message);
} catch (Exception e){
log.error("发送消息失败{}", e);
}
}
}
/**
* 消息消费方
*/
@Slf4j
public class Receive {
private static final String QUEUE_NAME = "simple";
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//消费者无需关闭连接,持续消费
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// //第一种方式
// Consumer consumer = new DefaultConsumer(channel){
// @Override
// public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException, UnsupportedEncodingException {
// // consumerTag 是固定的 可以做此会话的名字, deliveryTag 每次接收消息+1
// log.info("consumerTag 消息标识={}", consumerTag);
// //可以获取交换机,路由健等
// log.info("envelope元数据={}", envelope);
// log.info("properties配置信息={}", properties);
// log.info("body={}", new String(body, StandardCharsets.UTF_8));
// log.info("消费消息成功:{}", new String(body, StandardCharsets.UTF_8));
// }
// };
// //⾃动确认消息
// channel.basicConsume(QUEUE_NAME, true, consumer);
//第二种方式
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
log.info("消费消息成功:{}", message);
};
//⾃动确认消息
channel.basicConsume(QUEUE_NAME,true, deliverCallback, consumerTag -> { });
}
}
- 简单生产轮询消费
/**
* 消息投递方
*/
@Slf4j
public class Send {
private static final String QUEUE_NAME = "polling";
public static void main(String[] args) {
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//获取一个连接
try(Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel()) {
/**
* 队列名称
* 持久化配置:mq重启后还在
* 是否独占:只能有⼀个消费者监听队列;当connection关闭是否删除队列,⼀般是false,发布订阅是独占
* ⾃动删除: 当没有消费者的时候,⾃动删除掉,⼀般是false
* 其他参数
*
* 队列不存在则会⾃动创建,如果存在则不会覆盖,所以此时的时候需要注意属性
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//轮训发送 10个
for(int i=0;i<10;i++){
String message = "Hello World!"+i;
channel.basicPublish("",
QUEUE_NAME, null,
message.getBytes(StandardCharsets.UTF_8));
log.info("发送消息成功:队列={},消息={}", QUEUE_NAME, message);
}
} catch (Exception e){
log.error("发送消息失败{}", e);
}
}
}
/**
* 消息消费方1
*/
@Slf4j
public class Receive1 {
private static final String QUEUE_NAME = "polling";
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//消费者无需关闭连接,持续消费
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// //第一种方式
// Consumer consumer = new DefaultConsumer(channel){
// @Override
// public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException, UnsupportedEncodingException {
// // consumerTag 是固定的 可以做此会话的名字, deliveryTag 每次接收消息+1
// log.info("consumerTag 消息标识={}", consumerTag);
// //可以获取交换机,路由健等
// log.info("envelope元数据={}", envelope);
// log.info("properties配置信息={}", properties);
// log.info("body={}", new String(body, StandardCharsets.UTF_8));
// log.info("消费消息成功:{}", new String(body, StandardCharsets.UTF_8));
// }
// };
// //⾃动确认消息
// channel.basicConsume(QUEUE_NAME, true, consumer);
//第二种方式
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
//慢消费过程
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
//⼿⼯确认消息消费,不是多条确认
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
log.info("消费消息成功:{}", message);
};
//不自动确认消息
channel.basicConsume(QUEUE_NAME,false, deliverCallback, consumerTag -> { });
}
}
/**
* 消息消费方2
*/
@Slf4j
public class Receive2 {
private static final String QUEUE_NAME = "polling";
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//消费者无需关闭连接,持续消费
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// //第一种方式
// Consumer consumer = new DefaultConsumer(channel){
// @Override
// public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException, UnsupportedEncodingException {
// // consumerTag 是固定的 可以做此会话的名字, deliveryTag 每次接收消息+1
// log.info("consumerTag 消息标识={}", consumerTag);
// //可以获取交换机,路由健等
// log.info("envelope元数据={}", envelope);
// log.info("properties配置信息={}", properties);
// log.info("body={}", new String(body, StandardCharsets.UTF_8));
// log.info("消费消息成功:{}", new String(body, StandardCharsets.UTF_8));
// }
// };
// //⾃动确认消息
// channel.basicConsume(QUEUE_NAME, true, consumer);
//第二种方式
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
//慢消费过程
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
//⼿⼯确认消息消费,不是多条确认
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
log.info("消费消息成功:{}", message);
};
//不自动确认消息
channel.basicConsume(QUEUE_NAME,false, deliverCallback, consumerTag -> { });
}
}
- 简单生产根据消费能力消费
/**
* 消息投递方
*/
@Slf4j
public class Send {
private static final String QUEUE_NAME = "polling";
public static void main(String[] args) {
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//获取一个连接
try(Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel()) {
/**
* 队列名称
* 持久化配置:mq重启后还在
* 是否独占:只能有⼀个消费者监听队列;当connection关闭是否删除队列,⼀般是false,发布订阅是独占
* ⾃动删除: 当没有消费者的时候,⾃动删除掉,⼀般是false
* 其他参数
*
* 队列不存在则会⾃动创建,如果存在则不会覆盖,所以此时的时候需要注意属性
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//轮训发送 10个
for(int i=0;i<10;i++){
String message = "Hello World!"+i;
channel.basicPublish("",
QUEUE_NAME, null,
message.getBytes(StandardCharsets.UTF_8));
log.info("发送消息成功:队列={},消息={}", QUEUE_NAME, message);
}
} catch (Exception e){
log.error("发送消息失败{}", e);
}
}
}
/**
* 消息消费方1
*/
@Slf4j
public class Receive1 {
private static final String QUEUE_NAME = "polling";
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//消费者无需关闭连接,持续消费
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//凭消费能力消费,每次仅消费一次才能继续接收下一个消息
channel.basicQos(1);
// //第一种方式
// Consumer consumer = new DefaultConsumer(channel){
// @Override
// public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException, UnsupportedEncodingException {
// // consumerTag 是固定的 可以做此会话的名字, deliveryTag 每次接收消息+1
// log.info("consumerTag 消息标识={}", consumerTag);
// //可以获取交换机,路由健等
// log.info("envelope元数据={}", envelope);
// log.info("properties配置信息={}", properties);
// log.info("body={}", new String(body, StandardCharsets.UTF_8));
// log.info("消费消息成功:{}", new String(body, StandardCharsets.UTF_8));
// }
// };
// //⾃动确认消息
// channel.basicConsume(QUEUE_NAME, true, consumer);
//第二种方式
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
//慢消费过程
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
//⼿⼯确认消息消费,不是多条确认
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
log.info("消费消息成功:{}", message);
};
//不自动确认消息
channel.basicConsume(QUEUE_NAME,false, deliverCallback, consumerTag -> { });
}
}
/**
* 消息消费方2
*/
@Slf4j
public class Receive2 {
private static final String QUEUE_NAME = "polling";
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//消费者无需关闭连接,持续消费
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//凭消费能力消费,每次仅消费一次才能继续接收下一个消息
channel.basicQos(1);
// //第一种方式
// Consumer consumer = new DefaultConsumer(channel){
// @Override
// public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException, UnsupportedEncodingException {
// // consumerTag 是固定的 可以做此会话的名字, deliveryTag 每次接收消息+1
// log.info("consumerTag 消息标识={}", consumerTag);
// //可以获取交换机,路由健等
// log.info("envelope元数据={}", envelope);
// log.info("properties配置信息={}", properties);
// log.info("body={}", new String(body, StandardCharsets.UTF_8));
// log.info("消费消息成功:{}", new String(body, StandardCharsets.UTF_8));
// }
// };
// //⾃动确认消息
// channel.basicConsume(QUEUE_NAME, true, consumer);
//第二种方式
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
//慢消费过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
//⼿⼯确认消息消费,不是多条确认
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
log.info("消费消息成功:{}", message);
};
//不自动确认消息
channel.basicConsume(QUEUE_NAME,false, deliverCallback, consumerTag -> { });
}
}
五、交换机
- 简介
-
⽣产者将消息发送到 Exchange,交换器将消息路由到 ⼀个或者多个队列中,交换机有多个类型,队列和交换 机是多对多的关系。
-
交换机只负责转发消息,不具备存储消息的能力,如果没有队列和exchange绑定,或者没有符合的路由规则,则消息会被丢失
-
RabbitMQ有四种交换机类型,分别是Direct exchange、Fanout exchange、Topic exchange、Headers exchange,最后的基本不用
-
- 交换机类型
-
Direct Exchange 定向
-
将⼀个队列绑定到交换机上,要求该消息与⼀个特定的路由键完全匹配
-
例如:如果⼀个队列绑定到该交换机上要求路由键 “aa”,则只有被标记为“aa”的消息才被转发, 不会转发“aa.bb”,也不会转发“a.b.c”,只会转发“aa”
-
处理路由健
-
-
Fanout Exchange 广播
-
只需要简单的将队列绑定到交换机上,⼀个发送到交换机的消息都会被转发到与该交换机绑定的所有 队列上。很像子网广播,每台子网内的主机都获得 了⼀份复制的消息
-
Fanout交换机转发消息是最快的,用于发布订阅,广播形式,中文是扇形
-
不处理路由健
-
-
Topic Exchange 通配符
-
主题交换机是⼀种发布/订阅的模式,结合了直连交换机与扇形交换机的特点将路由键和某模式进行匹配。此时队列需要绑定要⼀个模式上
-
符号“#”匹配⼀个或多个词,符号“*”匹配不多不少⼀个词,“.”分割为一个词
-
例如:因此“abc.#”能够匹配到“abc.def.ghi”,但是“abc.*” 只会匹配到“abc.def”
-
- Headers Exchange(少用)
- 根据发送的消息内容中的headers属性进⾏匹配, 在绑定Queue与Exchange时指定⼀组键值对
- 当消息发送到RabbitMQ时会取到该消息的headers与Exchange绑定时指定的键值对进行匹配;
- 如果完全匹配则消息会路由到该队列,否则不会路由到该队列
- 不处理路由键
-
六、模式
发布订阅模式
- 什么是rabbitmq的发布订阅模式
- 发布-订阅模型中,消息生产者不再是直接⾯对queue(队列名称),而是直⾯exchange,都需要经过exchange来进行消息的发送, 所有发往同⼀个fanout交换机的消息都会被所有监听这个交换机的消费者接收到
- 发布订阅-消息模型引入fanout交换机
-
通过把消息发送给交换机,交互机转发给对应绑定的队列
-
交换机绑定的队列是排它独占队列,自动删除
-
场景
-
微信公众号
-
新浪微博关注
-
/**
* 消息投递方
*/
@Slf4j
public class Send {
private static final String EX_CHANGE_NAME = "fanout_ex_change";
public static void main(String[] args) {
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//获取一个连接
try(Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel()) {
/**
* 交换机名称
* 交换机类型
*/
channel.exchangeDeclare(EX_CHANGE_NAME, BuiltinExchangeType.FANOUT);
String message = "Hello World!";
channel.basicPublish(EX_CHANGE_NAME, "", null, message.getBytes(StandardCharsets.UTF_8));
log.info("发送消息成功:{}", message);
} catch (Exception e){
log.error("发送消息失败{}", e);
}
}
}
/**
* 消息消费方1
*/
@Slf4j
public class Receive1 {
private static final String EX_CHANGE_NAME = "fanout_ex_change";
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//消费者无需关闭连接,持续消费
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EX_CHANGE_NAME, BuiltinExchangeType.FANOUT);
//凭消费能力消费,每次仅消费一次才能继续接收下一个消息
channel.basicQos(1);
//获取队列(排它队列)
String queueName = channel.queueDeclare().getQueue();
//绑定队列和交换机,fanout交换机不⽤指定routingkey
channel.queueBind(queueName,EX_CHANGE_NAME,"");
//第二种方式
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
log.info("消费消息成功:{}", message);
};
//自动确认消息
channel.basicConsume(queueName,true, deliverCallback, consumerTag -> { });
}
}
/**
* 消息消费方2
*/
@Slf4j
public class Receive2 {
private static final String EX_CHANGE_NAME = "fanout_ex_change";
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//消费者无需关闭连接,持续消费
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EX_CHANGE_NAME, BuiltinExchangeType.FANOUT);
//凭消费能力消费,每次仅消费一次才能继续接收下一个消息
channel.basicQos(1);
//获取队列(排它队列)
String queueName = channel.queueDeclare().getQueue();
//绑定队列和交换机,fanout交换机不⽤指定routingkey
channel.queueBind(queueName,EX_CHANGE_NAME,"");
//第二种方式
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
log.info("消费消息成功:{}", message);
};
//自动确认消息
channel.basicConsume(queueName,true, deliverCallback, consumerTag -> { });
}
}
路由模式
- 什么是rabbitmq的路由模式
- 交换机类型是Direct 队列和交换机绑定,需要指定⼀个路由key( 也叫 Bingding Key)
- 消息生产者发送消息给交换机,需要指定routingKey
- 交换机根据消息的路由key,转发给对应的队列
- 场景
- 日志采集系统 ELK
/**
* 消息投递方
*/
@Slf4j
public class Send {
private static final String EX_CHANGE_NAME = "direct_ex_change";
public static void main(String[] args) {
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//获取一个连接
try(Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel()) {
/**
* 交换机名称
* 交换机类型
*/
channel.exchangeDeclare(EX_CHANGE_NAME, BuiltinExchangeType.DIRECT);
String info = "info信息";
String error = "error信息";
String warning = "warning信息";
channel.basicPublish(EX_CHANGE_NAME, "infoRoutingKey", null, info.getBytes(StandardCharsets.UTF_8));
channel.basicPublish(EX_CHANGE_NAME, "errorRoutingKey", null, error.getBytes(StandardCharsets.UTF_8));
channel.basicPublish(EX_CHANGE_NAME, "warningRoutingKey", null, warning.getBytes(StandardCharsets.UTF_8));
log.info("发送消息成功");
} catch (Exception e){
log.error("发送消息失败{}", e);
}
}
}
/**
* 消息消费方1
*/
@Slf4j
public class Receive1 {
private static final String EX_CHANGE_NAME = "direct_ex_change";
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//消费者无需关闭连接,持续消费
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EX_CHANGE_NAME, BuiltinExchangeType.DIRECT);
//凭消费能力消费,每次仅消费一次才能继续接收下一个消息
channel.basicQos(1);
//获取队列(排它队列)
String queueName = channel.queueDeclare().getQueue();
//绑定队列和交换机,三个级别路由到一个队列
channel.queueBind(queueName,EX_CHANGE_NAME,"infoRoutingKey");
channel.queueBind(queueName,EX_CHANGE_NAME,"errorRoutingKey");
channel.queueBind(queueName,EX_CHANGE_NAME,"warningRoutingKey");
//第二种方式
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
log.info("消费消息成功:{}", message);
};
//自动确认消息
channel.basicConsume(queueName,true, deliverCallback, consumerTag -> { });
}
}
/**
* 消息消费方2
*/
@Slf4j
public class Receive2 {
private static final String EX_CHANGE_NAME = "direct_ex_change";
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//消费者无需关闭连接,持续消费
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EX_CHANGE_NAME, BuiltinExchangeType.DIRECT);
//凭消费能力消费,每次仅消费一次才能继续接收下一个消息
channel.basicQos(1);
//获取队列(排它队列)
String queueName = channel.queueDeclare().getQueue();
//绑定队列和交换机,warning级别路由到一个队列
channel.queueBind(queueName,EX_CHANGE_NAME,"warningRoutingKey");
//第二种方式
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
log.info("消费消息成功:{}", message);
};
//自动确认消息
channel.basicConsume(queueName,true, deliverCallback, consumerTag -> { });
}
}
通配符模式(使用最多)
- 什么是rabbitmq的主题模式
- 交换机是 topic, 可以实现发布订阅模式fanout和路由模式Direct 的功能,更加灵活,⽀持模式匹配,通配符等
- 交换机同过通配符进行转发到对应的队列,* 代表⼀个词,#代表1个或多个词,⼀般⽤#作为通配符居多,比如 #.order, 会匹配 info.order 、sys.error.order, 而*.order ,只会匹配 info.order, 之间是使用. 点进行分割多个词的; 如果是 ., 则info.order、error.order都会匹配
- 注意
- 交换机和队列绑定时⽤的binding使用通配符的路由健
- 生产者发送消息时需要使用具体的路由健
- 场景
- 系统日志各个模块收集
/**
* 消息投递方
*/
@Slf4j
public class Send {
private static final String EX_CHANGE_NAME = "topic_ex_change";
public static void main(String[] args) {
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//获取一个连接
try(Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel()) {
/**
* 交换机名称
* 交换机类型
*/
channel.exchangeDeclare(EX_CHANGE_NAME, BuiltinExchangeType.TOPIC);
String info = "info信息";
String error = "error信息";
String warning = "warning信息";
channel.basicPublish(EX_CHANGE_NAME, "user.log.info", null, info.getBytes(StandardCharsets.UTF_8));
channel.basicPublish(EX_CHANGE_NAME, "user.log.error", null, error.getBytes(StandardCharsets.UTF_8));
channel.basicPublish(EX_CHANGE_NAME, "user.log.warning", null, warning.getBytes(StandardCharsets.UTF_8));
log.info("发送消息成功");
} catch (Exception e){
log.error("发送消息失败{}", e);
}
}
}
/**
* 消息消费方1
*/
@Slf4j
public class Receive1 {
private static final String EX_CHANGE_NAME = "topic_ex_change";
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//消费者无需关闭连接,持续消费
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EX_CHANGE_NAME, BuiltinExchangeType.TOPIC);
//凭消费能力消费,每次仅消费一次才能继续接收下一个消息
channel.basicQos(1);
//获取队列(排它队列)
String queueName = channel.queueDeclare().getQueue();
//绑定队列和交换机,三个级别路由到一个队列
channel.queueBind(queueName,EX_CHANGE_NAME,"*.log.*");
//第二种方式
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
log.info("消费消息成功:{}", message);
};
//自动确认消息
channel.basicConsume(queueName,true, deliverCallback, consumerTag -> { });
}
}
/**
* 消息消费方2
*/
@Slf4j
public class Receive2 {
private static final String EX_CHANGE_NAME = "topic_ex_change";
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.132.79.236");
connectionFactory.setVirtualHost("/dev");
connectionFactory.setPassword("111111");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
//消费者无需关闭连接,持续消费
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EX_CHANGE_NAME, BuiltinExchangeType.TOPIC);
//凭消费能力消费,每次仅消费一次才能继续接收下一个消息
channel.basicQos(1);
//获取队列(排它队列)
String queueName = channel.queueDeclare().getQueue();
//绑定队列和交换机,三个级别路由到一个队列
channel.queueBind(queueName,EX_CHANGE_NAME,"user.log.error");
//第二种方式
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
log.info("消费消息成功:{}", message);
};
//自动确认消息
channel.basicConsume(queueName,true, deliverCallback, consumerTag -> { });
}
}
七、SpringBoot + SpringAMQP + RabbitMQ(工作常用)
- 什么是Spring-AMQP
- 官网:https://spring.io/projects/spring-amqp
- Spring 框架的AMQP消息解决方案,提供模板化的发送和接收消息的抽象层,提供基于消息驱动的 POJO的消息监听等
- 提供不依赖于任何特定的AMQP代理实现或客户端库通用的抽象,最终⽤户代码将很容易实现更易替换、添加和删除AMQP,因为它可以只针对抽象层来开发总之就是提高我们的框架整合消息队列的效率,SpringBoot为更方便开发RabbitMQ推出了starter
- 项目中也经常使用 spring-boot-starter-amqp 进行开发
- 依赖
<!--引⼊AMQP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.5.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.2</version>
</dependency>
- yml 配置
#消息队列
spring:
rabbitmq:
host: 114.132.79.236
port: 5672
virtual-host: /dev
password: 111111
username: admin
- 代码
/**
* 交换机队列配置
*/
@Configuration
public class RabbitMQConfig {
public static final String EXCHANGE_NAME = "order_exchange";
public static final String QUEUE_NAME = "order_queue";
/**
* 交换机
*/
@Bean(name = "orderExchange")
public Exchange orderExchange() {
return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
//return new TopicExchange(EXCHANGE_NAME, true, false);
}
/**
* 队列
* @return
*/
@Bean(name = "orderQueue")
public Queue orderQueue() {
return QueueBuilder.durable(QUEUE_NAME).build();
//return new Queue(QUEUE_NAME, true,false, false, null);
}
/**
* 交换机和队列绑定关系
*/
@Bean
public Binding orderBinding(@Qualifier("orderQueue") Queue queue,@Qualifier("orderExchange") Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("order.#").noargs();
//return new Binding(QUEUE_NAME,Binding.DestinationType.QUEUE, EXCHANGE_NAME,"order.#", null);
}
}
/**
*消息监听
*/
@Component
@RabbitListener(queues = {"order_queue"})
public class OrderMQListener {
/**
* RabbitHandler 会⾃动匹配 消息类型(消息⾃动确认)
* @param msg
* @param message
* @throws IOException
*/
@RabbitHandler
public void releaseCouponRecord(String msg, Message message) throws IOException {
long msgTag = message.getMessageProperties().getDeliveryTag();
System.out.println("msgTag="+msgTag);
System.out.println("message="+message.toString());
System.out.println("监听到消息:消息内容:"+new String(message.getBody(), StandardCharsets.UTF_8));
}
}
/**
* 消息投递
*/
@SpringBootTest
class DemoApplicationTests {
@Autowired
private RabbitTemplate template;
@Test
void send() {
template.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "order.new", "您有一笔新的订单,请注意查看");
}
}
八、可靠投递和可靠消费
- 什么是消息的可靠性投递
- 保证消息百分百发送到消息队列中去
- 保证mq节点成功接受消息
- 消息发送端需要接受到mq服务端接受到消息的确认应答
- 完善的消息补偿机制,发送失败的消息可以再感知并⼆次处理
- 保证消息百分百发送到消息队列中去
- RabbitMQ消息投递路径
- 生产者-->交换机->队列->消费者
- 通过两个的点控制消息的可靠性投递
- 生产者到交换机通过confirmCallback
- 交换机到队列通过returnCallback
- 注意
-
开启消息确认机制以后,保证了消息的准确送达,但由于频繁的确认交互,rabbitmq 整体效率变低,吞吐量下降严重,不是非常重要的消息不建议用消息确认机制
-
- 生产者-->交换机可靠投递
- 通过confirmCallback
- 生产者投递消息后,如果Broker收到消息后,会给生产者⼀个ACK。⽣产者通过ACK,可以确认这条消息是否正常发送到Broker,这种方式是消息可靠性投递的核⼼
- 开启confirmCallback
#消息队列
spring:
rabbitmq:
#新版,NONE值是禁⽤发布确认模式,是默认值,CORRELATED值是发布消息成功到交换器后会触发回调⽅法
publisher-confirm-type: correlated
/**
* 开启ack确认消息投递
*/
@SpringBootTest
class DemoApplicationTests {
@Autowired
private RabbitTemplate template;
@Test
void testConfirmCallback() {
template.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* @param correlationData 配置
* @param ack 交换机是否收到消息,true是成功,false是失败
* @param cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm=====>");
System.out.println("confirm====ack=" + ack);
System.out.println("confirm====cause=" + cause);
//根据ACK状态做对应的消息更新操作 TODO
}
});
template.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "order.new", "您有一笔新的订单,请注意查看");
}
}
- 交换机-->队列可靠投递
- 通过returnCallback
- 消息从交换器发送到对应队列失败时触发
- 两种模式
- 交换机到队列不成功,则丢弃消息(默认)
- 交换机到队列不成功,返回给消息生产者,触发returnCallback
开始returnCallBack
#消息队列
spring:
rabbitmq:
#新版 开启returnCallback配置 消息从交换机到队列不成功则触发回调方法
publisher-returns: true
#为true,则交换机处理消息到路由失败,则会返回给⽣产者
template:
mandatory: true
/**
* 消息投递
*/
@SpringBootTest
class DemoApplicationTests {
@Autowired
private RabbitTemplate template;
@Test
void testReturnCallback() {
//为true,则交换机处理消息到路由失败,则会返回给⽣产者
//开启强制消息投递(mandatory为设置为true),但消息未被路由⾄任何⼀个queue,则回退⼀条消息
template.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returned) {
int code = returned.getReplyCode();
System.out.println("code="+code);
System.out.println("returned="+returned.toString());
}
});
template.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"order.new","您有一笔新的订单,请注意查看");
}
}
- 可靠消费
- RabbitMQ的消息确认机制ACK介绍
- 消费者从RabbitMQ收到消息并处理完成后,反馈给RabbitMQ,RabbitMQ收到反馈后才将此消息从队列中删除
- 消费者在处理消息出现了⽹络不稳定、服务器异常等现象,那么就不会有ACK反馈,RabbitMQ会认为这个消息没有正常消费,会将消息重新放⼊队列中只有当消费者正确发送ACK反馈,RabbitMQ确认收到后,消息才会从RabbitMQ服务器的数据中删除。
- 消息的ACK确认机制默认是打开的,消息如未被进行ACK的消息确认机制,这条消息被锁定Unacked
- 确认方式
- 自动确认(默认)
- 手动确认 manual
- 开启手动确认消息
- RabbitMQ的消息确认机制ACK介绍
spring:
rabbitmq:
#开启⼿动确认消息,如果消息重新⼊队,进⾏重试
listener:
simple:
acknowledge-mode: manual
/**
* 可靠消费
*/
@Component
@RabbitListener(queues = {"order_queue"})
public class OrderMQListener {
/**
* RabbitHandler 会⾃动匹配 消息类型(消息⾃动确认)
* @param msg
* @param message
* @throws IOException
*/
@RabbitHandler
public void releaseCouponRecord(String msg, Message message, Channel channel) throws IOException {
long msgTag = message.getMessageProperties().getDeliveryTag();
System.out.println("msgTag="+msgTag);
System.out.println("message="+message.toString());
System.out.println("监听到消息:消息内容:"+new String(message.getBody(), StandardCharsets.UTF_8));
try {
//todo 业务操作
int i = 1/0;
//成功确认,使⽤此回执⽅法后,消息会被rabbitmq broker 删除
channel.basicAck(msgTag,false);
} catch (Exception e){
e.printStackTrace();
//业务失败,拒绝并重入队列 这里要限制重试次数,不然会一直重入队列
channel.basicNack(msgTag,false,true);
}
}
}
- deliveryTag介绍
- 表示消息投递序号,每次消费消息或者消息重新投递后,deliveryTag都会增加
- basicNack和basicReject介绍
- basicReject⼀次只能拒绝接收⼀个消息,可以设置是否requeue
- basicNack方法可以⽀持⼀次0个或多个消息的拒收,可以设置是否requeue。
- 人工审核异常消息
- 设置重试阈值,超过后确认消费成功,记录消息,人工处理
九、死信队列
- 什么是TTL
- time to live 消息存活时间
- 如果消息在存活时间内未被消费,则会被清除
- RabbitMQ⽀持两种ttl设置
- 单独消息进行配置ttl
- 整个队列进行配置ttl(居多)
- 什么是rabbitmq的死信队列
- 没有被及时消费的消息存放的队列
- 什么是rabbitmq的死信交换机Dead Letter Exchange(死信交换机,缩写:DLX)
- 当消息成为死信后,会被重新发送到另⼀个交换机,这个交换机就是DLX死信交换机。
- 消息有哪几种情况成为死信
- 消费者拒收消息(basic.reject/ basic.nack),并且没有重新入队 requeue=false
- 消息在队列中未被消费,且超过队列或者消息本身的过期时间TTL(time-to-live)
- 队列的消息长度达到极限
- 结果:消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
- 队列过期时间使用参数,对整个队列消息统⼀过期
- x-message-ttl 单位ms(毫秒)
- 消息过期时间使用参数(如果队列头部消息未过期,队列中级消息已经过期,已经还在队列里面)
- expiration 单位ms(毫秒)
- 两者都配置的话,时间短的先触发
- 实战
/**
* 交换机队列配置
*/
@Configuration
public class RabbitMQConfig {
public static final String ORDER_EXCHANGE_NAME = "order_exchange";
public static final String ORDER_QUEUE_NAME = "order_queue";
public static final String ORDER_ROUTING_KEY = "order.#";
public static final String DIE_ORDER_EXCHANGE_NAME = "die_order_exchange";
public static final String DIE_ORDER_QUEUE_NAME = "die_order_queue";
public static final String DIE_ORDER_ROUTING_KEY = "die.order.#";
/**
* 普通订单交换机
*/
@Bean(name = "orderExchange")
public Exchange orderExchange() {
return ExchangeBuilder.topicExchange(ORDER_EXCHANGE_NAME).durable(true).build();
//return new TopicExchange(EXCHANGE_NAME, true, false);
}
/**
* 普通订单队列
* @return
*/
@Bean(name = "orderQueue")
public Queue orderQueue() {
Map<String,Object> args = new HashMap<>(3);
//消息过期后,进⼊到死信交换机
args.put("x-dead-letter-exchange",DIE_ORDER_EXCHANGE_NAME);
//消息过期后,进⼊到死信交换机的路由key
args.put("x-dead-letter-routing-key",DIE_ORDER_ROUTING_KEY);
//过期时间,单位毫秒
args.put("x-message-ttl", 10000L);
return QueueBuilder.durable(ORDER_QUEUE_NAME).withArguments(args).build();
}
/**
* 交换机和队列绑定关系
*/
@Bean
public Binding orderBinding(@Qualifier("orderQueue") Queue queue,@Qualifier("orderExchange") Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(ORDER_ROUTING_KEY).noargs();
//return new Binding(QUEUE_NAME,Binding.DestinationType.QUEUE, EXCHANGE_NAME,"order.#", null);
}
/**
* 死信订单交换机
* @return
*/
@Bean(name = "dieOrderExchange")
public Exchange dieOrderExchange(){
return ExchangeBuilder.topicExchange(DIE_ORDER_EXCHANGE_NAME).durable(true).build();
}
/**
* 死信订单队列
* @return
*/
@Bean(name = "dieOrderQueue")
public Queue dieOrderQueue(){
return QueueBuilder.durable(DIE_ORDER_QUEUE_NAME).build();
}
/**
* 死信交换机和队列绑定关系
*/
@Bean
public Binding dieOrderBinding(@Qualifier("dieOrderQueue") Queue queue,@Qualifier("dieOrderExchange") Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(DIE_ORDER_ROUTING_KEY).noargs();
//return new Binding(QUEUE_NAME,Binding.DestinationType.QUEUE, EXCHANGE_NAME,"die.order.#", null);
}
}
/**
* 接受消息
*/
@Component
@Slf4j
public class OrderMQListener {
/**
* 死信队列监听
* @param msg
* @param message
* @throws IOException
*/
@RabbitHandler
@RabbitListener(queues = {"die_order_queue"})
public void overdueOrderListener(String msg, Message message, Channel channel) throws IOException {
long msgTag = message.getMessageProperties().getDeliveryTag();
log.info("msgTag={}", msgTag);
log.info("message={}", message.toString());
log.info("监听到消息:消息内容:{}", new String(message.getBody(), StandardCharsets.UTF_8));
try {
//todo 业务操作
//成功确认,使⽤此回执⽅法后,消息会被rabbitmq broker 删除
channel.basicAck(msgTag,false);
} catch (Exception e){
e.printStackTrace();
//业务失败,拒绝并重入队列 这里要限制重试次数,不然会一直重入队列
channel.basicNack(msgTag,false,true);
}
}
}
/**
* 发送消息
*/
@SpringBootTest
class DemoApplicationTests {
@Autowired
private RabbitTemplate template;
@Test
void send() {
template.convertAndSend(RabbitMQConfig.ORDER_EXCHANGE_NAME, "order.new", "您有一笔新的订单,请注意查看");
}
}
十、高可用集群
- RabbitMQ集群模式介绍
- 普通集群
- 默认集群模式, 比如有节点 node1和node2、node3,三个节点是普通集群,但是他们仅有相同的元数据,即交换机、队列的结构;
- 案例:
- 消息只存在其中的⼀个节点里面,假如消息A,存储在node1节点,消费者连接node1个节点消费消息时,可以直接取出来;但如果消费者是连接的是其他节点
那rabbitmq会把 queue 中的消息从存储它的节点中取出,并经过连接节点转发后再发送给消费者
- 消息只存在其中的⼀个节点里面,假如消息A,存储在node1节点,消费者连接node1个节点消费消息时,可以直接取出来;但如果消费者是连接的是其他节点
-
问题:
-
假如node1故障,那node2⽆法获取node1存储未被消费的消息;如果node1持久化后故障,那需要等node1恢复后才可以正常消费,如果ndoe1没做持久化后故障,那消息将会丢失,这个情况无法实现高可用性,且节点间会增加通讯获取消息,性能存在瓶颈
-
项目中springboot+amqp里面需要写多个节点的配置,比如下面spring.rabbitmq.addresses = 192.168.1.1:5672,192.168.1.2:5672,192.168.1.3:5672
-
该模式更适合于消息无需持久化的场景,如日志传输的队列
-
注意:集群需要保证各个节点有相同的token令牌 ,erlang.cookie是erlang的分布式token⽂件,集群内各个节点的erlang.cookie需要相同,才可以互相通信
-
- 镜像集群
- 队列做成镜像队列,让各队列存在于多个节点中,和普通集群比较大的区别就是【队列queue的消息message 】会在集群各节点之间同步,且并不是在 consumer 获取数据时临时拉取,而普通集群则是临时从存储的节点里面拉取对应的数据
- 结论:实现了高可用性,部分节点挂掉后,不影响正常的消费可以保证100%消息不丢失,推荐3个奇数节点,结合LVS+Keepalive进⾏IP漂移,防止单点故障
- 缺点:由于镜像队列模式下,消息数量过多,⼤量的消息同步也会加大网络带宽开销,适合高可用要求比较高的项⽬,过多节点的话,性能则更加受影响
- 注意:集群需要保证各个节点有相同的token令牌erlang.cookie是erlang的分布式token文件,集群内各个节点的erlang.cookie需要相同,才可以互相通信
- 普通集群
- 普通集群搭建
- 准备3个节点安装好rabbitmq,形成集群 (记得每个节点间隔几十秒再启动,如果失败删除宿主机文件重新搭建)
- 参数说明
- --hostname ⾃定义Docker容器的 hostname
- --link 容器之间连接,link不可或缺,使得三个容器能互相通信
- --privileged=true 使用该参数,container内的root拥有真正的root权限,否则容器出现permission denied
- -v 宿主机和容器路径映射
- 参数 RABBITMQ_NODENAME,缺省 Unix*: rabbit@$HOSTNAME
- 参数 RABBITMQ_DEFAULT_USER=admin
- 参数 RABBITMQ_DEFAULT_PASS=111111
- Erlang Cookie 值必须相同,也就是⼀个集群内RABBITMQ_ERLANG_COOKIE 参数的值必须相同, 相当于不同节点之间通讯的密钥,erlang.cookie是erlang的分布式 token文件,集群内各个节点的erlang.cookie需要相同,才可以互相通信
#三个容器启动
#节点⼀,主节点,创建-v映射⽬录
docker run -d --hostname rabbit_host1 --name rabbitmq1 -p 15672:15672 -p 5672:5672
-e RABBITMQ_NODENAME=rabbit
-e RABBITMQ_DEFAULT_USER=admin
-e RABBITMQ_DEFAULT_PASS=111111
-e RABBITMQ_ERLANG_COOKIE='rabbitmq_cookie_111111'
--privileged=true
-v /usr/local/rabbitmq/1/lib:/var/lib/rabbitmq
-v /usr/local/rabbitmq/1/log:/var/log/rabbitmq
rabbitmq:management
#节点⼆,创建-v映射⽬录
docker run -d --hostname rabbit_host2 --name rabbitmq2 -p 15673:15672 -p 5673:5672
--link rabbitmq1:rabbit_host1
-e RABBITMQ_NODENAME=rabbit
-e RABBITMQ_DEFAULT_USER=admin
-e RABBITMQ_DEFAULT_PASS=111111
-e RABBITMQ_ERLANG_COOKIE='rabbitmq_cookie_111111'
--privileged=true
-v /usr/local/rabbitmq/2/lib:/var/lib/rabbitmq
-v /usr/local/rabbitmq/2/log:/var/log/rabbitmq
rabbitmq:management
#节点三,创建-v映射⽬录
docker run -d --hostname rabbit_host3 --name rabbitmq3 -p 15674:15672 -p 5674:5672
--link rabbitmq1:rabbit_host1
--link rabbitmq2:rabbit_host2
-e RABBITMQ_NODENAME=rabbit
-e RABBITMQ_DEFAULT_USER=admin
-e RABBITMQ_DEFAULT_PASS=111111
-e RABBITMQ_ERLANG_COOKIE='rabbitmq_cookie_111111'
--privileged=true
-v /usr/local/rabbitmq/3/lib:/var/lib/rabbitmq
-v /usr/local/rabbitmq/3/log:/var/log/rabbitmq
rabbitmq:management
#进入三个容器中配置
节点⼀配置集群
docker exec -it rabbitmq1 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
exit
节点⼆加⼊集群,--ram是以内存⽅式加⼊,忽略该参数默认为磁盘节点。
docker exec -it rabbitmq2 bash
rabbitmqctl stop_app
rabbitmqctl join_cluster --ram rabbit@rabbit_host1
rabbitmqctl start_app
exit
节点三加⼊集群,--ram是以内存⽅式加⼊,忽略该参数默认为磁盘节点。
docker exec -it rabbitmq3 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbit_host1
rabbitmqctl start_app
exit
#查看集群节点状态,配置启动了3个节点,1个磁盘节点和2个内存节点
rabbitmqctl cluster_status
- 镜像集群搭建
- 背景:前面搭建了普通集群,如果磁盘节点挂掉后,如果没开启持久化数据就丢失了,其他节点也无法获取消息,所以基于普通集群方案在进⼀步改造为镜像模式集群。
- 策略policy介绍
- rabbitmq的策略policy是用来控制和修改集群的vhost队列和Exchange复制行为,就是要设置哪些Exchange或者queue的数据需要复制、同步,以及如何复制同步
-
创建⼀个策略来匹配队列
-
路径:rabbitmq管理页面 —> Admin —> Policies —> Add / update a policy
-
参数: 策略会同步同⼀个VirtualHost中的交换器和队列数据
-
name:⾃定义策略名称
-
Pattern:^ 匹配符,代表匹配所有
-
Definition:ha-mode=all 为匹配类型,分为3种模式:all(表示所有的queue)
-
ha-mode: 指明镜像队列的模式,可选下面的其中⼀个
-
all:表示在集群中所有的节点上进⾏镜像同步(⼀般都用这个参数)
-
exactly:表示在指定个数的节点上进行镜像同步,节点的个数由ha-params指定
-
nodes:表示在指定的节点上进行镜像同步,节点名称通过ha-params指定
-
-
ha-sync-mode:镜像消息同步方式 automatic(自动),manually(手动)
-
-
- 配置好后,+2的意思是有三个节点,⼀个节点本身和两个镜像节点, 且可以看到策略名称 xdclass_mirror
- 集群重启顺序
- 集群重启的顺序是固定的,并且是相反的
- 启动顺序:磁盘节点 => 内存节点
- 关闭顺序:内存节点 => 磁盘节点
- 最后关闭必须是磁盘节点,否则容易造成集群启动失败、数据丢失等异常情况