RabbitMQ的理解

参考文章

rabbitmq常见面试题
RabbitMQ:消息发送确认 与 消息接收确认(ACK)

简述

RabbitMQ是一个开源的消息代理软件。它接收生产者发布的消息并发送给消费者。它扮演中间商的角色,可以用来降低web服务器因发送消息带来的负载以及延时。

RabbitMQ里的几个重要概念。

  1. 生产者(Producer):发送消息的应用。
  2. 消费者(Consumer):接收消息的应用。
  3. 队列(Queue):存储消息的缓存。
  4. 消息(Message):由生产者通过RabbitMQ发送给消费者的信息。
  5. 连接(Connection):连接RabbitMQ和应用服务器的TCP连接。
  6. 通道(Channel):连接里的一个虚拟通道。当你通过消息队列发送或者接收消息时,这个操作都是通过通道进行的。注:由于TCP连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ使用Channel的方式来传输数据。Channel是建立在真实的TCP连接内的虚拟连接,且每条TCP连接上的Channel数量没有限制。
  7. 交换机(Exchange):交换机负责从生产者那里接收消息,并根据交换类型分发到对应的消息列队里。要实现消息的接收,一个队列必须到绑定一个交换机。
  8. 绑定(Binding):绑定是队列和交换机的一个关联连接。
  9. 路由键(Routing Key):路由键是供交换机查看并根据键来决定如何分发消息到列队的一个键。路由键可以说是消息的目的地址。

exchange 4个类型

  1. exchange在和queue进行binding时会设置routingkey
  2. 生产者在将消息发送到exchange时会设置对应的routingkey:

direct

在direct类型的exchange中,只有这两个routingkey完全相同,exchange才会选择对应的binging进行消息路由。
具体的流程如下:
在这里插入图片描述
producer 发送消息的outingkey=routing a 则 queue a 会接收到消息,其他不会

topic

将消息上的 routingkey 和 exchange 的routingkey 进行匹配。符号“#”匹配一个或多个词,符号“*”只能匹配一个词。
在这里插入图片描述
producer 发送消息的routingkey=a.b.c 则 queue a,queue b,queue c 都会接收到消息
producer 发送消息的outingkey=a.d.c 则 queue b,queue c 都会接收到消息
producer 发送消息的outingkey=a.d.e.c 则 queue c 会接收到消息

fanout

不处理routingkey。你只需要简单的将队列绑定到交换机上。一个发送到该类型交换机的消息都会被广播到与该交换机绑定的所有队列上。
在这里插入图片描述
producer 发送消息的routingkey=a.b.c 则 queue a,queue b,queue c 都会接收到消息
producer 发送消息的outingkey=a.d.c 则 queue a,queue b,queue c 都会接收到消息
producer 发送消息的outingkey=a.d.e.c 则 queue a,queue b,queue c 都会接收到消息

header

不处理路由键,而是根据发送的消息内容中的headers属性进行匹配,headers属性是一个键值对,如Dictionary。在绑定Queue与Exchange时指定一组headers属性;

Dictionary<string, object> aHeader = new Dictionary<string, object>();
aHeader.Add("format", "pdf");
aHeader.Add("type", "report");
aHeader.Add("x-match", "all");
channel.QueueBind(queue: "queue.A",exchange: "agreements", routingKey: string.Empty, arguments: aHeader);

其中的x-match为特殊的header,可以为all则表示要匹配所有的header,如果为any则表示只要匹配其中的一个header即可。
当消息发送到RabbitMQ时会取到该消息的headers与Exchange绑定时指定的键值对进行匹配;如果匹配则消息会路由到该队列,否则不会。

RabbitMQ:消息发送确认 与 消息接收确认(ACK)

默认情况下如果一个 Message 被消费者所正确接收则会被从 Queue 中移除,如果一个 Queue 没被任何消费者订阅,那么这个 Queue 中的消息会被 Cache(缓存),当有消费者订阅时则会立即发送,当 Message 被消费者正确接收时,就会被从 Queue 中移除。

消息发送确认

RabbitMQ使用发送方确认模式,确保消息正确地发送到RabbitMQ。发送方确认模式:将信道设置成confirm模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的ID。一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一ID)。如果RabbitMQ发生内部错误从而导致消息丢失,会发送一条nack(not acknowledged,未确认)消息。发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。

消息接收确认

消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ才能安全地把消息从队列中删除。这里并没有用到超时机制,RabbitMQ仅通过Consumer的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ给了Consumer足够长的时间来处理消息。

如果一个消费者在处理消息出现了网络不稳定、服务器异常等现象,那么就不会有ACK反馈,RabbitMQ会认为这个消息没有正常消费,会将消息重新放入队列中。消息的ACK确认机制默认是“自动确认模式”。

确认模式:
①自动确认模式:自动确认会在消息发送给消费者后立即确认。如果消费者挂掉,待ack的消息回归到队列中。消费者抛出异常,消息会不断的被重发,直到处理成功。如果消费端没挂掉但消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。

②手动确认模式:消费者手动调用 ack、nack、reject 几种方法进行确认。如果某个服务忘记 ACK 了,RabbitMQ会立即将这个消息推送给这个在线的其他消费者,并认为该消费者繁忙,将不会给该消费者分发更多的消息。
一直忘记ACK,那么后果很严重。当Consumer退出时候,Message会一直重新分发。然后RabbitMQ会占用越来越多的内容,由于RabbitMQ会长时间运行,因此这个"内存泄漏"是致命的。

③不确认模式,acknowledge=“none” 不使用确认机制,只要消息发送完成会立即在队列移除,无论客户端异常还是断开,只要发送完就移除,不会重发。

避免消息重复投递或重复消费

发送消息:在MQ内部针对每条生产者发送的消息生成一个inner-msg-id,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列;

消费消息:在消息消费时,要求消息体中必须要有一个bizId(对于同一业务全局唯一,如支付ID、订单ID、帖子ID等)作为去重和幂等的依据,避免同一条消息被重复消费。

如何解决丢数据的问题

生产者丢数据

RabbitMQ提供发送方confirm模式来确保生产者不丢消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。如果失败可以从新发送消息,并记录重试次数。如果重试次数大于你自己预定的值,则不重发消息,把消息和失败的原因保存到数据库,之后人工查明原因并解决。

ConfirmCallback:通过实现 ConfirmCallback 接口,消息发送到 Broker 后触发回调,确认消息是否到达 Broker 服务器,也就是只确认是否正确到达Exchange 中

@Component
public class RabbitTemplateConfig implements RabbitTemplate.ConfirmCallback{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);            //指定 ConfirmCallback
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println("消息唯一标识:"+correlationData);
        System.out.println("确认结果:"+ack);
        System.out.println("失败原因:"+cause);
    }

还需要在配置文件添加配置

 spring.rabbitmq.publisher-confirms= true

ReturnCallback :通过实现 ReturnCallback 接口,启动消息失败返回,比如路由不到队列时触发回调。

@Component
public class RabbitTemplateConfig implements RabbitTemplate.ReturnCallback{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setReturnCallback(this);             //指定 ReturnCallback
    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.out.println("消息主体 message : "+message);
        System.out.println("消息主体 message : "+replyCode);
        System.out.println("描述:"+replyText);
        System.out.println("消息使用的交换器 exchange : "+exchange);
        System.out.println("消息使用的路由键 routing : "+routingKey);
    }
}

还需要在配置文件添加配置

spring.rabbitmq.publisher-returns = true 

消息队列丢数据

处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。

那么如何持久化呢,这里顺便说一下吧,其实也很容易,就下面两步
①、将queue的持久化标识durable设置为true,则代表是一个持久的队列
②、发送消息的时候将deliveryMode=2
这样设置以后,rabbitMQ就算挂了,重启后也能恢复数据。在消息还没有持久化到硬盘时,可能服务已经死掉,这种情况可以通过引入mirrored-queue即镜像队列,但也不能保证消息百分百不丢失(整个集群都挂掉)

消费者丢数据

启用手动确认模式可以解决这个问题,自动确认模式下消费者收掉消息后会自动确认,queue会删除消息,这时如果消费者业务逻辑处理异常时数据还是会丢失。手动确认模式下,如果业务逻辑异常可以做一下消息补偿机制,如重发消息,保存错误消息等。手动确认模式必须要返回ack,否则容易出现“内存泄漏”。

Broker

Broker:简单来说就是消息队列服务器实体。中文意思:中间件。broker 是指一个或多个 erlang node 的逻辑分组,且 node 上运行着 RabbitMQ 应用进程并共享用户、虚拟主机、队列、交换器、绑定和运行时参数。我们将节点的集合称为cluster 。一个broker里可以开设多个vhost,用作不同用户的权限分离。

vhost

每一个RabbitMQ服务器都能创建虚拟消息服务器,我们称之为虚拟主机。每一个vhost本质上是一个mini版的RabbitMQ服务器,拥有自己的交换机、队列、绑定等,拥有自己的权限机制。vhost之于Rabbit就像虚拟机之于物理机一样。他们通过在各个实例间提供逻辑上分离,允许为不同的应用程序安全保密的运行数据,这很有,它既能将同一个Rabbit的众多客户区分开来,又可以避免队列和交换器的命名冲突。RabbitMQ提供了开箱即用的默认的虚拟主机“/”,如果不需要多个vhost可以直接使用这个默认的vhost,通过使用缺省的guest用户名和guest密码来访问默认的vhost。
vhost之间是相互独立的,这避免了各种命名的冲突,就像App中的沙盒的概念一样,每个沙盒是相互独立的,且只能访问自己的沙盒,以保证非法访问别的沙盒带来的安全隐患。(一个典型的例子就是不同的应用可以跑在不同的 vhost 中)

元数据

在非 cluster 模式下,元数据主要分为 Queue 元数据(queue 名字和属性等)、Exchange 元数据(exchange 名字、类型和属性等)、Binding 元数据(存放路由关系的查找表)、Vhost 元数据(vhost 范围内针对前三者的名字空间约束和安全属性设置)。在 cluster 模式下,还包括 cluster 中 node 位置信息和 node 关系信息。元数据按照 erlang node 的类型确定是仅保存于 RAM 中,还是同时保存在 RAM 和 disk 上。元数据在 cluster 中是全 node 分布的。
在这里插入图片描述

死信队列&死信交换器

死信消息:

  1. 消息被拒绝(Basic.Reject或Basic.Nack)并且设置 requeue 参数的值为 false
  2. 消息过期了
  3. 队列达到最大的长度

消息过期:在 rabbitmq 中存在2种方可设置消息的过期时间,第一种通过对队列进行设置,这种设置后,该队列中所有的消息都存在相同的过期时间,第二种通过对消息本身进行设置,那么每条消息的过期时间都不一样。如果同时使用这2种方法,那么以过期时间小的那个数值为准。当消息达到过期时间还没有被消费,那么那个消息就成为了一个 死信 消息。

  1. 队列设置:在队列申明的时候使用 x-message-ttl 参数,单位为 毫秒
  2. 单个消息设置:是设置消息属性的 expiration 参数的值,单位为 毫秒

DLX 全称(Dead-Letter-Exchange),称之为死信交换器,当消息变成一个死信之后,如果这个消息所在的队列存在x-dead-letter-exchange参数,那么它会被发送到x-dead-letter-exchange对应值的交换器上,这个交换器就称之为死信交换器,与这个死信交换器绑定的队列就是死信队列。

使用场景:用户在系统中创建一个订单,如果超过时间用户没有进行支付,那么自动取消订单。
分析:
1、上面这个情况,我们就适合使用延时队列来实现,那么延时队列如何创建
2、延时队列可以由 过期消息+死信队列 来时间
3、过期消息通过队列中设置 x-message-ttl 参数实现
4、死信队列通过在队列申明时,给队列设置 x-dead-letter-exchange 参数,然后另外申明一个队列绑定x-dead-letter-exchange对应的交换器。
在这里插入图片描述
代码如下:

ConnectionFactory factory = new ConnectionFactory(); 
factory.setHost("127.0.0.1"); 
factory.setPort(AMQP.PROTOCOL.PORT); 
factory.setUsername("guest"); 
factory.setPassword("guest"); 
Connection connection = factory.newConnection(); 
Channel channel = connection.createChannel();
 
// 声明一个接收被删除的消息的交换机和队列 
String EXCHANGE_DEAD_NAME = "exchange.dead"; 
String QUEUE_DEAD_NAME = "queue_dead"; 
channel.exchangeDeclare(EXCHANGE_DEAD_NAME, BuiltinExchangeType.DIRECT); 
channel.queueDeclare(QUEUE_DEAD_NAME, false, false, false, null); 
channel.queueBind(QUEUE_DEAD_NAME, EXCHANGE_DEAD_NAME, "routingkey.dead"); 
 
String EXCHANGE_NAME = "exchange.fanout"; 
String QUEUE_NAME = "queue_name"; 
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT); 
Map<String, Object> arguments = new HashMap<String, Object>(); 
// 统一设置队列中的所有消息的过期时间 
arguments.put("x-message-ttl", 30000); 
// 设置超过多少毫秒没有消费者来访问队列,就删除队列的时间 
arguments.put("x-expires", 20000); 
// 设置队列的最新的N条消息,如果超过N条,前面的消息将从队列中移除掉 
arguments.put("x-max-length", 4); 
// 设置队列的内容的最大空间,超过该阈值就删除之前的消息
arguments.put("x-max-length-bytes", 1024); 
// 将删除的消息推送到指定的交换机,一般x-dead-letter-exchange和x-dead-letter-routing-key需要同时设置
arguments.put("x-dead-letter-exchange", "exchange.dead"); 
// 将删除的消息推送到指定的交换机对应的路由键 
arguments.put("x-dead-letter-routing-key", "routingkey.dead"); 
// 设置消息的优先级,优先级大的优先被消费 
arguments.put("x-max-priority", 10); 
channel.queueDeclare(QUEUE_NAME, false, false, false, arguments); 
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ""); 
String message = "Hello RabbitMQ: "; 
 
for(int i = 1; i <= 5; i++) { 
	// expiration: 设置单条消息的过期时间 
	AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties().builder()
			.priority(i).expiration( i * 1000 + ""); 
	channel.basicPublish(EXCHANGE_NAME, "", properties.build(), (message + i).getBytes("UTF-8")); 
} 
channel.close(); 
connection.close();

使用了消息队列会有什么缺点

1.系统可用性降低:你想啊,本来其他系统只要运行好好的,那你的系统就是正常的。现在你非要加个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性降低

2.系统复杂性增加:要多考虑很多方面的问题,比如一致性问题、如何保证消息不被重复消费,如何保证保证消息可靠传输。因此,需要考虑的东西更多,系统复杂性增大。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值