1、死信队列
1.1、死信的概念
先从概念上解释,搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,Procedure 将消息投递到 Broker 或者 直接到 Queue 里面了,Consumer 从Queue 取出消息进行消费,但是某些时候由于特定的原因导致 Queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然而然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常的时候,将消息投入死信队列中,还有比如说:用户再商城下单成功并点击去支付后再指定的时间未支付时自动失效。
1.2、死信的来源
- 消息 TTL(最大存活时间) 过期
- 队列达到最大长度(队列满了,无法再添加数据到 MQ 中)
- 消息被拒绝(basic.reject 或者 basic.nack) 并且 requeue = false
死信队列实战01 ==> 消息 TTL 过期
生产者
/**
* @author wcc
* @date 2021/11/11 15:55
* 死信队列的生产者代码
*/
public class Producer {
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtils.getChannel();
//死信消息,设置TTL时间 最大存活时间
AMQP.BasicProperties properties =
new AMQP.BasicProperties().builder()
.expiration("10000").build();
for (int i = 1; i < 11; i++) {
String message = "info" + i;
channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties,message.getBytes("UTF-8"));
}
}
}
正常消息消费者
/**
* @author wcc
* @date 2021/11/11 15:04
* 死信队列 实战
* 消费者01
*/
public class Consume01 {
// 普通交换机的名称
public static final String NORMAL_EXCHANGE = "normal_exchange";
// 死信交换机的名称
public static final String DEAD_EXCHANGE = "dead_exchange";
// 普通队列的名称
public static final String NORMAL_QUEUE = "normal_queue";
// 死信队列的名称
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtils.getChannel();
// 声明死信和普通的交换机,类型为 direct
channel.exchangeDeclare(NORMAL_QUEUE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);
// 声明普通队列
Map<String,Object> arguments = new HashMap<>();
// 过期时间 设置为10秒钟
arguments.put("x-message-ttl",10000); //注意 以毫秒为单位
//正常的队列中的消息过期之后设置死信交换机 注意 key x-dead-letter-exchange是固定的
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信交换机的routingKey
arguments.put("x-dead-letter-routing-key","lisi");
channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);
//声明死信队列
channel.queueDeclare(DEAD_EXCHANGE,false,false,false,null);
//绑定普通的交换机与队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");
//绑定死信的交换机与死信的队列
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
System.out.println("Consume01等待接收消息...");
DeliverCallback deliverCallback = (consumeTag,message) -> {
System.out.println("Consume01接收的消息是:"+new String(message.getBody(),"UTF-8"));
};
channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,(consumeTag)->{});
}
}
死信消费者
/**
* @author wcc
* @date 2021/11/11 15:04
* 死信队列 实战
* 消费者02
*/
public class Consume02 {
// 死信队列的名称
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtils.getChannel();
System.out.println("Consume02等待接收消息...");
DeliverCallback deliverCallback = (consumeTag,message) -> {
System.out.println("Consume02接收的消息是:"+new String(message.getBody(),"UTF-8"));
};
channel.basicConsume(DEAD_QUEUE,true,deliverCallback,(consumeTag)->{});
}
}
死信队列实战02 ==> 队列达到最大长度
生产者代码
/**
* @author wcc
* @date 2021/11/11 15:55
* 死信队列的生产者代码
*/
public class Producer_MaxLength {
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtils.getChannel();
for (int i = 1; i < 11; i++) {
String message = "info" + i;
channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",null,message.getBytes("UTF-8"));
}
}
}
消费者1代码
/**
* @author wcc
* @date 2021/11/11 15:04
* 死信队列 实战
* 消费者01
*/
public class Consume01_MaxLength {
// 普通交换机的名称
public static final String NORMAL_EXCHANGE = "normal_exchange";
// 死信交换机的名称
public static final String DEAD_EXCHANGE = "dead_exchange";
// 普通队列的名称
public static final String NORMAL_QUEUE = "normal_queue";
// 死信队列的名称
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtils.getChannel();
// 声明死信和普通的交换机,类型为 direct
channel.exchangeDeclare(NORMAL_EXCHANG, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);
// 声明普通队列
Map<String,Object> arguments = new HashMap<>();
// 过期时间 设置为10秒钟
//arguments.put("x-message-ttl",10000); //注意 以毫秒为单位
//正常的队列中的消息过期之后设置死信交换机 注意 key x-dead-letter-exchange是固定的
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信交换机的routingKey
arguments.put("x-dead-letter-routing-key","lisi");
arguments.put("x-max-length",6);
channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);
//声明死信队列
channel.queueDeclare(DEAD_EXCHANGE,false,false,false,null);
//绑定普通的交换机与队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");
//绑定死信的交换机与死信的队列
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
System.out.println("Consume01等待接收消息...");
DeliverCallback deliverCallback = (consumeTag,message) -> {
System.out.println("Consume01接收的消息是:"+new String(message.getBody(),"UTF-8"));
};
channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,(consumeTag)->{});
}
}
死信队列实战03 ===> 消息被拒绝
正常消费者代码
/**
* @author wcc
* @date 2021/11/11 15:04
* 死信队列 实战
* 消费者01
*/
public class Consume01_Reject {
// 普通交换机的名称
public static final String NORMAL_EXCHANGE = "normal_exchange";
// 死信交换机的名称
public static final String DEAD_EXCHANGE = "dead_exchange";
// 普通队列的名称
public static final String NORMAL_QUEUE = "normal_queue";
// 死信队列的名称
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtils.getChannel();
// 声明死信和普通的交换机,类型为 direct
channel.exchangeDeclare(NORMAL_QUEUE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);
// 声明普通队列
Map<String,Object> arguments = new HashMap<>();
// 过期时间 设置为10秒钟
//arguments.put("x-message-ttl",10000); //注意 以毫秒为单位
//正常的队列中的消息过期之后设置死信交换机 注意 key x-dead-letter-exchange是固定的
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信交换机的routingKey
arguments.put("x-dead-letter-routing-key","lisi");
channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);
//声明死信队列
channel.queueDeclare(DEAD_EXCHANGE,false,false,false,null);
//绑定普通的交换机与队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");
//绑定死信的交换机与死信的队列
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
System.out.println("Consume01_Reject等待接收消息...");
DeliverCallback deliverCallback = (consumeTag,message) -> {
String messageExample = new String(message.getBody(),"UTF-8");
//模拟消息被拒绝
if(messageExample.equalsIgnoreCase("info5")){
System.out.println("Consume01_Reject接收的消息是" + messageExample + "被C1拒绝的");
/**
* 第一个参数代表该消息的标识符
* 第二个参数代表拒绝后不放回队列
*/
channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
}else{
System.out.println("Consume01_Reject接收的消息是:"+ messageExample);
//这里第二个参数代表是否批量处理
channel.basicAck(message.getEnvelope().getDeliveryTag(),true);
}
};
//发生之前一定要开启手动应答
channel.basicConsume(NORMAL_QUEUE,false,deliverCallback,(consumeTag)->{});
}
}
2、延迟队列
2.1、延迟队列的概念
延迟队列,队列内部是有序的,最重要的特性就是体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或者之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
2.2、延迟队列使用场景
- 订单在十分钟之内未支付则自动取消
- 新创建的商铺,如果在十天内都没有上传过商品,则自动发消息提醒。
- 用户注册成功后,如果三天内没有登陆则进行短信提醒。
- 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
- 预定会议后,需要在预定的时间点前10分钟通知各个与会人员参加会议。
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理就好了吗?
如果数据量比较少,确实可以这样做,比如:对于 “账单一周内未支付则进行自动结算” 这样的需求,如果对于事件不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检察一下所有未支付的账单,确实也是一个可行的方案。
但是对于数据量比较大,并且时效性较强的场景,如: “订单十分钟内未支付则关闭” ,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有的订单的检查,同时会给数据库带来很大的压力,无法满足业务要求而且性能低下。
2.3、队列 TTL(延时队列)
2.3.1、代码架构图
创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如下:
整合 SpringBoot 的方式实现延迟队列
TTL 延时队列的配置类
总结以下:根据上图可知道,我们这个案例的效果应该是这样的,X 是普通交换机,Y 是死信交换机,QA、QB 是普通队列,QD 是死信队列,我们这里生产者发送的消息没有消费者进行接收,在等待 TTL 的时间后,会自动进入死信队列。
/**
* @author wcc
* @date 2021/11/15 11:04
* @Description:
* TTL队列 配置文件类代码
*/
@Configuration
public class TtlQueueConfig {
// 普通交换机的名称
public static final String X_EXCHANGE = "X";
// 死信交换机的名称
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
// 普通队列的名称
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
// 死信队列的名称
public static final String DEAD_LETTER_QUEUE = "QD";
// 声明普通交换机 xExchange 别名
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
// 声明死信交换机名称 yExchange 别名
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
// 声明普通队列 TTL为10秒钟
@Bean("queueA")
public Queue queueA(){
Map<String,Object> arguments = new HashMap<>(3);
// 设置默认参数
// 设置死信交换机
arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
// 设置死信Routing key
arguments.put("x-dead-letter-routing-key","YD");
//设置过期时间 单位是ms
arguments.put("x-message-ttl",10000);
return QueueBuilder.durable(QUEUE_A)
.withArguments(arguments)
// .singleActiveConsumer() 设置是否只能一个消费者进行消费
// .autoDelete() 设置是否删除
.build();
}
// 声明普通队列 TTL为40秒钟
@Bean("queueB")
public Queue queueB(){
Map<String,Object> arguments = new HashMap<>(3);
// 设置默认参数
// 设置死信交换机
arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
// 设置死信Routing key
arguments.put("x-dead-letter-routing-key","YD");
//设置过期时间 单位是ms
arguments.put("x-message-ttl",40000);
return QueueBuilder.durable(QUEUE_B)
.withArguments(arguments)
// .singleActiveConsumer() 设置是否只能一个消费者进行消费
// .autoDelete() 设置是否删除
.build();
}
//死信队列
@Bean("queueD")
public Queue queueD(){
return QueueBuilder.durable(DEAD_LETTER_QUEUE)
.build();
}
// 队列QA绑定普通交换机xExchange
@Bean
// 注意 @Qualifier 注解可以被用在单个构造器或者方法的参数上,当上下文有几个相同类型的Bean
// 使用 @Autowired 则无法区分要绑定的 bean,此时可以使用 @Qualifier 注解来指定名称
public Binding queueABingbingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
// 队列QB绑定普通交换机xExchange
@Bean
// 注意 @Qualifier 注解可以被用在单个构造器或者方法的参数上,当上下文有几个相同类型的Bean
// 使用 @Autowired 则无法区分要绑定的 bean,此时可以使用 @Qualifier 注解来指定名称
public Binding queueBBingbingX(@Qualifier("queueB") Queue queueB,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueB).to(xExchange).with("XB");
}
// 队列QD绑定死信交换机yExchange
@Bean
// 注意 @Qualifier 注解可以被用在单个构造器或者方法的参数上,当上下文有几个相同类型的Bean
// 使用 @Autowired 则无法区分要绑定的 bean,此时可以使用 @Qualifier 注解来指定名称
public Binding queueDBingbingY(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange yExchange){
return BindingBuilder.bind(queueD).to(yExchange).with("YD");
}
}
生产者
/**
* @author wcc
* @date 2021/11/15 11:45
* @Description :
* 发送延迟消息
*/
@RestController
@RequestMapping("/ttl")
@Slf4j
public class SendMessageController {
@Autowired
private RabbitTemplate rabbitTemplate;
//生产者开始发消息
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable String message){
log.info("当前时间:{},发送一条信息给两个TTL队列:{}",new Date().toString(),message);
rabbitTemplate.convertAndSend("X","XA","消息来自TTL为10秒的队列:"+message);
rabbitTemplate.convertAndSend("X","XB","消息来自TTL为40秒的队列:"+message);
}
}
死信消费者
/**
* @author wcc
* @date 2021/11/15 13:43
* 队列TTL 消费者
*/
@Slf4j
@Component
public class DeadLetterQueueConsumer {
//接收消息
@RabbitListener(queues = "QD")
public void receiveQD(Message message, Channel channel) throws Exception{
String messages = new String(message.getBody(),"UTF-8");
log.info("当前时间:{},收到死信队列消息:{}",new Date().toString(),messages);
}
}
结果如下:
第一条消息在 10S 后变成了死信消息,然后被消费者消费掉,第二条消息在 40S 后变成了死信消息,然后被消费掉,这样一个延迟队列就完成了。
问题:不过,如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有 10S 和 20S 两个时间选项,如果需要一个小时后处理,那么就需要增加 TTL 为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?
2.4、延时队列优化
2.4.1、代码架构图
在这里增加了一个队列QC,绑定关系如下,该队列不设置 TTL 时间。
在 TTL 延时队列的配置类中添加如下
// 普通队列的名称
public static final String QUEUE_C = "QC";
// 声明普通队列QC
@Bean("queueC")
public Queue queueC(){
Map<String,Object> arguments = new HashMap<>();
// 设置死信交换机
arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
// 设置死信的routingKey
arguments.put("x-dead-letter-routing-key","YD");
return QueueBuilder.durable(QUEUE_C)
.withArguments(arguments)
// .singleActiveConsumer() 设置是否只能一个消费者进行消费
// .autoDelete() 设置是否自动删除
.build();
}
// 队列QC绑定普通交换机xExchange
@Bean
// 注意 @Qualifier 注解可以被用在单个构造器或者方法的参数上,当上下文有几个相同类型的Bean
// 使用 @Autowired 则无法区分要绑定的 bean,此时可以使用 @Qualifier 注解来指定名称
public Binding queueCBingbingX(@Qualifier("queueC") Queue queueC,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueC).to(xExchange).with("XC");
}
生产者发送消息并设置想要延时的时间
@GetMapping("/sendExpirationMsg/{message}/{ttlTime}")
public void sendMessage(@PathVariable String message, @PathVariable String ttlTime){
log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给一个 TTL 队列QC:{}",
new Date().toString(), ttlTime, message);
rabbitTemplate.convertAndSend("X","XC",message,messages->{
// 设置发送消息时候的延时时长
messages.getMessageProperties().setExpiration(ttlTime);
return messages;
});
}
测试结果如下:
看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用消息属性上设置 TTL 的方式,消息可能并不会按时死亡,因为 RabbitMQ只会检查第一个消息是否过期,如果过期则丢到死信队列中去,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。
2.5、RabbitMQ 插件实现延时队列
上面的在生产者消息属性上设置 TTL 的方式会造成消息排队的情况,不能实现消息粒度上的 TTL,并使其在设置的 TTL 时间及时死亡,无法设计成一个通用的延时队列,下面使用 RabbitMQ 插件实现的延时队列可以解决这个问题。
在官网上下载插件:rabbitmq-delayed-message-exchange-3.9.0.ez下载地址
这里我是用的 FinalShell
软件上传的,且上传到了 home
目录下。
安装步骤(这里使用 Docker ):
# 启动 rabbitmq 容器 隔行看没这么乱
docker run -it -p 15672:15672 -p 5672:5672
-e RABBITMQ_DEFAULT_USER=admin
-e RABBITMQ_DEFAULT_PASS=admin
--name rabbitmq
-v /rabbitmq/etc:/etc/rabbitmq # 挂载 rabbitmq 配置文件
-v /rabbitmq/lib:/var/lib/rabbitmq # 挂载 rabbitmq 数据信息
-v /rabbitmq/log:/var/log/rabbitmq # 挂载 rabbitmq 日志记录
--hostname=rabbitmqhostone
rabbitmq:management /bin/bash
# 第一种挂载的方式
docker run -it -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin --name rabbitmq -v /rabbitmq/etc:/etc/rabbitmq -v /rabbitmq/lib:/var/lib/rabbitmq -v /rabbitmq/log:/var/log/rabbitmq --hostname=rabbitmqhostone rabbitmq:management /bin/bash
# 第二种不进行挂载的方式
docker run -d -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin --name rabbitmq --hostname=rabbitmqhostone rabbitmq:management
# 拷贝到 rabbitMQ 容器 fca095a805b8(容器id号)中
docker cp /home/rabbitmq_delayed_message_exchange-3.9.0.ez 4e39e3307e50:/plugins
# 进入 rabbitmq 容器中
docker exec -it fca095a805b8 /bin/bash
# 启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
rabbitmq-plugins disable rabbitmq_delayed_message_exchange
# 查看插件是否安装成功
rabbitmq-plugins list
# 退出容器 后台运行并退出
ctrl + P + Q
# 重新启动容器
docker restart 4e39e3307e50
新建基于插件的延迟队列配置类
/**
* @author wcc
* @date 2021/11/16 10:34
* @Description:
* 延迟交换机
*/
@Configuration
public class DelayedQueueConfig {
// 队列
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
// 交换机
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
// Routing Key
public static final String DELAYED_ROUTING_KEY = "delayed.routingKey";
// 声明队列
@Bean
public Queue delayedQueue(){
return new Queue(DELAYED_QUEUE_NAME);
}
//声明交换机 基于插件的
@Bean
public CustomExchange dealyedExchange(){
Map<String,Object> arguments = new HashMap<>();
arguments.put("x-delayed-type","direct"); // 声明延迟类型
/**
* 1.交换机的名称
* 2.交换机的类型
* 3.是否需要持久化
* 4.是否需要自动删除
* 5.其他参数
*/
return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,arguments);
}
// 声明绑定
@Bean
public Binding delayedQueueBindDelayedExchange(@Qualifier("dealyedExchange") CustomExchange dealyedExchange,
@Qualifier("delayedQueue") Queue delayedQueue){
return BindingBuilder.bind(delayedQueue).to(dealyedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
基于插件的生产者
//开始发消息 基于插件的消息以及延迟的时间
@GetMapping("/sendDelayedMsg/{message}/{delayedTime}")
public void sendDelayedMessage(@PathVariable String message,@PathVariable Integer delayedTime){
log.info("当前时间:{},发送一条时长{}毫秒 Dealyed 信息给一个延迟队列 delayed.queue:{}",
new Date().toString(), delayedTime, message);
rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME,DelayedQueueConfig.DELAYED_ROUTING_KEY,
message,message1 -> {
//发送消息的时候 延迟时长 单位 ms
message1.getMessageProperties().setDelay(delayedTime);
return message1;
});
}
消费者
/**
* @author wcc
* @date 2021/11/16 11:05
*/
@Component
@Slf4j
public class DelayedQueueConsumer {
// 监听消息
@RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
public void receiveDelayed(Message message, Channel channel) throws Exception{
String messages = new String(message.getBody(),"UTF-8");
log.info("当前时间:{},收到延时队列消息:{}",new Date().toString(),messages);
}
}
结果如下:
这里,先后发送一个 20 秒的延迟消息和 2 秒的延迟消息,当使用交换机延迟的时候(即配置交换机类型为:x-delayed-message),这里队列的消息不会再发生阻塞,2 秒延迟的消息先被消费解决了上面延时队列优化留下的问题。
2.6、总结
延时队列在需要延时处理的场景下非常有用,使用 TRabbitMQ 来实现延时队列可以很好的利用 RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabboitMQ 集群的特性,可以很好的解决单点故障问题
,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。
当然,延时队列还有很多其他选择,比如利用 JAVA 的 DelayedQueue,利用 Redis 的 zset,利用 Quartz 或者利用 Kafka 的时间轮,这些方式各有特点,看需要适用的场景。
3、发布确认高级
在生产环境中由于一些不明原因,导致 RabbitMQ 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢?特别是在这样比较极端的情况下,RabbitMQ 集群不可用的时候,无法投递的消息该如何去处理呢?
发布确认高级配置类
/**
* @author wcc
* @date 2021/11/16 14:20
* 发布确认高级配置类
*/
@Configuration
public class ConfirmConfig {
// 交换机
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
// 队列
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
// RoutingKey
public static final String CONFIRM_ROUTING_KEY = "key1";
// 交换机声明
@Bean(name = "confirmExchange")
public DirectExchange confirmExchange(){
return new DirectExchange(CONFIRM_EXCHANGE_NAME);
}
// 队列声明
@Bean(name = "confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME)
.build();
}
// routingKey声明
@Bean(name = "key1")
public Binding confirmKey1(@Qualifier("confirmQueue") Queue confirmQueue,
@Qualifier("confirmExchange") DirectExchange confirmExchange){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).
with(CONFIRM_ROUTING_KEY);
}
}
发布确认高级生产者
// 开始发消息,发布确认高级,一旦交换机接受不到消息该怎么实施
@GetMapping("/sendConfirmMsg/{message}")
public void sendConfirmMessage(@PathVariable String message){
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,
ConfirmConfig.CONFIRM_ROUTING_KEY,message);
log.info("发送消息内容为:{}",message);
}
发布确认高级消费者
/**
* @author wcc
* @date 2021/11/16 14:44
*/
@Component
@Slf4j
public class ConfirmConsume {
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
public void receiveConfirmMessage(Message message) throws UnsupportedEncodingException {
log.info("接收到的队列confirm.queue消息为:{}",new String(message.getBody(),"UTF-8"));
}
}
这里,我们假设一下,交换机突然宕机或者队列出现了问题,消息不能到达队列完成持久化,也无法给生产者发送确认消息,这个时候我们该怎么解决呢?
答:通过确认回调接口来解决这个问题,当生产者感知不到确认消息的时候,就会通过回调接口 RabbitTemplate.ConfirmCallback把消息保存到缓存中。
发布确认高级添加回调配置类
/**
* @author wcc
* @date 2021/11/16 15:02
*/
@Component
@Slf4j
public class MyCallback implements RabbitTemplate.ConfirmCallback {
// 注入到RabbitTemplate实例中,以便可以调用到这个接口的实现类
@Autowired
private RabbitTemplate rabbitTemplate;
// 这个是JSR250定义的注解,被注解的方法将在bean创建并且赋值完成后,在执行初始化方法之前调用的逻辑
// 原理:后置处理器起的作用
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
}
/**
* 交换机确认回调方法
* @param correlationData 保存回调消息的ID以及相关信息
* @param ack 交换机收到消息为true 交换机没有收到消息为false
* @param cause 交换机没有收到消息的原因 成功则没有原因为null
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String s = correlationData!=null?correlationData.getId():"";
if(ack){
log.info("交换机已经收到ID为:{}的消息",s);
}else{
log.info("交换机还未收到ID为:{}的消,由于原因:{}",s,cause);
}
}
}
发布确认生产者代码
// 开始发消息,发布确认高级,一旦交换机接受不到消息该怎么实施
@GetMapping("/sendConfirmMsg/{message}")
public void sendConfirmMessage(@PathVariable String message){
CorrelationData correlationData = new CorrelationData("1");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,
ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData);
log.info("发送消息内容为:{}",message);
}
此外,还需要在SpringBoot的配置文件中添加:
# 开启交换机发布确认模式
spring.rabbitmq.publisher-confirm-type= correlated
- NONE
- 禁用发布确认模式,是默认值
- CORRELATED
- 发布消息成功到交换机后会触发回调方法
- SIMPLE
- 有两种效果:
- 其一效果和 CORRELATED 只一样会触发回调方法
- 其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或者 waitForConfirmsOrDie 方法等待 Broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是:waitForConfirmsOrDie 方法如果返回 false,则会关闭 channle,则接下来无法发送消息到 Broker。
测试结果如下:
下面我们来测试交换机出现问题的时候:(这里以发送端修改交换机的与其不对应来模拟)
// 开始发消息,发布确认高级,一旦交换机接受不到消息该怎么实施
@GetMapping("/sendConfirmMsg/{message}")
public void sendConfirmMessage(@PathVariable String message){
CorrelationData correlationData = new CorrelationData("1");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME+"123", # 现在这个交换机名字找不到
ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData);
log.info("发送消息内容为:{}",message);
}
我们在想,交换机出现了问题可以触发回调方法,那么如果队列出现问题接收不了消息该怎么解决呢?
3.1、回退消息
3.1.1、Mandatory 参数
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息就会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何让无法被路由的消息得到解决呢?通过设置 Mandatory 参数可以在当消息传递过程中不可达目的地的时候将消息返回给生产者
。
/**
* @author wcc
* @date 2021/11/16 15:02
*/
@Component
@Slf4j
public class MyCallback implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
// 注入到RabbitTemplate实例中,以便可以调用到这个接口的实现类
@Autowired
private RabbitTemplate rabbitTemplate;
// 这个是JSR250定义的注解,被注解的方法将在bean创建并且赋值完成后,在执行初始化方法之前调用的逻辑
// 原理:后置处理器起的作用
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
/**
* 交换机确认回调方法
* @param correlationData 保存回调消息的ID以及相关信息
* @param ack 交换机收到消息为true 交换机没有收到消息为false
* @param cause 交换机没有收到消息的原因 成功则没有原因为null
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String s = correlationData!=null?correlationData.getId():"";
if(ack){
log.info("交换机已经收到ID为:{}的消息",s);
}else{
log.info("交换机还未收到ID为:{}的消,由于原因:{}",s,cause);
}
}
/**
* 可以在当消息传递过程中不可达目的地的时候将消息返回给生产者
* 注意 只有不可达目的地的时候才进行回退消息
* @param returnedMessage
*/
@SneakyThrows
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.error("消息{},被交换机{}退回,退回的原因:{},路由 RoutingKey是:{}",
new String(returnedMessage.getMessage().getBody(),"UTF-8"),
returnedMessage.getExchange(),
returnedMessage.getReplyText(),
returnedMessage.getRoutingKey());
}
}
生产者发送消息时设置错的 RoutingKey 使得消息无法路由给队列会发生回退
// 开始发消息,发布确认高级,一旦交换机接受不到消息该怎么实施
@GetMapping("/sendConfirmMsg/{message}")
public void sendConfirmMessage(@PathVariable String message){
CorrelationData correlationData = new CorrelationData("1");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,
ConfirmConfig.CONFIRM_ROUTING_KEY+"123",message,correlationData);
log.info("发送消息内容为:{}",message);
}
测试结果如下:
3.2、备份交换机
有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递的时候发现并处理。但是有时候,我们并不知道该如何处理这些无法路由的消息,最多打印个日志,然后触发报警,再来动手处理。而通过日志来处理这些无法路由的消息是很不好的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。
如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?
前面设置死信队列的章节中,提到过,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由的消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。
在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份交换机可以理解为 RabbitMQ 中交换机的备胎,当我们为某一个交换声明一个对应的备份交换机的时候,就是为它创建一个备胎,当交换机接收到一条不可路由的消息的时候,将会把这条消息转发到对应的备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型是 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
3.2.1、代码架构图
配置类声明备份交换机及绑定关系
/**
* @author wcc
* @date 2021/11/16 14:20
* 发布确认高级配置类
*/
@Configuration
public class ConfirmConfig {
// 交换机
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
// 队列
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
// RoutingKey
public static final String CONFIRM_ROUTING_KEY = "key1";
// 备份交换机
public static final String BACKUP_EXCHANGE_NAME = "backup_exchange";
// 备份队列
public static final String BACKUP_QUEUE_NAME = "backup_queue";
// 报警队列
public static final String WARNING_QUEUE_NAME = "warning_queue";
// 交换机声明
@Bean(name = "confirmExchange")
public DirectExchange confirmExchange(){
Map<String,Object> arguments = new HashMap<>();
arguments.put("alternate-exchange",BACKUP_EXCHANGE_NAME);
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
.withArguments(arguments)
.build();
}
// 队列声明
@Bean(name = "confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME)
.build();
}
// routingKey声明
@Bean(name = "key1")
public Binding confirmKey1(@Qualifier("confirmQueue") Queue confirmQueue,
@Qualifier("confirmExchange") DirectExchange confirmExchange){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).
with(CONFIRM_ROUTING_KEY);
}
// 备份交换机声明
@Bean(name = "backupExchange")
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
// 备份队列声明
@Bean(name = "backupQueue")
public Queue backupQueue(){
return QueueBuilder.durable(BACKUP_QUEUE_NAME)
.build();
}
// 报警队列声明
@Bean(name = "warningQueue")
public Queue warningQueue(){
return QueueBuilder.durable(WARNING_QUEUE_NAME)
.build();
}
// 备份交换机绑定备份队列
@Bean
public Binding backupQueueBingingBackupExchange(@Qualifier("backupQueue") Queue backupQueue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(backupQueue).to(backupExchange);
}
// 备份交换机绑定报警队列
@Bean
public Binding warningQueueBingingBackupExchange(@Qualifier("warningQueue") Queue warningQueue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(warningQueue).to(backupExchange);
}
}
报警消费者进行接收
/**
* @author wcc
* @date 2021/11/16 16:51
* 报警消费者
*/
@Slf4j
@Component
public class WarningConsumer {
//接收报警消息
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
public void receiveWarningMessage(Message message) throws UnsupportedEncodingException {
String messages = new String(message.getBody(), "UTF-8");
log.error("报警发现不可路由消息:{}",messages);
}
}
测试结果如下:
注意,当 mandatory 参数与备份交换机可以一起使用的时候,经上面测试我们发现,mandatory 参数配置的回退消息并没有生效,所以备份交换机的优先级高。
4、RabbitMQ 的其他知识点
4.1、幂等性
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等。
消息重复消费
消费者在消费 MQ 中的消息,MQ 已经把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断,故MQ 未收到确认信息,该条消息会重新发送给其他地消费者,胡哦这在网络重连后再次发送给该消费者,但实际上该消费者已经成功消费了该条消息,造成消费者消费了重复的消息。
解决思路
MQ 消费者的幂等性的解决一般使用全局ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按照自己的规则生成一个全局的唯一 id,每次消费者消费信息的时候用该 id 先判断该消息是否已经消费过。
4.2、消费端的幂等性保障
在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:a. 唯一ID + 指纹码的机制,利用数据库主键去重 b. 利用 redis 的原子性来实现。
4.2.1、唯一ID + 指纹机制
指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存在数据库中,优势就是实现简单,就一个拼接操作,然后拆线呢判断是否存在;劣势就是在高并发的时候,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
4.2.2、Redis 的原子性
利用 Redis 执行 setnx 命令,天然具有幂等性,从而实现不重复消费。
4.3、优先级队列
在我们的系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时讲订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,商家对我们来说,肯定是要分大客户和小客户对吧,比如像苹果,小米这样大商家一年起码能给我们创造很大的利润,所以理所应当,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对较高的优先级,否则就是默认优先级。
生产者代码
/**
* @author wcc
* @date 2021/11/2 17:15
*/
public class Proceducer {
//队列名称
public static final String QUEUE_NAME="hello";
public static void main(String[] args) throws IOException, TimeoutException {
//发消息
//创建一个连接工厂
ConnectionFactory factory=new ConnectionFactory();
//工厂IP,连接RabbitMQ的队列
factory.setHost("39.107.103.173");
//用户名
factory.setUsername("admin");
//密码
factory.setPassword("admin");
//设置端口号 注意 15672是RabbitMQ是后台管理界面的端口号 应该设置5672端口
factory.setPort(5672);
//创建连接
Connection connection = factory.newConnection();
//获取信道(Channel)
Channel channel = connection.createChannel();
//创建一个队列
/**
* queueDeclare(String var1, boolean var2, boolean var3, boolean var4, Map<String, Object> var5)
* var1:队列名称
* var2:队列里面的消息是否持久化,默认情况下消息是存储在内存中,持久化即为存储在磁盘中
* var3:该队列是否只供一个消费者进行消费,是否进行消息共享,true可以多个消费者消费(即消息共享)
* false表示只能一个消费者消费(即不进行消息共享)
* var4:是否自动删除 最后一个消费者断开连接以后 该队列是否自动进行删除
* var5:其他参数,例如:延迟消息等等
*/
Map<String,Object> arguments = new HashMap<>();
arguments.put("x-max-priority",10); // 官方允许范围是0-255,此处设置为10,允许优化范围是0-10,不要设置过大,浪费内存
channel.queueDeclare(QUEUE_NAME,true,false,false,arguments);
//发消息
for (int i = 0; i < 11; i++) {
String message = "info" + i;
if(i == 5){
AMQP.BasicProperties properties =
new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("",QUEUE_NAME,properties,message.getBytes());
}else{
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
}
}
/**
* 发送一个消息:
* 1.发送到哪个交换机
* 2.路由的key值 本次是队列的名称 根据routing key分发消息到队列中
* 3.其他参数信息
* 4.发送消息的消息体
*/
//channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完毕!");
}
}
消费者代码
/**
* @author wcc
* @date 2021/11/3 11:07
* 消费者,接受消息的
*/
public class Consumer {
//队列的名称,目的是接受此队列的消息
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
//接收消息
//创建连接RabbitMQ的连接工厂
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("39.107.103.173");
factory.setUsername("admin");
factory.setPassword("admin");
factory.setPort(5672);
Connection connection=factory.newConnection();
Channel channel=connection.createChannel();
//成功的消费回调
DeliverCallback deliverCallback = (consumeTag,message)->{
System.out.println(new String(message.getBody()));
};
//取消消息时的回调
CancelCallback cancelCallback= (consumerTag)->{
System.out.println("消费消息被中断");
};
//消费者接收消息
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true代表的是自动应答 false代表的是手动应答
* 3.消费者成功消费的回调
* 4.消费者取消消费回调
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
测试结果如下:
4.4、惰性队列
RabbitMQ 从3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能地将消息存入磁盘中,而在消费者消费到相应地消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费造成堆积的时候,惰性队列就很有必要了。
默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能地存储在内存之中,这样可以更加快速地将消息发送给消费者。即使是持久化地消息,在被写入磁盘地同时也会在内存中驻留一份备份。当 RabbitMQ 需要释放内存地时候,会将内存中地消息换页至磁盘中,这个操作会耗费较长地时间,也会阻塞队列地操作,进而无法接收新地消息。
4.4.1、两种模式
队列具有两种模式:default 和 lazy 。默认的为 default 模式,在 3.6.0 之前的版本无需做任何变更。lazy 模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,也可以通过 Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。
在队列声明的时候可以通过 “x-queue-mode” 参数来设置队列的模式,取值为 “default” 和 “lazy” 。下面举个例子:
Map<String,Object> arguments = new HashMap<>();
arguments.put("x-queue-mode","lazy");
channel.queueDeclare("myqueue",true,false,false,arguments);
5、RabbitMQ 的集群
5.1、默认模式
RabbitMQ 集群允许消费者和生产者在 RabbitMQ 单个节点崩溃的情况下继续运行,它可以通过添加更多的节点来线性的扩展消息通信的吞吐量。当失去一个 RabbitMQ 节点的时候,客户端能够重新连接到集群中的任何其他节点并继续生产或者消费
RabbitMQ 集群并不能保证消息的万无一失,即使将消息、队列、交换机等都设置为可持久化,生产端和消费端也都正确的使用了消息应答方式,当集群中一个 RabbitMQ 节点崩溃的时候,该节点上的所有队列中的消息也会丢失。RabbitMQ 集群中的所有节点都会备份所有的元数据信息,包括以下内容:
- 队列元数据:队列的名称以及属性
- 交换机:交换机的名称及属性
- 绑定关系元数据:交换机与队列或者交换机与交换机之间的绑定关系
- vhost 元数据:为 vhost 内的队列、交换机和绑定提高命名空间及安全属性
但是不会备份消息。基于存储空间和性能的考虑,在 RabbitMQ 集群中创建队列,集群只会在单个节点而不是在所有节点上创建队列的进程并包含完整的队列信息。这样只有队列的宿主节点,即所有者节点知道队列的所有信息,所有其他节点非所有者节点只知道队列的元数据和指向该队列存在的那个节点的指针。并且任何匹配该队列绑定信息的新消息也都会消失。
不同于队列那样拥有自己的进程,交换机其实只是一个名称和绑定列表。当消息发布到交换机的时候,实际上是由所连接的信道将消息上的路由键同交换机的绑定列表进行比较,然后再路由消息。当创建一个新的交换机的时候,RabbitMQ 所要做的就是将绑定列表添加到集群中的所有节点上。
5.2、使用 Docker 搭建 RabbitMQ 集群
- 启动多个 RabbitMQ 容器
docker run -d --hostname rabbit1 --name myrabbit1 -p 15672:15672 -p 5672:5672 -e RABBITMQ_ERLANG_COOKIE=‘rabbitcookie’ rabbitmq:management
docker run -d --hostname rabbit2 --name myrabbit2 -p 5673:5672 --link myrabbit1:rabbit1 -e RABBITMQ_ERLANG_COOKIE=‘rabbitcookie’ rabbitmq:management
docker run -d --hostname rabbit3 --name myrabbit3 -p 5674:5672 --link myrabbit1:rabbit1 --link myrabbit2:rabbit2 -e RABBITMQ_ERLANG_COOKIE=‘rabbitcookie’ rabbitmq:management
多个容器之间用–link 连接
配置相同的 Erlang Cookie
为什么要配置相同的 Erlang Cookie
因为 RabbitMQ 使用Erlang 实现的,Erlang Cookie 相当于不同节点之间相互通讯的密钥,Erlang 节点通过交换Erlang Cookie 获得认证
要想知道 Erlang Cookie 的位置,首先要获取 RabbitMQ 启动日志里面的 home dir 路径,作为根路径。
Starting broker...2021-11-17 05:45:05.061059+00:00 [info] <0.1719.0>
2021-11-17 05:45:05.061059+00:00 [info] <0.1719.0> node : rabbit@rabbit1
2021-11-17 05:45:05.061059+00:00 [info] <0.1719.0> home dir : /var/lib/rabbitmq
2021-11-17 05:45:05.061059+00:00 [info] <0.1719.0> config file(s) : /etc/rabbitmq/conf.d/10-default-guest-user.conf
2021-11-17 05:45:05.061059+00:00 [info] <0.1719.0> cookie hash : l7FRc4s6MFrXQLBiUlLnOA==
2021-11-17 05:45:05.061059+00:00 [info] <0.1719.0> log(s) : /var/log/rabbitmq/rabbit@rabbit1_upgrade.log
2021-11-17 05:45:05.061059+00:00 [info] <0.1719.0> : <stdout>
2021-11-17 05:45:05.061059+00:00 [info] <0.1719.0> database dir : /var/lib/rabbitmq/mnesia/rabbit@rabbit1
所以 Erlang Cookie 的全部路径就是 : “/var/lib/rabbitmq/.erlang.cookie”;
注意:.erlang.cookie文件是一个隐藏文件,要使用 ls -al才可以看到
获取到第一个RabbitMQ 的 Erlang Cookie 之后,只需要把这个文件复制到其他 RabbitMQ 节点即可。
物理机和容器之间的复制命令如下:
- 容器复制文件物理机:
docker cp 容器名称:容器目录 物理机目录
- 物理机复制文件到容器:
docker cp 物理机目录 容器名称:容器目录
设置 Erlang Cookie 文件权限:chmod 600 /var/lib/rabbitmq/.erlang.cookie
步骤二:加入RabbitMQ 节点到集群
设置节点一:
docker exec -it myrabbit1 /bin/bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
ctrl + P + Q
设置节点二,加入到集群中:
docker exec -it myrabbit2 /bin/bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbit1
rabbitmqctl start_app
ctrl + P + Q
参数 “-ram” 代表设置为内存节点,忽略此参数默认设置为 磁盘节点。
设置节点3,加入到集群:
docker exec -it myrabbit3 /bin/bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbit1
rabbitmqctl start_app
ctrl + P + Q
5.3、镜像队列
5.3.1、使用镜像的原因
如果 RabbitMQ 集群中只有一个 Broker 节点,那么该节点的失效将导致整体服务的临时不可用,并且也可能会导致消息的丢失。可以将所有消息都设置为持久化,并且对应队列的 durable 属性也设置为 true,但是这样仍然无法避免由于缓存导致的问题:因为消息在发送之后和背写入磁盘中中执行刷盘动作之间存在一个短暂却会产生问题的时间窗。通过 publisherconfirm 机制能够确保客户端知道哪些消息已经存入磁盘,尽管如此,一般不希望遇到因为单点故障导致的服务不可用。
引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他 Broker 节点之上,如果集群中的一个节点失效了,队列能够自动的切换到镜像中的另一个节点上以保证服务的可用性。
5.4、Federation Exchange (联合交换机)
(Broker 北京),(Broker 深圳)彼此之间相距甚远,网络延迟是一个不得不面对的问题。有一个在北京的业务(Client 北京)需要连接(Broker 北京),向其中的交换器 exchangeA 发送消息,此时的网络延迟很小,(Client 北京)可以迅速将消息发送至 exchangeA 中,就算在开启了 publisherconfirm 机制或者事务机制的情况下,也可以迅速收到确认消息。此时又有个在深圳的业务(Client 深圳)需要向 exchangeA 发送消息,那么(Client 深圳)、(Broker 北京)之间有很大的网络延迟,(Client 深圳)需要发送消息至 exchangeA 会经历一定的延迟,尤其是在开启了 publisherconfirm 机制或者事务的情况下,(Client 深圳)会等待很长的延迟时间来接收(Broker 北京)的确认信息,进而必然造成这条发送线程的性能降低,甚至造成一定程度上的阻塞。
将业务(Client 深圳)部署到北京的机房可以解决这个问题,但是如果(Client 深圳)调用的那些服务都部署在深圳,那么又会引发新的时延问题,总不见得将所有业务全部部署在一个机房,那么容灾又何以实现?这里使用 Federation 插件就可以很好的解决这个问题。
5.4.1、搭建步骤、
1、需要保证每台节点单独运行
2、在每台机器上开启 Federation 相关插件
rabbitmq-plugins enable rabbitmq_federation
rabbitmq-plugins enable rabbitmq_federation_managerment