RabbitMQ(三) rabbitMQ概念详解

5 篇文章 0 订阅
4 篇文章 0 订阅

一 、Exchange 交换机

1. Exchange :接收消息,并且根据路由键转发消息所绑定的队列
交换机图解
蓝色:生产A,B将消息投递到Exchange上 ,Exchange根据RoutingKey转发到指定的队列中
绿色:消费者1,2,3监听队列来获取消息
黄色:根据RoutingKey绑定交换机和队列
红色:rabbitMQ服务

2. 交换机属性
Name:交换机名称
Type:交换机类型 direct,topic,fanout,headers
Durability:是否需要持久化,true 为持久化
Auto Delete:当最后一个绑定到Exchange上的队列删除后,自动删除该Exchange
Internal:当前Exchange是否用于RabbitMQ内部使用,默认为false(一般不去做修改)
Arguments:扩展参数,用于扩展AMQP协议自制定化使用

类型
1、Direct Exchange(直连交换机)

所有发送到Direct Exchange的消息都会被转发到RouteKey中指定的Queue,不允许模糊匹配路由键,
投递消息中的routingKey 要和队列中binding的key完全一致

Direct模式可以使用RabbitMQ中自带的Exchange:default Exchange,所以不需要将Exchange进行任何绑定,消息传递时,RouteKey必须完全匹配才会被队列接收,否则该消息将会被抛弃
在这里插入图片描述
routing key中的key要和队列名完全一致,就会将消息直接路由到队列中,否则抛弃
当routing key和队列名完全一致时,不需要binding,可以直接投递

2、Topic Exchange(主题交换机)

所有发送到 Topic Exchange 的消息被转发到所有关心RouteKey中指定的Topic的Queue上
Exchange将 RouteKey 和某个Topic进行模糊匹配,此时队列需要绑定一个Topic
模糊匹配可以使用通配符
符号
" # " 匹配一个到多个词 ,“log.#” 能够匹配到"log.info.ao"
" * " 匹配不多不少一个词 , “log.*” 只会匹配到 "log.error"

在这里插入图片描述
队列中的routing key会和消息中的 routing key进行匹配,将消息投递到所有符合匹配规则的队列中

3、Fanout Exchange(扇型交换机)

将消息路由给绑定到它身上的所有队列。不同于直连交换机,路由键在此类型上不启任务作用。如果N个队列绑定到某个扇型交换机上,当有消息发送给此扇型交换机时,交换机会将消息的发送给这所有的N个队列
转发消息的速度最快,不需要去做匹配路由规则,性能最好
在这里插入图片描述

4、Headers exchange(头部交换机)

头部交换机和扇形交换机都不需要路由键 routing Key,交换机时通过Headers头部来将消息映射到队列的,有点像HTTP的Headers,Hash结构中要求携带一个键“x-match”,这个键的Value可以是any或者all,这代表消息携带的Hash是需要全部匹配(all),还是仅匹配一个键(any)就可以了。相比直连交换机,首部交换机的优势是匹配的规则不被限定为字符串(string)而是Object类型。

消费方指定的headers中必须包含一个"x-match"的键。键"x-match"的值有2个:all和any。

  • any: 只要在发布消息时携带的有一对键值对headers满足队列定义的多个参数arguments的其中一个就能匹配上,注意这里是键值对的完全匹配,只匹配到键了,值却不一样是不行的;
  • all:在发布消息时携带的所有Entry必须和绑定在队列上的所有Entry完全匹配
    在这里插入图片描述

二 、Binding 绑定

Exchange 和 Exchange 、Queue之间的连接关系
Binding 中 可以包含Routing Key或者参数

三 、Queue 消息队列

消息队列,实际存储消息数据

队列属性

**Name**:交换机名称
**Durability**:是否需要持久化,Durable 为持久化,Transient 为不持久化
**Auto Delete**:如果为yes,当最后一个监听被移除后,该队列也会被自动删除
**arguments(其他属性):** 
**1、x-message-ttl:** 消息的过期时间,单位:毫秒
**2、x-expires:** 队列过期时间,队列在多长时间未被访问将被删除,单位:毫秒
**3、x-max-length:** 队列最大长度,超过该最大值,则将从队列头部开始删除消息
**4、x-max-length-bytes:** 队列消息内容占用最大空间,受限于内存大小,超过该值则从队列头部开始删除消息
**5、x-overflow:** 设置队列溢出行为。这决定了当达到队列的最大长度时消息会发生什么。有效值是drop-head、reject-publish或reject-publish-dlx
**6、x-dead-letter-exchange:** 死信交换器名称,过期或被删除(因队列长度超长或因空间超出该值)的消息可指定发送到该交换器中
**7、x-dead-letter-routing-key:** 死信消息路由键,在消息发送到死信交换器时会使用该路由键,如果不设置,则使用消息的原来的路由键值
**8、x-single-active-consumer:** 表示队列是否是单一活动消费者,true时,注册的消费组内只有一个消费者消费消息,其他被忽略,false时消息循环分发给所有消费者(默认false)
**9、x-max-priority:** 队列要支持的最大优先级数;如果未设置,队列将不支持消息优先级
**10、Lazy mode:** 将队列设置为延迟模式,在磁盘上保留尽可能多的消息,以减少RAM的使用;如果未设置,队列将保留内存缓存以尽可能快地传递消息
**11、x-queue-master-locator:** 在集群模式下设置镜像队列的主节点信息

四 、Message 消息

服务器和应用程序之间传送的数据

消息的组成:Properties , Payload(Body)

消息的属性

**delivery mode**: 是否持久化
**headers** :可以自定义属性
 **content_type** :类型
 **content_encoding** :字符集
 **priority** :优先级 0-9 优先级越来越高
 **correlation_id** :消息唯一id
 **reply_to** :消息指定返回队列
 **expiration** :消息过期时间,如果到期没有被消费就会被MQ删除
 **message_id** :消息id

五 、Virtual host 虚拟主机

虚拟地址,最上层的消息路由,用于进行逻辑隔离
一个Virtual Host里面有若干个 Exchange 和 Queue
同一个Virtual Host里面不能有相同名称的 Exchange 和 Queue

六 、消息100%投递

什么是生产端的可靠性投递?

1.保障消息的成功发出
2.保障MQ节点的成功接收
3.发送端收到节点MQ(Broker)的确认应答
4.完善的消息进行补偿机制 

BAT大厂解决方案:
(1) 、消息落库,对消息状态进行打标在这里插入图片描述
蓝色:生产者,将消息投递到MQ

Step1:将业务数据入库(BIZ),并将发送的消息入库(MSG)
Step2:发送消息到Broker
Step3: Broker 确认收到 返回应答给生产者( Confrim Listener 异步监听相应)
Step4: 将消息入库的记录状态更新为投递成功状态
Step5:定时拉取状态为0(发送失败消息)
Step6: 生产者重新发送,并将入库的消息 count + 1
Step7: 重试发送3次的消息将状态更新为无法发送  

优点:操作简单,易使用
缺点:对数据库进行多次insert和update操作,在高并发的业务场景下性能有瓶颈
适用场景:并发小的应用
(2) 、消息的延迟投递,做二次确认,回调检查
在这里插入图片描述
Upstream:上游服务(生产者),Downstream: 下游服务(消费者),MQ Broker(MQ集群)
Callback : 回调服务

Step1: 业务数据入库结束后做第一次消息的发送
Step2: 发送一条延迟消息的检查(定5分钟)
Step3: 消费者监听队列消费消息
Step4: 消费者回送相应消息到MQ(重新生成一条消息投递到MQ)  
Step5: Callback 服务监听消费者投递回送消息的队列,如果收到回送消息则将消息入库
Step6: Callback 服务监听生产者投递的延时检查消息,检查数据库中是否有下游服务的回送消息,如果有回送消息的记录则结束。没有的话进行补偿机制,Callback服务主动发起RPC 通信(参数是业务的id或者别的唯一标识),通知上游服务消息投递失败,上游服务查询业务数据库获取数据重新发送消息

优点 : 节省数据库操作,DB解耦,提高并发性能
缺点 : 业务需要加外围监听服务
适用场景: 对高并发要求高的应用

七 、什么是幂等性,如何避免消息重复消费

用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用
例如乐观锁:update Order set COUNT= COUNT - 1 , VERSION = VERSION +1 where ORDER_ID =1 AND VERSION =1
高并发下可能COUNT 只有1个 ,但是同时又两个请求,如果同时处理COUNT会成为负数业务不允许
解决办法:添加乐观锁 每次操作先查询version号,执行操作将version号带上,执行成功后将version +1 ,如果同时两个请求也第一个先将version更新成2完成了操作,第二个请求则会失败
消费端-幂等性保障
在海量订单产生的业务高峰期,如何避免消息的重复消费问题?
消费端实现幂等性,就意味,我们的消息永远不会消费多次,即时我们收到了多条一样的消息
解决方案:

1、唯一ID + 指纹码 机制 ,利用数据库主键去重
  唯一ID + 指纹码(业务规则拼接而成) ,利用数据库主键去重
  SELECT COUNT(1) FROM T_ORDER WHERE  ID = 唯一ID + 指纹码
  优点:实现方法简单
  缺点:高并发下数据库写入的性能瓶颈
  解决方法:跟进 ID 进行分表分库 ,实现分压分流来提高性能

2、 利用redis的原子性去重

  通过redis 实现 
  业务id来区分是否唯一,通过EXISTS来判断是否存在 ,存在则忽略,不存在则set 
  使用redis的问题:
  第一:是否要对数据落库,如果落库的话怎么解决数据库和缓存间如何做到原子性
  第二:如果不进行落库,都存储到缓存中,如何设置定时同步的策略(同步到数据库)

八 、Confirm 确认消息

什么是Confirm消息确认机制:
消息的确认,是指生产者投递消息后,如果Broker收到消息,则会给生产者一个应答
生产者进行接收应答,用来确定这条消息是否正常的发送到Broker,该方式是可靠性投递的核心保障
在这里插入图片描述
send message:发送消息 broker confirm:回送相应 Confirm Listener:异步监听Broker的响应

springboot 如何实现Confirm 确认消息?

#rabbitmq 消息发送到交换机确认机制,ack
spring.rabbitmq.publisher-confirms=true
@Component
@Slf4j
public class RabbitSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    private final ConfirmCallback confirmCallback = (correlationData, ack, s) -> {
            log.info("correlationData={}",correlationData); //消息唯一id
            log.info("ack={}",ack);
            if (ack){
                log.info("更新订单状态");
            }else {
                log.error("投递失败处理,重发机制");
            }
    };
    public void sendOrder(OrderMessageDto message ) throws Exception{

        rabbitTemplate.setConfirmCallback(confirmCallback);

		log.info("业务数据入库");
		
        CorrelationData correlationData = new CorrelationData("test-hello"+ LocalDateTime.now().toString());
		
        rabbitTemplate.convertAndSend("order-exchange","order.test",message,correlationData);
    }

}
    

九 、Return 返回消息

Return 消息机制:
Return Listener 用于处理一些不可路由的消息
生产者通过指定一个Exchange 和 RoutingKey,把消息投递到某个队列中,然后消费者监听队列进行消息的消费操作
但在某些情况下,如果我们发送消息的时候,当前的Exchange不存在或者指定的RoutingKey路由不到,Return Listener 来监听这种不可达的消息
在这里插入图片描述

#rabbitmq 是否确认回调
spring.rabbitmq.publisher-returns=true
# true 监听器接收不可达消息进行处理,false broker自动删除消息 默认为false
spring.rabbitmq.template.mandatory=true
@Component
@Slf4j
public class RabbitSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    private final RabbitTemplate.ReturnCallback callback = (message, errorCode, text, exchange, routingKey)
            -> {
        log.error("return exchange :{},routingKey:{},message:{},code:{},text:{}"
            ,exchange,routingKey,message,errorCode,text);
        log.info("找不到对应的队列");
    };


    public void sendOrder(OrderMessageDto message ) throws Exception{

        rabbitTemplate.setReturnCallback(callback);

        CorrelationData correlationData = new CorrelationData("test-hello"+ LocalDateTime.now().toString());

        rabbitTemplate.convertAndSend("order-exchange","order.test",message,correlationData);
    }

}

十 、消息的签收(ack)与重回队列

消费端的手工ACK和NACK
ACK 消费成功,
NACK 消费失败 ,将消息重新投递回broker中队列的尾端( 重回队列)

public void onOrderMessage(@Payload OrderMessageDto messageDto, @Headers Map<String,Object> headers, Channel channel) throws Exception{
     log.info("message : {}",messageDto.toString());
     Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);

     if(messageDto.getOrderNumber().equals("")){
        /**
          * multiple 是否批量处理
          * requeue 是否重回队列
          * 将消息重新投递回broker中队列的尾端( 重回队列)
          */
         channel.basicNack(deliveryTag,false,true);
     }else{
         //手工ack 确认签收
         channel.basicAck(deliveryTag,false);
     }

消费端进行消费的时候,由于业务异常我们可以进行日志记录,然后进行补偿。
如果由于服务器宕机等严重问题,那我们就需要手工ack保障消费端消费成功。
消费端重回队列
消费端重回队列是为了对没有成功消费的消息,把消息重新投递会broker。
一般实际开发都会关闭重回队列,设置为false

十一 、消息限流(消费端限流)

场景:在rabbitmq中有上万条数据未被消费,我们启动消费端后会有巨量的消息推送过来,但是我们单个客户端无法同时处理这么多消息,导致服务器奔溃
RabbitMQ提供了一种qos(服务质量保证)功能,即在非自动确认消息的前提下,如果一定数量的消息(通过基于consume 或者 channel设置Qos的值)未被确认前,不进行消费新的消息

十二 、消息生存周期(TTL)

TTL(time to live):生存时间
RabbitMQ支持消息过期时间,在发送消息时指定消息的过期时间。
RabbitMQ支持队列的过期时间,从消息入队列开始计算,只要超过了队列的超时时间配置,那么消息就会自动清除
1、添加队列过期时间
在这里插入图片描述

2、 查看队列
在这里插入图片描述
可以看到test3-queue队列的消息过期时间为10000毫秒,队列的过期时间为20000毫秒
springboot下配置队列

spring.rabbitmq.listener.order.queue.name=test3-queue
spring.rabbitmq.listener.order.queue.durable=true
spring.rabbitmq.listener.order.exchange.name=test3-exchange
spring.rabbitmq.listener.order.exchange.type=topic
spring.rabbitmq.listener.order.exchange.durable=true
spring.rabbitmq.listener.order.exchange.ignoreDeclarationExceptions=true
spring.rabbitmq.listener.order.key=test3.*
@RabbitHandler
  @RabbitListener(
          bindings = @QueueBinding(
                  value = @Queue(
                          value = "${spring.rabbitmq.listener.order.queue.name}",
                          durable = "${spring.rabbitmq.listener.order.queue.durable}",
                          arguments = {
                                  @Argument(name="x-expires",value = "20000",type = "java.lang.Integer"),
                                  @Argument(name="x-message-ttl",value = "10000",type = "java.lang.Integer")
                                  }

                  ),
                  exchange = @Exchange(
                          value = "${spring.rabbitmq.listener.order.exchange.name}" ,
                          type = "${spring.rabbitmq.listener.order.exchange.type}" ,
                          durable = "${spring.rabbitmq.listener.order.exchange.durable}" ,
                          ignoreDeclarationExceptions = "${spring.rabbitmq.listener.order.exchange.ignoreDeclarationExceptions}"),
                  key = "${spring.rabbitmq.listener.order.key}"
          )
  )
  public void onOrderMessage(@Payload OrderMessageDto messageDto, @Headers Map<String,Object> headers, Channel channel) throws Exception{
      log.info("message : {}",messageDto.toString());
      Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);

      if(messageDto.getOrderNumber().equals("")){
          /**
           * multiple 是否批量处理
           * requeue 是否重回队列
           * 将消息重新投递回broker中队列的尾端( 重回队列)
           */

          channel.basicNack(deliveryTag,false,true);
      }else{
          //手工ack 确认签收
          channel.basicAck(deliveryTag,false);
      }

  }

在这里插入图片描述

十三 、死信队列

死信队列: DLX ,Dead-Letter-Exchange
利用DLX,当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange 就是DLX
DLX也是一个正常的Exchange,和普通的Exchange没有区别,他能在任何的队列上被指定,实际上就是设置某个队列的属性,当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列中,可以监听这个队列中的消息做相应的死信处理

死信:在队列中没有任何消费者消费的消息就是死信
消息变成死信的情况

1.消息被拒绝( basic.reject / basic.nack ) 并且request= false 
消息消费失败并且不重回队列
2.消息的TTL过期
3.当队列达到最大长度

死信队列的设置
首先设置死信队列的Exchange 和 Queue,然后进行绑定
例如 : Exchange :dlx.exchange 、Queue:dlx.queue 、 RoutingKey:# (路由任何规则)
然后正常的队列中添加参数( “x-dead-letter-exchange”, “dlx.exchange” )
这样消息在过期,消息被拒绝,队列到达最大长度的时候 消息就可以直接路由到死信队列,我们只需要监听死信队列的queue即可

在这里插入图片描述

创建死信队列

在这里插入图片描述
绑定死信队列

    @RabbitHandler
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(
                            value = "${spring.rabbitmq.listener.order.queue.name}",
                            durable = "${spring.rabbitmq.listener.order.queue.durable}",
                            arguments = {
                                    @Argument(name="x-expires",value = "20000",type = "java.lang.Integer"),
                                    @Argument(name="x-message-ttl",value = "10000",type = "java.lang.Integer"),
                                    @Argument(name="x-dead-letter-exchange",value = "exchange.dlx")
                            }

                    ),
                    exchange = @Exchange(
                            value = "${spring.rabbitmq.listener.order.exchange.name}" ,
                            type = "${spring.rabbitmq.listener.order.exchange.type}" ,
                            durable = "${spring.rabbitmq.listener.order.exchange.durable}" ,
                            ignoreDeclarationExceptions = "${spring.rabbitmq.listener.order.exchange.ignoreDeclarationExceptions}"),
                    key = "${spring.rabbitmq.listener.order.key}"
            )
    )
    public void onOrderMessage(@Payload OrderMessageDto messageDto, @Headers Map<String,Object> headers, Channel channel) throws Exception{
        log.info("message : {}",messageDto.toString());
        Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);

        if(messageDto.getOrderNumber().equals("")){
            /**
             * multiple 是否批量处理
             * requeue 是否重回队列
             * 将消息重新投递回broker中队列的尾端( 重回队列)
             */

            channel.basicNack(deliveryTag,false,true);
        }else{
            //手工ack 确认签收
            channel.basicAck(deliveryTag,false);
        }

    }

绑定效果
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值