RabbitMQ 模式、消息队列知识详解

1,968 阅读14分钟

RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。

AMQP :Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。

1.RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。具体特点包括:

2.可靠性(Reliability) RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。

3.灵活的路由(Flexible Routing) 在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。

4.消息集群(Clustering) 多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。

5.高可用(Highly Available Queues) 队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。

6.多种协议(Multi-protocol) RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。

7.多语言客户端(Many Clients) RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。

8.管理界面(Management UI) RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。

9.跟踪机制(Tracing) 如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。

10.插件机制(Plugin System) RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。

RabbitMQ的几种模式

基本消息模型

20190610232537261.png

在上图的模型中,有以下概念:

P:生产者,也就是要发送消息的程序

C:消费者:消息的接受者,会一直等待消息到来。

queue:消息队列,图中红色部分。可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。

工作消息模型

20190611201227139.png

work queues与入门程序相比,多了一个消费端,两个消费端共同消费同一个队列中的消息,但是一个消息只能被一个消费者获取。

这个消息模型在Web应用程序中特别有用,可以处理短的HTTP请求窗口中无法处理复杂的任务。

接下来我们来模拟这个流程:

P:生产者:任务的发布者

C1:消费者1:领取任务并且完成任务,假设完成速度较慢(模拟耗时)

C2:消费者2:领取任务并且完成任务,假设完成速度较快

C1 与 C2 会平均的消费消息

Publish/subscribe(交换机类型:Fanout,也称为广播 )

20190611200955466.png

和前面两种模式不同:

1) 声明Exchange,不再声明Queue

2) 发送消息到Exchange,不再发送到Queue

1、publish/subscribe与work queues有什么区别。

区别:

1)work queues不用定义交换机,而publish/subscribe需要定义交换机。

2)publish/subscribe的生产方是面向交换机发送消息,work queues的生产方是面向队列发送消息(底层使用默认交换机)。

3)publish/subscribe需要设置队列和交换机的绑定,work queues不需要设置,实际上work queues会将队列绑定到默认的交换机 。

相同点:

所以两者实现的发布/订阅的效果是一样的,多个消费端监听同一个队列不会重复消费消息。

2、实际工作用建议还是建议使用 publish/subscribe,发布订阅模式比工作队列模式更强大(也可以做到同一队列竞争),并且发布订阅模式可以指定自己专用的交换机。

Routing 路由模型(交换机类型:direct)

20190611151532501.png  

P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。

X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列

C1:消费者,其所在队列指定了需要routing key 为 error 的消息

C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息

Topics 通配符模式(交换机类型:topics)

20190611154905286.png

 每个消费者监听自己的队列,并且设置带统配符的routingkey,生产者将消息发给broker,由交换机根据routingkey来转发消息到指定的队列。

Routingkey一般都是有一个或者多个单词组成,多个单词之间以“.”分割,例如:inform.sms

通配符规则:

#:匹配一个或多个词

*:匹配不多不少恰好1个词

举例:

audit.#:能够匹配audit.irs.corporate 或者 audit.irs

audit.*:只能匹配audit.irs

RabbitMQ管理

启动

#1.服务启动相关
#启动
systemctl start rabbitmq-server

#重启
systemctl restart rabbitmq-server

#停止
systemctl stop rabbitmq-server

#查看状态
systemctl status rabbitmq-server

#2.管理命令行 用来不使用web管理界面情况下的命令操作RabbitMQ
rabbitmqctl help 可以查看更多命令

#3.插件管理命令行
rabbitmqplugins enable|list|disable

RabbitMQ传递对象

RabbitMQ是消息队列,发送和接收的都是字符串/字节数组类型的消息

消息队列可以发送字符串、字节数组、序列化对象

传递对象只需要序列化对象 或转化json字符串 即可

Boot 交换机与队列创建

创建一个配置类,用@Configuration标注

声明一个队列

/**
		
         * dureable:是否持久化,默认是false, 持久化队列: 会被存储在磁盘上,当消息代理重启时仍然存在
         * exclusive: 默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列会被关闭
         * autoDelete:是否自动删除,当没有生产者或者消费者使用队列,该队列会自动删除
*/

public Queue newQueue(){
    return new Queue("队列名",dureable,exclusive,autoDelete)
}

声明一个订阅交换机

@Bean
public FanouExchange newFanouExchange(){
    return new FanouExchange("交换机名")
}

声明路由模式交换机

@Bean
public DirectExchange newDirectExchange(){
    return new DirectExchange("交换机名")
}

绑定队列 需要队列和交换机 (如果需要绑定多个队列,只需要在重复操作即可)

@Bean
public Binding bindingDirectExchange(Queue 上方定义好的队列方法名,DirectExchange 上方定义好的交换机方法名){
    return BindingBuilder.bind(Queue).to(DirectExchange).with("路由模式的key");
}

消息的可靠投递

RabbitMQ事务(不使用)

当在消息发送过程中添加了事务,处理效率降低几十倍甚至上百倍

channel.txSelect() 开启事务

channel.txCommit() 提交事务

channel.txRollback() 事务回滚

消息确认和return机制

消息确认机制:确认消息提供者是否成功发送消息到交换机

return机制:确认消息是否成功的从交换机分发到队列

在spring boot中配置

1,yml添加开启机制

 publisher-confirm-type: simple ##开启消息确认机制
 publisher-returns: true  #使用return监听

2,创建监听类

@Component
public class MsgConfirmAndReturn implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {

    Logger logger = LoggerFactory.getLogger(MsgConfirmAndReturn.class);

    @Resource
    public RabbitTemplate rabbitTemplate;

    //设置当前类给rabbitTemplate
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {
        //消息确认
        if (b) {
            logger.info("-----消息成功发送到交换机-----");
        } else {
            logger.warn("-----消息发送到交换机失败-----");
        }


    }

    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        //return监听(当交换机分发消息到队列时执行)
        logger.warn("-----交换机分发消息失败-----");
    }
}

消息消费手动确认

1,yml开启手动确认机制

rabbitmq:
  listener:
  	 ##开启手动确认机制
     acknowledge-mode: manual

开启后如果消息手动确认后如果消息未被确认,该消息将一直停留在队列

//消息进行手动确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);

如何保证消息可靠性

(W3RRZ`@2F[6OE4HX]@AN.png

1、消息丢失 消息发送出去,由于网络问题没有抵达服务器

  • 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描 重发的方式

  • 做好日志记录,每个消息状态是否都被服务器收到都应该记录

  • 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发

消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。

  • publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。

自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机

  • 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队

2、消息重复 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者 消息消费失败,由于重试机制,自动又将消息发送出去

成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送

  • 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志

  • 使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理

  • rabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的

3、消息积压

消费者宕机积压

消费者消费能力不足积压

发送者发送流量太大

  • 上线更多的消费者,进行正常消费
  • 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

rabbitmq basicReject / basicNack / basicRecover区别

channel.basicReject(deliveryTag, true);

basicReject方法拒绝deliveryTag对应的消息,第二个参数是否requeue,true则重新入队列,否则丢弃或者进入死信队列。该方法reject后,该消费者还是会消费到该条被reject的消息。

channel.basicNack(deliveryTag, false, true);

basic.nack方法为不确认deliveryTag对应的消息,第二个参数是否应用于多消息,第三个参数是否requeue,与basic.reject区别就是同时支持多个消息,可以nack该消费者先前接收未ack的所有消息。nack后的消息也会被自己消费到。

channel.basicRecover(true);

basic.recover是否恢复消息到队列,参数是是否requeue,true则重新入队列,并且尽可能的将之前recover的消息投递给其他消费者消费,而不是自己再次消费。false则消息会重新被投递给自己。

消息消费幂等问题

RabbitMQ消息自动重试机制 1.当我们消费者处理执行我们业务代码的时候,如果抛出异常的情况下 在这时候mq_会自动触发重试机制,默认的情况下rabbitmq是无限次数的重试。需要人为指定重试次数限制问题

##配置重试机制
rabbitmq:
  listener:
      simple:
        retry:
          ##开启消费者(程序出现问题)进行重试
          enabled: true
          ##重试次数 (重试完后将丢掉该未被成功消费的消息)
          max-attempts: 5
          ##重试间隔时间
          initial-interval: 3000

2.在什么情况下消费者需要实现重试策略?

A. 消费者获取消息后,调用第三方接口,但是调用第三方接口失败呢?是否需要重试? 该情况下需要实现重试策略,网络延迟只是暂时调用不通,重试多次有可能会调用通。

B. 消费者获取消息后,因为代码问题抛出数据异常,是否需要重试?

该情况下是不需要实现重试策略,就算重试多次,最终还是失败的。可以将日志存放起来,后期通过定时任务或者人工补偿形式。如果是重试多次还是失败消息,需要重新发布消费者版本实现消费可以使用死信队列

Mq在重试的过程中,有可能会引发消费者重复消费的问题。

Mq消费者需要解决幂等性问题 幂等性保证数据唯一

死信队列

当一条消息在队列中出现以下三种情况的时候,该消息就会变成一条死信。

  • 消息被拒绝(basic.reject / basic.nack),并且requeue = false
  • 消息TTL过期
  • 队列达到最大长度

当消息在一个队列中变成一个死信之后,如果配置了死信队列,它将被重新publish到死信交换机,死信交换机将死信投递到一个队列上,这个队列就是死信队列。

创建队列的一些属性


 /**
     * 创建死信队列 DLX
     * @return
     */
    @Bean
    public Queue orderDelayQueue(){
        HashMap<String, Object> map = new HashMap<String, Object>();
        map.put("x-message-ttl",10000);
        map.put("x-dead-letter-exchange",ORDER_DELAY_EXCHANGE);
        map.put("x-dead-letter-routing-key","k2");
        
        return new Queue(ORDER_DELAY_QUEUE,true,false,false,map);

    }

队列的一些属性参数
	 (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。仲裁队列类型仅支持drop-head;
         (6)x-dead-letter-exchange:死信交换器名称,过期或被删除(因队列长度超长或因空间超出阈值)的消息可指定发送到该交换器中;
         (7)x-dead-letter-routing-key:死信消息路由键,在消息发送到死信交换器时会使用该路由键,如果不设置,则使用消息的原来的路由键值
         (8)x-single-active-consumer:表示队列是否是单一活动消费者,true时,注册的消费组内只有一个消费者消费消息,其他被忽略,false时消息循环分发给所有消费者(默认false)
         (9)x-max-priority:队列要支持的最大优先级数;如果未设置,队列将不支持消息优先级;
         (10)x-queue-mode(Lazy mode):将队列设置为延迟模式,在磁盘上保留尽可能多的消息,以减少RAM的使用;如果未设置,队列将保留内存缓存以尽可能快地传递消息;
         (11)x-queue-master-locator:在集群模式下设置镜像队列的主节点信息。
        

延迟队列

AMQP协议和RabbitMQ队列本身是不支持延迟队列功能的,但是可以通过TTL(Time To Live) 特性模拟延迟队列的功能

TTL就是消息的存活时间。RabbitMQ可以分别对队列和消息设置存活时间

在创建队列的时候可以设置队列的存活时间,当消息进入到队列并且在存活时间内没有消费者消费,则此消息就会从当前队列被移除;

创建消息队列没有设置TTL,但是消息设置了TTL,那么当消息的存活时间结束,也会被移除;

image-20210421120355578.png

实现延迟队列

1,创建路由交换机

2,创建两个队列,一个普通队列 设置k1,一个死信队列 设置k2

  • x-dead-letter-exchange:超时后转发的交换机
    
  • x-dead-letter-routing-key :超时后转发的交换机的key
    
  • x-message-ttl:超时时间
    

3,进行绑定

4,发送消息到 死信队列 k2,而接收消息 k1

springboot整合rabbitmq

启动rabbitmq

#启动
systemctl start rabbitmq-server
#重启
systemctl restart rabbitmq-server
#停止
systemctl stop rabbitmq-server
#查看状态
systemctl status rabbitmq-server

启动成功后 ip地址+15672访问

创建队列必须要有消费者,如果没有消费者的话那队列就不会发送消息

导入依赖

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

yml

spring: 
	 rabbitmq:
    	host: 192.168.141.124 #主机IP
    	port: 5672 #端口
    	username: root #用户名
    	password: root #密码

消息发送

##注入模板
@Autowired
private RabbitMQTemplate RabbitMQTemplate;


##直接发送消息到队列
amqpTemplate.convertAndSend("队列名",消息)

##发送消息到交换机(订阅交换机)
amqpTemplate.convertAndSend("交换机名" ,"" ,消息)

##发送消息到交换机(路由交换机)
amqpTemplate.convertAndSend("交换机名" ,路由key ,消息)


//接收消息
@Component
public class HelloCustomer {
    @RabbitListener(queuesToDeclare = @Queue(value = "队列名"))
    public void a(String msg){
        System.out.println("msg = " + msg);
    }
}