分布式框架中间件总纲
https://www.jianshu.com/p/00aa796bb5b8
友情链接(消息三解序)
1、RabbitMQ(消息队列)--- 分解 A
2、RabbitMQ(消息队列)--- 分解 B
3、RabbitMQ(消息队列)--- 分解 C
4、RabbitMQ(消息队列)--- 面试题
本章目录
一、发布确认高级
1、发布确认 springboot 版本
2、回退消息
3、备份交换机
二、RabbitMQ 其他知识点
1、幂等性
2、优先级队列
3、惰性队列
三、RabbitMQ 集群
1、clustering
2、镜像队列
3、Haproxy+Keepalive 实现高可用负载均衡
4、Federation Exchange
5、Federation Queue
6、Shovel
一、发布确认高级
引出问题:
在生产环境中由于一些不明原因,导致 rabbitmq 重启,在RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢? 特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢?
1、发布确认 springboot 版本
1、消息确认机制方案
2、代码架构图
3、配置文件
spring.rabbitmq.publisher-confirm-type=correlated
⚫ NONE
禁用发布确认模式,是默认值
⚫ CORRELATED
发布消息成功到交换器后会触发回调方法
⚫ SIMPLE
经测试有两种效果,
其一效果和 CORRELATED 值一样会触发回调方法,
其二在发布消息成功后使用 rabbitTemplate 调用waitForConfirms 或 waitForConfirmsOrDie 方法
等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker
spring.rabbitmq.host=106.52.23.202
spring.rabbitmq.port=5672
spring.rabbitmq.username=mykk
spring.rabbitmq.password=abc666
spring.rabbitmq.publisher-confirm-type=correlated #开启回调
4、配置类
package com.rabbit.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
public static final String CONFIRM_ROUTING_KEY = "key1";
// 声明交换机
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
return new DirectExchange (CONFIRM_EXCHANGE_NAME);
}
// 声明队列
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable (CONFIRM_QUEUE_NAME).build ();
}
// 绑定
@Bean
public Binding queueBinding(@Qualifier("confirmExchange")DirectExchange confirmExchange,
@Qualifier("confirmQueue")Queue queue){
return BindingBuilder.bind (queue).to (confirmExchange).with (CONFIRM_ROUTING_KEY);
}
}
5、回调接口
package com.rabbit.callback;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class MyCallback implements RabbitTemplate.ConfirmCallback {
/**
* 交换机无论是否接受消息都会进入回调
*
* CorrelationData 消息相关数据
* ack 交换机是否接受消息
* cause ack 为 true 成功返回null ,akc 为 false 失败 返回原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String id = correlationData != null ? correlationData.getId ( ) : "";
if (ack) {
log.info ("回调成功:交换机已经收到 id 为:{}的消息", id);
} else {
log.info ("回调失败:交换机还未收到 id 为:{}消息,由于原因:{}", id, cause);
}
}
}
6、消息生产者
package com.rabbit.controller;
import com.rabbit.callback.MyCallback;
import com.rabbit.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
@Slf4j
@RestController
@RequestMapping("/confirm")
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private MyCallback myCallback;
// 依赖注入 rabbitTemplate 之后再设置它的回调对象
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback (myCallback);
}
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable String message) {
// 指定消息的id 为 1
CorrelationData correlationData1 = new CorrelationData ("1");
String routingKey = "key1";
rabbitTemplate.convertAndSend (ConfirmConfig.CONFIRM_EXCHANGE_NAME, routingKey, message, correlationData1);
log.info ("发送消息内容:{}", message);
// 模拟发不出去(发送的交换机名字 改错)
rabbitTemplate.convertAndSend ("123"+ConfirmConfig.CONFIRM_EXCHANGE_NAME, routingKey, message, correlationData1);
log.info ("发送消息内容:{},测试交换机问题", message);
// 默认队列出问题
CorrelationData correlationData2 = new CorrelationData ("2");
routingKey = "key2";// 改为一个没有绑定过的key2
rabbitTemplate.convertAndSend (ConfirmConfig.CONFIRM_EXCHANGE_NAME, routingKey, message , correlationData2);
log.info ("发送消息内容:{},测试队列问题", message);
}
}
7、消息消费者
package com.rabbit.consumer;
import com.rabbit.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class ConfirmConsumer {
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
public void receiveMsg(Message message){
String msg = new String (message.getBody ( ));
log.info("接受到队列 confirm.queue 消息:{}",msg);
}
}
8、结果分析
可以看到,发送了两条消息,第一条消息的 RoutingKey 为 "key1",第二条消息的 RoutingKey 为
"key2",两条消息都成功被交换机接收,也收到了交换机的确认回调,但消费者只收到了一条消息,因为
第二条消息的 RoutingKey 与队列的 BindingKey 不一致,也没有其它队列能接收这个消息,所有第二条
消息被直接丢弃了。
2、回退消息
1、Mandatory 参数
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者(简单来说就是解决上面的routingKey错误,导致队列发送的消息被丢弃,解决的就是队列发送的消息不被丢弃而是回退,有点类似事务的回滚)
2、回调接口(改写:初始化主要在这里)
package com.rabbit.callback;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Slf4j
@Component
public class MyCallback implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
// 依赖注入 rabbitTemplate 之后再设置它的回调对象
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback (this);
rabbitTemplate.setReturnCallback (this);
}
/**
* 当消息无法路由的时候的回调方法
*
* @param message
* @param replyCode 失败码
* @param replyText 失败原因
* @param exchange
* @param routingKey
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.error (" 消 息 {}, 被 交 换 机 {} 退 回 , 退 回 原 因 :{}, 路 由 key:{}",
new String (message.getBody ( )), exchange, replyText, routingKey);
}
/**
* 交换机无论是否接受消息都会进入回调
*
* CorrelationData 消息相关数据
* ack 交换机是否接受消息
* cause ack 为 true 成功返回null ,akc 为 false 失败 返回原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String id = correlationData != null ? correlationData.getId ( ) : "";
if (ack) {
log.info ("回调成功:交换机已经收到 id 为:{}的消息", id);
} else {
log.info ("回调失败:交换机还未收到 id 为:{}消息,由于原因:{}", id, cause);
}
}
}
3、消息生产者代码(改写:取消注入回调,不在此处初始化)
package com.rabbit.controller;
import com.rabbit.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/confirm")
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable String message) {
// 指定消息的id 为 1
CorrelationData correlationData1 = new CorrelationData ("1");
String routingKey = "key1";
rabbitTemplate.convertAndSend (ConfirmConfig.CONFIRM_EXCHANGE_NAME, routingKey, message, correlationData1);
log.info ("发送消息内容:{}", message);
// 模拟发不出去(发送的交换机名字 改错)
// rabbitTemplate.convertAndSend ("123"+ConfirmConfig.CONFIRM_EXCHANGE_NAME, routingKey, message, correlationData1);
// log.info ("发送消息内容:{},测试交换机问题", message);
// 默认队列出问题
CorrelationData correlationData2 = new CorrelationData ("2");
routingKey = "key2";// 改为一个没有绑定过的key2
rabbitTemplate.convertAndSend (ConfirmConfig.CONFIRM_EXCHANGE_NAME, routingKey, message , correlationData2);
log.info ("发送消息内容:{},测试队列问题", message);
}
}
4、配置文件
spring.rabbitmq.host=106.52.23.202
spring.rabbitmq.port=5672
spring.rabbitmq.username=mykk
spring.rabbitmq.password=aaa666
#回调开启
spring.rabbitmq.publisher-confirm-type=correlated
#回退开启
spring.rabbitmq.publisher-returns=true
5、结果分析:捕获到队列不可路由的数据(消息)
3、备份交换机
个人小结:上面提到了交换机出问题,就无法接受到消息,通过回调可以检测到,但是需要重新发送消息,可以通过备份交换机的队列去发送。
比较官方的理论:
有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息
无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然
后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者
所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增
加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的
复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些
处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。
在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份
交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就
是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备
份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定
的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进
入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
1、代码架构图(已下代码根据此图编写)
2、修改配置
package com.rabbit.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ConfirmConfigBack {
// 交换机
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
public FanoutExchange backupExchange(){
return new FanoutExchange (BACKUP_EXCHANGE_NAME);
}
// 声明备份队列
@Bean
public Queue backupQueue(){
return QueueBuilder.durable (BACKUP_QUEUE_NAME).build ();
}
// 声明报警队列
@Bean
public Queue warningQueue(){
return QueueBuilder.durable (WARNING_QUEUE_NAME).build ();
}
// 备份队列绑定备份交换机
@Bean
public Binding backupQueueBindingBackupExchange(@Qualifier("backupExchange")FanoutExchange backupExchange,
@Qualifier("backupQueue")Queue backupQueue){
return BindingBuilder.bind (backupQueue).to (backupExchange);// 无绑定 routingKey,扇出(广播)类型没有必要有
}
// 报警队列绑定备份交换机
@Bean
public Binding warningQueueBindingBackupExchange(@Qualifier("backupExchange")FanoutExchange backupExchange,
@Qualifier("warningQueue")Queue warningQueue){
return BindingBuilder.bind (warningQueue).to (backupExchange);// 无绑定 routingKey,扇出(广播)类型没有必要有
}
// 声明交换机(改写:若无法投递需要转发到备份交换机上面)
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
//return new DirectExchange (CONFIRM_EXCHANGE_NAME);
// 代码意思:直接交换机 confirm.exchange ,持久化,并且无法投递的时候转发到备份交换机
return ExchangeBuilder.directExchange (CONFIRM_EXCHANGE_NAME).durable (true)
.withArgument ("alternate-exchange",BACKUP_EXCHANGE_NAME).build ();
}
// ------------------------------以上为核心扩展部分--------------------------------------------------
// 声明队列
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable (CONFIRM_QUEUE_NAME).build ();
}
// 绑定
@Bean
public Binding queueBinding(@Qualifier("confirmExchange")DirectExchange confirmExchange,
@Qualifier("confirmQueue")Queue queue){
return BindingBuilder.bind (queue).to (confirmExchange).with (CONFIRM_ROUTING_KEY);
}
}
3、报警消费者
package com.rabbit.consumer;
import com.rabbit.config.ConfirmConfigBack;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class WarningConsumer {
@RabbitListener(queues = ConfirmConfigBack.WARNING_QUEUE_NAME)
public void receiveWarningMsg(Message message){
log.error("报警发现不可路由消息:{}", new String (message.getBody ()));
}
}
4、注意事项:需要删除掉之前定义好的交换机(confrim.exchange)因为配置关系已经变了
5、输出结果
mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,备份交换机优先级高。
二、RabbitMQ 其他知识点
1、幂等性
1、概念
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,
此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱
了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误
立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等
2、消息重复消费
消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给MQ 返回 ack 时网络中断,
故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但
实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
3、解决思路
MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费
者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消
息时用该 id 先判断该消息是否已消费过。
4、消费端的幂等性保障
在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,
这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:a.
唯一 ID+指纹码机制,利用数据库主键去重, b.利用 redis 的原子性去实现
5、唯一ID+指纹码机制
指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基
本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存
在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数
据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
6、Redis原子性(最佳方案)
利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费
2、优先级队列
1、场景
在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如
果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tmall
商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创
造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存
放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级,否则就是默认优先级。(简称,开后门)
2、如何添加
方式一:客户端操作
方式二:代码控制,在队列层面
Map params = new HashMap ( );
params.put ("x-max-priority", 10);
channel.queueDeclare ("hello", true, false, false, params);
方式三:代码控制,在消息层面
AMQP.BasicProperties properties = new
AMQP.BasicProperties ( ).builder ( ).priority (5).build ( );
注意事项:
要让队列实现优先级需要做的事情有如下事情:队列需要设置为优先级队列,消息需要设置消息的优先级,消费者需要等待消息已经发送到队列中才去消费因为,这样才有机会对消息进行排序
3、惰性队列
1、场景:
RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消
费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持
更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致
使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中,
这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留
一份备份。当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的
时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法,
但是效果始终不太理想,尤其是在消息量特别大的时候。
2、两种模式
队列具备两种模式:default 和 lazy。默认的为default 模式,在3.6.0 之前的版本无需做任何变更。lazy
模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,也可以通过
Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。
如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。
在队列声明的时候可以通过“x-queue-mode”参数来设置队列的模式,取值为“default”和“lazy”。下面示
例中演示了一个惰性队列的声明细节:
Map args = new HashMap ( );
args.put ("x-queue-mode", "lazy");
channel.queueDeclare ("myqueue", false, false, false, args);
3、内存开销问题
在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用内存是 1.2GB,而惰性队列仅仅
占用 1.5MB
三、RabbitMQ 集群(因没机子没硬盘搞集群,暂更新)
1、clustering
2、镜像队列
3、Haproxy+Keepalive 实现高可用负载均衡
4、Federation Exchange
5、Federation Queue
6、Shovel
四、遇到的问题
1、rabbitmq宕机:Management API returned status code 500
重启命令不是一下博客的:因为有改过
rabbitmqctl start_app
,其他参考以下
解决方案:
https://blog.csdn.net/qq_38890137/article/details/104403234
2、补充:查看liunx剩余内存:free -h
五、总结了几点
1、交换机出问题可以通过回调,但是队列无法区分;所以队列无法路由只能通过回退操作。
2、