深浅模式
RabbitMQ
docker容器安装
sh
docker pull rabbitmq:3.8-management
运行RabbitMQ
sh
docker run \
-e RABBITMQ_DEFAULT_USER=admin \
-e RABBITMQ_DEFAULT_PASS=admin \
-v $PWD/rabbitmq-plugins:/plugins \
--name rabbitmq \
--hostname rabbitmq \
-p 15672:15672 \
-p 5672:5672 \
-d rabbitmq:3.8-management
SpringAMQP
依赖导入
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
基本消息队列
hello world
publisher
消息发布者,将消息发送到队列queuequeue
消息队列,负责接受并缓存消息consumer
订阅队列,处理队列中的消息
publisher
代码实现
java
public class PublisherTest {
@Test
public void SendMessage() throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.43.10");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("admin");
factory.setPassword("admin");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "hello world";
channel.queueDeclare(queueName, false, false, false, null);
// 4.发送消息
String message = "hello world";
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("发送消息成功:【" + message + "】");
// 5.关闭通道和连接
channel.close();
connection.close();
}
}
consumer
代码实现
java
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.43.10");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("admin");
factory.setPassword("admin");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.订阅消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body){
// 5.处理消息
String message = new String(body);
System.out.println("接收到消息:【" + message + "】");
}
});
System.out.println("等待接收消息。。。。");
}
}
RabbitMQ
连接配置
yaml
spring:
rabbitmq:
host: 192.168.43.10
port: 5672
virtual-host: /
username: admin
password: admin
AMQP
发送消息
java
@RunWith( SpringRunner.class )
@SpringBootTest
public class SpringAmqpTest {
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void sendMessage(){
String queueName = "hello world";
String message = "hello world";
rabbitTemplate.convertAndSend( queueName,message );
}
}
AMQP
消息监听
java
@Slf4j
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "hello world")
public void listenerQueueMessage(String msg){
log.info( "spring 消费着监听到 [ {} ]",msg );
}
}
RabbitMQ
默认发过去的消息使用的是jdk默认的序列化流
xml
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
配置序列化流
java
@Bean
public MessageConverter messageContext(){
return new Jackson2JsonMessageConverter();
}
demo
发送对象
java
/**
* 发送对象
*/
@Test
public void sendObjectMessage(){
Map<String, Object> map = new HashMap<>();
map.put( "name","张三" );
map.put( "age",18 );
rabbitTemplate.convertAndSend( "object.queue",map );
}
消息监听
java
@RabbitListener( queues = "object.queue" )
public void listenerObjectQueueMessage2( Map<String,Object> msg){
System.err.println( "object.queue : [ "+msg+" ] "+LocalDateTime.now() );
}
Work Queue工作队列
Work queues
也被称为**(Task queues)任务模型**,简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息,当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。使用work 模型,多个消费者共同处理消息处理,速度就能大大提高了
消费预取限制
yaml
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
消息监听
java
/**
* 监听work
*/
@RabbitListener(queues = "simple.queue")
public void listenerWorkQueueMessage1(String msg) throws InterruptedException {
System.out.println( "spring 消费着(1)监听到simple.queue : [ "+msg+" ] "+LocalDateTime.now() );
Thread.sleep( 20 );
}
@RabbitListener(queues = "simple.queue")
public void listenerWorkQueueMessage2(String msg) throws InterruptedException {
System.err.println( "spring 消费着(2)监听到simple.queue : [ "+msg+" ] "+LocalDateTime.now() );
Thread.sleep( 200 );
}
消息发送
java
@Test
public void sendWorkQueue(){
String queueName = "simple.queue";
String message = "message__";
for ( int i = 1; i <= 50; i++ ) {
rabbitTemplate.convertAndSend( queueName,message + i );
}
}
- 多个消费者绑定到一个队列,同一条消息只会被一个消费者处理
- 通过设置prefetch来控制消费者预取的消息数量
发布订阅
Publisher
:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给ExchangeExchange
:交换机,一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型
Fanout
:广播,将消息交给所有绑定到交换机的队列Direct
:定向,把消息交给符合指定routing key 的队列Topic
:通配符,把消息交给符合routing pattern(路由模式) 的队列Consumer
:消费者,与以前一样,订阅队列,没有变化Queue
:消息队列也与以前一样,接收消息、缓存消息
Exchange
(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失
Fanout Exchange
- 可以有多个队列
- 每个队列都要绑定到Exchange(交换机)
- 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定
- 交换机把消息发送给绑定过的所有队列
- 订阅队列的消费者都能拿到消息
声明队列和交换机
Spring提供了Exchange接口,来表示所有不同类型的交换机
在consumer中声明队列和交换机
java
@Configuration
public class FanoutConfig {
/**
* 交换机
*/
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange( "rabbitMQ.fanout" );
}
/**
* 声明队列
*/
@Bean("queue1")
public Queue fanoutQueue1(){
return new Queue( "fanout.queue1" );
}
@Bean("queue2")
public Queue fanoutQueue2(){
return new Queue( "fanout.queue2" );
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindingQueue1(Queue queue1,FanoutExchange fanoutExchange){
return BindingBuilder.bind( queue1 ).to( fanoutExchange );
}
@Bean
public Binding bindingQueue2(Queue queue2,FanoutExchange fanoutExchange){
return BindingBuilder.bind( queue2 ).to( fanoutExchange );
}
}
监听消息
java
/**
* 监听fanoutExchange
*/
@RabbitListener(queues = "fanout.queue1")
public void listenerFanoutQueueMessage1(String msg){
System.err.println( "spring 消费着(1)监听到fanout.queue : [ "+msg+" ] "+LocalDateTime.now() );
}
@RabbitListener(queues = "fanout.queue2")
public void listenerFanoutQueueMessage2(String msg){
System.out.println( "spring 消费着(2)监听到fanout.queue : [ "+msg+" ] "+LocalDateTime.now() );
}
发送消息
java
/**
* fanout发布消息
*/
@Test
public void sendFanoutMessage(){
String exchangeName = "rabbitMQ.fanout";
String message = "fanout message";
rabbitTemplate.convertAndSend( exchangeName,"",message );
}
- 接收publisher发送的消息
- 将消息按照规则路由到与之绑定的队列
- 不能缓存消息,路由失败,消息丢失
- FanoutExchange的会将消息路由到每个绑定的队列
Direct Exchange
将消息发送到指定的queue
- 每一个Queue都与Exchange设置一个
BindingKey
- 发布者发送消息时,指定消息的
RoutingKey
- Exchange将消息路由到
BindingKey
与消息RoutingKey
一致的队列
监听消息
java
/**
* 监听directExchange
*/
@RabbitListener( bindings = @QueueBinding (
value = @Queue( name = "direct.queue1" ),
exchange = @Exchange( name = "RabbitMQ.direct",type = ExchangeTypes.DIRECT ),
key = {"red","blue"}
))
public void listenerDirectQueueMessage1(String msg){
System.out.println( "spring 消费着(1)监听到direct.queue : [ "+msg+" ] "+LocalDateTime.now() );
}
@RabbitListener( bindings = @QueueBinding (
value = @Queue( name = "direct.queue2" ),
exchange = @Exchange( name = "RabbitMQ.direct",type = ExchangeTypes.DIRECT ),
key = {"yellow","blue"}
))
public void listenerDirectQueueMessage2(String msg){
System.err.println( "spring 消费着(2)监听到direct.queue : [ "+msg+" ] "+LocalDateTime.now() );
}
发送消息
java
/**
* direct发布消息
*/
@Test
public void sendDirectMessage(){
String exchangeName = "RabbitMQ.direct";
String message = "red message";
rabbitTemplate.convertAndSend( exchangeName,"blue",message );
}
- 队列与交换机的绑定,不能是任意绑定,而是要指定一个
RoutingKey
(路由key)- 消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey
- Exchange不再把消息交给每一个绑定的队列,而是根据消息的
Routing Key
进行判断,只有队列的Routingkey
与消息的Routing key
完全一致,才会接收到消息- 系统默认发布订阅方式
- 与Fanout交换机的差异
- Fanout交换机将消息路由给每一个与之绑定的队列
- Direct交换机根据RoutingKey判断路由给哪个队列
- 如果多个队列具有相同的RoutingKey,则与Fanout功能类似
Topic Exchange
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key
的时候使用通配符!Routingkey
一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: mq.send
# 匹配一个或多个词
mq.#
#.mq
***** 匹配一个词
*.mq
mq.*
消息监听
java
/**
* 监听topicExchange
*/
@RabbitListener( bindings = @QueueBinding (
value = @Queue( name = "topic.queue1" ),
exchange = @Exchange( name = "RabbitMQ.topic",type = ExchangeTypes.TOPIC ),
key = "china.#"
))
public void listenerTopicQueueMessage1(String msg){
System.out.println( "spring 消费着(1)监听到topic.queue1 : [ "+msg+" ] "+LocalDateTime.now() );
}
@RabbitListener( bindings = @QueueBinding (
value = @Queue( name = "topic.queue2" ),
exchange = @Exchange( name = "RabbitMQ.topic",type = ExchangeTypes.TOPIC ),
key = "#.news"
))
public void listenerTopicQueueMessage2(String msg){
System.err.println( "spring 消费着(2)监听到topic.queue2 : [ "+msg+" ] "+LocalDateTime.now() );
}
消息发送
java
/**
* topic发布消息
*/
@Test
public void sendTopicMessage(){
String exchangeName = "RabbitMQ.topic";
String message = "topic message";
rabbitTemplate.convertAndSend( exchangeName,".",message );
}
服务异步通信
生产者确认机制
消息从发送,到消费者接收,会经理多个过程,其中的每一步都可能导致消息丢失,常见的丢失原因包括:
- 发送时丢失:
- 生产者发送的消息未送达exchange
- 消息到达exchange后未到达queue
- MQ宕机,queue将消息丢失
- consumer接收到消息后未消费就宕机
RabbitMQ提供了publisher confirm
机制来避免消息发送到MQ过程中丢失。这种机制必须给每个消息指定一个唯一ID。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功
- publisher-confirm,发送者确认
- 消息成功投递到交换机,返回ack
- 消息未投递到交换机,返回nack
- publisher-return,发送者回执
- 消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因
确认机制发送消息时,需要给每个消息设置一个全局唯一的id
,以区分不同消息,避免ack
冲突
SpringAMQP
实现生产者确认
配置publisher
yaml
spring:
rabbitmq:
publisher-confirm-type: correlated
publisher-returns: true
template:
mandatory: true
publish-confirm-type
:开启publisher-confirm
simple
:同步等待confirm结果,直到超时correlated
:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallbackpublish-returns
:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallbacktemplate.mandatory
:消息路由失败时的策略true,则调用ReturnCallback;false:则直接丢弃消息
定义Return
回调
每个RabbitTemplate
只能配置一个ReturnCallback
,因此需要在项目加载时配置
java
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取RabbitTemplate对象
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
// 配置ReturnCallback
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
// 记录日志
log.error("消息发送到队列失败,响应码:{}, 失败原因:{}, 交换机: {}, 路由key:{}, 消息: {}",
replyCode, replyText, exchange, routingKey, message);
// 如果有需要的话,重发消息,人工处理?
});
}
}
定义ConfirmCallBack
java
@Test
public void sendMessagesSimpleQueue() throws InterruptedException {
//key
String routingKey = "simple.msg";
//消息
String message = "hello,spring amqp";
//交换机
String exchange = "simple.topic";
//添加correlationData设置id
CorrelationData correlationData = new CorrelationData( UUID.randomUUID().toString() );
//回调
correlationData.getFuture().addCallback( confirm -> {
//判断ack
if ( confirm.isAck() ) {
log.info( "消息发送到交换机,消息ID: {}",correlationData.getId() );
}else {
//nack
log.error( "消息投送到交换机失败,消息ID: {}",correlationData.getId() );
}
}, throwable -> {
log.error( "消息发送失败" );
//重发?
} );
//发送消息
rabbitTemplate.convertAndSend( exchange,routingKey,message,correlationData );
//当测试方法结束,rabbitmq相关的资源也就关闭了,
// 消息发送出去,但异步的ConfirmCallback却由于资源关闭而出现了发送交换机失败的的问题
Thread.sleep( 300 );
}
消息持久化
生产者确认可以确保消息投递到RabbitMQ
的队列中,但是消息发送到RabbitMQ
以后,如果突然宕机,也可能导致消息丢失
交换机持久化
java
@Bean
public DirectExchange simpleDirect(){
//交换机名称,是否持久化,当没有队列与其绑定时是否自动删除
return new DirectExchange("simple.direct",true,false);
}
队列持久化
java
@Bean
public Queue simpleQueue(){
return QueueBuilder.durable("simple.queue").build();
}
消息持久化
java
/**
* 持久化消息
*/
@Test
public void durableMessage(){
//构建持久化消息
Message message = MessageBuilder.withBody( "hello,spring".getBytes(StandardCharsets.UTF_8) )
.setDeliveryMode( MessageDeliveryMode.PERSISTENT )
.build();
rabbitTemplate.convertAndSend( "simple.queue",message );
}
⭐ 在RabbitMQ中交换机,队列和消息默认都是持久化的
消费者消息确认
RabbitMQ
是阅后即焚机制,RabbitMQ
确认消息被消费者消费后会立刻删除。而RabbitMQ
是通过消费者回执来确认消费者是否成功处理消息的,消费者获取消息后,应该向RabbitMQ
发送ACK
回执,表明自己已经处理消息SpringAMQP
允许配置三种确认模式
manual
手动ack,需要在业务代码结束后,调用api发送ack(代码侵入)auto
自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack(AOP)none
关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除
配置auto
模式
yaml
spring:
rabbitmq:
listener:
simple:
prefetch: 1
acknowledge-mode: auto
消费失败重试机制
当消费者出现异常后,消息会不断requeue(重入队)
到队列,再重新发送给消费者,然后再次异常,再次requeue
,无限循环,导致mq的消息处理飙升,带来不必要的压力
==本地方法重试==
利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列
yaml
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000 # 初识的失败等待时长为1秒
multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
- 开启本地重试时,消息处理过程中抛出异常,不会requeue到队列,而是在消费者本地重试
- 重试达到最大次数后,Spring会返回ack,消息会被丢弃
==失败策略==
开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecovery
接口来处理,它包含三种不同
RejectAndDontRequeueRecoverer
重试耗尽后,直接reject
,丢弃消息。默认就是这种方式ImmediateRequeueMessageRecoverer
重试耗尽后,返回nack
,消息重新入队RepublishMessageRecoverer
重试耗尽后,将失败消息投递到指定的交换机
配置处理消费失败的交换机和队列
java
@Bean
public DirectExchange errorMessageExchange(){
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
return new Queue("error.queue", true);
}
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
}
配置消费重试策略
java
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
==保证消息的可靠性?==
- 开启生产者确认机制,确保生产者的消息能到达队列
- 开启持久化功能,确保消息未消费前在队列中不会丢失
- 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
- 开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理
死信交换机
当一个队列中的消息满足下列情况之一时,可以成为死信dead letter
- 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
- 消息是一个过期消息,超时无人消费
- 要投递的队列消息满了,无法投递
如果这个包含死信的队列配置了dead-letter-exchange
属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机Dead Letter Exchange
==TTL==
一个队列中的消息如果超时未消费,则会变为死信,超时分为两种情况:
- 消息所在的队列设置了超时时间
- 消息本身设置了超时时间
监听死信队列
java
@RabbitListener( bindings = @QueueBinding(
value = @Queue( name = "dl.queue",durable = "true"),
exchange = @Exchange( name = "dl.exchange"),
key = "dl"
))
public void listenDeadLetterQueue(String msg){
log.info( "监听到消息: {}",msg );
}
声明队列
java
@Bean
public DirectExchange ttlExchange(){
return new DirectExchange( "ttl.exchange",true,false );
}
@Bean
public Queue ttlQueue(){
return QueueBuilder.durable( "ttl.queue" )
.deadLetterExchange( "dl.exchange" )
.deadLetterRoutingKey( "dl" )
.ttl( 10000 )
.build();
}
@Bean
public Binding binding(){
return BindingBuilder.bind( ttlQueue() ).to( ttlExchange() ).with( "ttl" );
}
延迟队列
利用TTL结合死信交换机,我们实现了消息发出后,消费者延迟收到消息的效果。这种消息模式就称为延迟队列(Delay Queue)模式,延迟队列的需求非常多,所以RabbitMQ的官方也推出了一个DelayExchange
插件,原生支持延迟队列效果
延迟队列的使用场景包括:
- 延迟发送短信
- 用户下单,如果用户在15 分钟内未支付,则自动取消
- 预约工作会议,20分钟后自动通知所有参会人员
==下载插件后将插件上传到mq容器数据卷位置,并进入mq容器内开启插件==
sh
# 进入容器内部
docker exec -it rabbitmq bash
#开启插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
DelayExchange需要将一个交换机声明为delayed类型。当我们发送消息到delayExchange时,流程如下:
- 接收消息
- 判断消息是否具备x-delay属性
- 如果有x-delay属性,说明是延迟消息,持久化到硬盘,读取x-delay值,作为延迟时间
- 返回routing not found结果给消息发送者
- x-delay时间到期后,重新投递消息到指定队列
声明DelayExchange
交换机
java
@RabbitListener( bindings = @QueueBinding(
value = @Queue( "delay.queue" ),
exchange = @Exchange( value = "delay.exchange",delayed = "true"),
key = "delay"
) )
public void listenDelayMessages(String msg){
log.info( "延迟队列监听到消息: {}",msg );
}
发送消息
java
@Test
public void sendDelayedMessages(){
Message message = MessageBuilder.withBody( "hello".getBytes() )
.setDeliveryMode( MessageDeliveryMode.PERSISTENT )
.setHeader( "x-delay",5000 )
.build();
CorrelationData correlationData = new CorrelationData( UUID.randomUUID().toString() );
rabbitTemplate.convertAndSend( "delay.exchange","delay",message,correlationData );
}
惰性队列
==消息堆积==
当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是消息堆积问题
消息堆积问题的解决方案?
- 队列上绑定多个消费者,提高消费速度
- 使用惰性队列,可以再mq中保存更多消息
==惰性队列==
- 接收到消息后直接存入磁盘而非内存
- 消费者要消费消息时才会从磁盘中读取并加载到内存
- 支持数百万条的消息存储
优点
- 基于磁盘存储,消息上限高
- 没有间歇性的page-out,性能比较稳定
缺点
- 基于磁盘存储,消息时效性会降低
- 性能受限于磁盘的IO
基于命令行设置lazy-queue
sh
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues
rabbitmqctl
RabbitMQ的命令行工具set_policy
添加一个策略Lazy
策略名称,可以自定义"^lazy-queue$"
用正则表达式匹配队列的名字'{"queue-mode":"lazy"}'
设置队列模式为lazy模式--apply-to queues
策略的作用对象,是所有的队列
基于@Bean
声明lazy-queue
java
@Bean
public Queue lazyQueue() {
return QueueBuilder.durable("lazy.queue")
.lazy()
.build();
}
基于@RabbitListener
声明lazy-queue
java
@RabbitListener( queuesToDeclare = @Queue(
name = "lazy.queue",
durable = "true",
arguments = @Argument( name = "x-queue-mode",value = "lazy")
))
public void listenLazyQueue(String msg){
log.info( "惰性队列监听到消息: {}",msg );
}
集群
普通集群,或者叫标准集群(classic cluster)
- 会在集群的各个节点间共享部分数据,包括:交换机、队列元信息。不包含队列中的消息
- 当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回
- 队列所在节点宕机,队列中的消息就会丢失
==普通集群部署==
获取cookie
RabbitMQ底层依赖于Erlang,而Erlang虚拟机就是一个面向分布式的语言,默认就支持集群模式。集群模式中的每个RabbitMQ 节点使用 cookie 来确定它们是否被允许相互通信
获取任意一个mq容器的cookie值
shdocker exec -it rabbitmq cat /var/lib/rabbitmq/.erlang.cookie
记录cookie值
shYXDTBEFTLPBBPUGFFWTJ
准备集群配置
创建目录存放集群配置信息
shmkdir mq-cluster
编写
rabbitmq.conf
文件shloopback_users.guest = false listeners.tcp.default = 5672 cluster_formation.peer_discovery_backend = rabbit_peer_discovery_classic_config cluster_formation.classic_config.nodes.1 = rabbit@mq1 cluster_formation.classic_config.nodes.2 = rabbit@mq2 cluster_formation.classic_config.nodes.3 = rabbit@mq3
创建文件记录cookie
sh# 创建cookie文件 touch .erlang.cookie # 写入cookie echo "YXDTBEFTLPBBPUGFFWTJ" > .erlang.cookie # 修改cookie文件的权限 chmod 600 .erlang.cookie
准备三个目录,mq1、mq2、mq3,并将配置信息拷贝入
sh# 创建目录 mkdir mq1 mq2 mq3 # 拷贝 cp rabbitmq.conf mq1 cp rabbitmq.conf mq2 cp rabbitmq.conf mq3 cp .erlang.cookie mq1 cp .erlang.cookie mq2 cp .erlang.cookie mq3
启动集群
创建网络,用于节点之间互联
shdocker network create mq-net
创建运行容器
shdocker run -d --net mq-net \ -v ${PWD}/mq1/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \ -v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \ -e RABBITMQ_DEFAULT_USER=admin \ -e RABBITMQ_DEFAULT_PASS=admin \ --name mq1 \ --hostname mq1 \ -p 8071:5672 \ -p 8081:15672 \ rabbitmq:3.8-management
查看启动状态
登录mq1节点查看
镜像集群
- 交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份
- 创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点
- 一个队列的主节点可能是另一个队列的镜像节点
- 所有操作都是主节点完成,然后同步给镜像节点
- 主宕机后,镜像节点会替代成新的主
==镜像集群部署==
镜像模式的配置有3种模式:
ha-mode | ha-params | 效果 |
---|---|---|
准确模式exactly | 队列的副本量count | 集群中队列副本(主服务器和镜像服务器之和)的数量。count如果为1意味着单个副本:即队列主节点。count值为2表示2个副本:1个队列主和1个队列镜像。换句话说:count = 镜像数量 + 1。如果群集中的节点数少于count,则该队列将镜像到所有节点。如果有集群总数大于count+1,并且包含镜像的节点出现故障,则将在另一个节点上创建一个新的镜像。 |
all | (none) | 队列在群集中的所有节点之间进行镜像。队列将镜像到任何新加入的节点。镜像到所有节点将对所有群集节点施加额外的压力,包括网络I / O,磁盘I / O和磁盘空间使用情况。推荐使用exactly,设置副本数为(N / 2 +1)。 |
nodes | node names | 指定队列创建到哪些节点,如果指定的节点全部不存在,则会出现异常。如果指定的节点在集群中存在,但是暂时不可用,会创建节点到当前客户端连接到的节点。 |
exactly模式
sh
rabbitmqctl set_policy ha-two "^two\." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'
rabbitmqctl set_policy
:固定写法ha-two
:策略名称,自定义"^two\."
:匹配队列的正则表达式,符合命名规则的队列才生效,这里是任何以two.
开头的队列名称'{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'
: 策略内容"ha-mode":"exactly"
:策略模式,此处是exactly模式,指定副本数量"ha-params":2
:策略参数,这里是2,就是副本数量为2,1主1镜像"ha-sync-mode":"automatic"
:同步策略,默认是manual,即新加入的镜像节点不会同步旧的消息。如果设置为automatic,则新加入的镜像节点会把主节点中所有消息都同步,会带来额外的网络开销
all模式
sh
rabbitmqctl set_policy ha-all "^all\." '{"ha-mode":"all"}'
ha-all
:策略名称,自定义"^all\."
:匹配所有以all.
开头的队列名'{"ha-mode":"all"}'
:策略内容"ha-mode":"all"
:策略模式,此处是all模式,即所有节点都会称为镜像节点
nodes模式
sh
rabbitmqctl set_policy ha-nodes "^nodes\." '{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}'
rabbitmqctl set_policy
:固定写法ha-nodes
:策略名称,自定义"^nodes\."
:匹配队列的正则表达式,符合命名规则的队列才生效,这里是任何以nodes.
开头的队列名称'{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}'
: 策略内容"ha-mode":"nodes"
:策略模式,此处是nodes模式"ha-params":["rabbit@mq1", "rabbit@mq2"]
:策略参数,这里指定副本所在节点名称
仲裁队列
==创建仲裁队列==
java
@Bean
public Queue quorumQueue() {
return QueueBuilder
.durable("quorum.queue") // 持久化
.quorum() // 仲裁队列
.build();
}
==连接MQ集群==
java
spring:
rabbitmq:
addresses: 192.168.150.105:8071, 192.168.150.105:8072, 192.168.150.105:8073
username: mq
password: admin
virtual-host: /
集群扩容
启动一个新的MQ容器
shdocker run -d --net mq-net \ -v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \ -e RABBITMQ_DEFAULT_USER=admin \ -e RABBITMQ_DEFAULT_PASS=admin \ --name mq4 \ --hostname mq5 \ -p 8074:15672 \ -p 8084:15672 \ rabbitmq:3.8-management
进入容器
shdocker exec -it mq4 bash #停止mq进程 rabbitmqctl stop_app #重置RabbitMQ中的数据 rabbitmqctl reset #加入mq1 rabbitmqctl join_cluster rabbit@mq1 #再次启动mq进程 rabbitmqctl start_app
增加仲裁队列副本
shdocker exec -it mq1 bash rabbitmq-queues add_member "quorum.queue" "rabbit@mq4" rabbitmq-queues quorum_status "quorum.queue"