RabbitMQ小结

187 阅读10分钟

一、什么是RabbitMQ

RabbitMQ是一个基于AMQP协议的开源消息代理的中间件,采用Erlang语言实现,具有高可用(支持集群部署)、高可靠性(统一高效的协议AMQP、消息持久化)。主要应用场景:服务解耦、异步通信、流量削峰

二、什么是AMQP

AMQP是基于TCP/IP协议实现面向消息,定义消息可靠交互的协议,全名是Advanced Message Queuing Protocal(高级消息队列协议)

三、RabbitMQ主要概念

  1. 交换机(exchange):接收消息、分发消息
  2. 队列(queue):存储消息
  3. 路由(routing key):交换机分发策略限制条件

四、RabbitMQ的7种模式

  1. 简单模式(Simple)

image.png

1. 消息一对一:一个生产者和一个消费者直接通信,消息通过queue传递,消息只被消费一次
2. 适用场景:消息只被单个消费者处理的场景,比如日志记录等 

2. 发布订阅模式(Publish/Subscribe)

image.png

1. 消息广播:生产者发布消息到Fanout交换机,消息被广播到交换机绑定的所有队列
2. 适用场景:实时通知、多用户审核;一条消息同时被多个消费者消费的场景,例如用户注册后需要发布注册成功等待审核的邮件给用户,同时需要发布短信通知系统管理员进行审核,此时可以定义个email和sms队列,绑定到同一个交换机,注册成功发布一条消息到交换机

3. 工作模式(Work Queues)

image.png

1. 轮询模式:多个消费者监听同一个队列,消息被平均分配给所有消费者(自动ack)
2. 公平模式(能者多劳) :多个消费者监听同一个队列,消息根据不同消费者消费速度,消费快的消费者消息分配权重大(手动ack,需指定每次从队列中获取几条消息)
3. 适用场景:需要资源调度、负载均衡的场景,发挥每台服务器的最大性能

4. 路由模式(Routing)

image.png

1. 生产者发布消息到Direct交换机,消息根据routing key(路由key)分发到不同的队列
2. 适用场景:日志记录、系统各种类型的消息通知;一个系统一般会有短信、邮件、微信、站内信等通知方式,这种情形就可以给交换机绑定不同队列定义一个routing key,发送消息的时候携带routing key,交换机根据routing key进行消息分发

5. 主题模式(Topics)

image.png

1. 生产者发布消息到Direct交换机,消息根据routing key进行通配符匹配分发到不同队列
2. 通配符含义:
    #:表示可以0个、多个/多级字符串
    *: 表示至少且只能有一个/一级字符串
    举例:*.com.#表示test1.com、test2.com.test2、test3.com.test3.test3都能匹配上,test4.test4.com、com.test5则无法匹配上,因为*代表只能至少且有一级字符串
3. 适用场景:需要复杂消息过滤

6. 远程过程调用模式(RPC)

image.png

1. 远程服务端消费者消费消息后写入消息到另外一个队列中通知生产者,响应消息处理结果
2. 适用场景:需要同步通信的场景

7. 发布确认模式(Publisher Confirms)

1. 生产者将信道设置为Confirm模式,所有在该信道发送的消息都会分配一个唯一id,一旦消息被成功投递到对应的队列,Broker会发送确认给生产者
2. 适用场景:确保消息可靠生产,即成功发布到MQ服务器

五、RabbitMQ常见面试题

  1. 如果没有给队列绑定交换机,则队列会绑定到默认交换机,默认交换机是Direct交换机
  2. 内存和磁盘不足的时候,所有消息队列都会处于Blocking状态,生产者无法发布消息,消费者可以继续消费消息
  3. 引发Blocking可能的原因:消息手动ack在出现异常时容易导致死循环,进而导致内存或者磁盘空间不足;内存和磁盘空间确实不足
  4. 如何避免消息消费时死循环
    1. 自动ack:可能丢消息
    2. 设置消息重试次数:缺点会丢消息
    3. try catch + 手动ack、nack:缺点会丢消息
    4. try catch + 手动ack、nack + 死信队列
  5. rabbitmq内存默认设置:默认是相对值比例0.4,本机内存*0.4,如果本机内存是8GB,即rabbitmq内存是3.2GB;也可以修改配置为绝对值具体内存大小多少MB、GB;相对值比例更加灵活
  6. rabbitmq内存翻页设置:默认是50%,当rabbitmq内存消耗接近最大值时,rabbitmq会将本机内存x0.4x0.5的数据写入磁盘,保证rabbitmq的高可靠
  7. rabbitmq磁盘默认设置:默认是50MB,当磁盘空间小于50MB时会预警
  8. 延时队列的实现:设置队列的ttl+死信队列、设置消息的ttl+死信队列
  9. 为什么rabbitmq不直接用TCP协议,而是使用AMQP协议
    1. TCP连接交互需要三次握手、四次挥手,开销大,存在性能瓶颈,无法支持每秒成千上万的消息处理场景
    2. AMQP协议通过创建一个TCP长连接,在长连接中创建多个可复用channel进行通信(有点类似线程池的概念),channel是一个轻量化的连接,因此可以创建成千上万的channel,从而多个线程可以共享一个TCP连接,大大减少开销;当通信结束需要销毁连接,直接关闭channel即可,无需关闭底层的TCP链接
  10. 队列在生产者还是消费者端创建
    1. 个人想法,无论是在web页面创建、生产者、消费者哪一地方创建都可以,只要保证先创建后使用即可
    2. 生产者和消费者端都创建:如果只在生产者端创建,需要保证生产者服务先于消费者服务启动,否则消费者先启动会报错;如果只在消费者端创建,需要保证消费者服务先于生产者服务启动,否则生产者先启动会报错;最稳妥就是都创建
  11. 如何保证消息不丢失
    1. 服务器足够的内存和磁盘空间
    2. 交换机持久化、队列持久化、消息持久化
    3. 可靠生产:生产者开启消息发送确认模式,发送消息同时冗余消息到mysql数据库等,然后根据rabbitmq服务返回的ack或者nack修改冗余消息状态为成功或者失败,同时开启定时任务对状态失败的消息进行重发,确保消息正常投递到rabbitmq服务器
    4. 可靠消费:try catch + 手动ack + 死信队列,死信队列对消息进行兜底处理,无法兜底处理时可以记录下来并通知开发人员进行处理
  12. Rabbitmq消息发送的流程是怎么样的?以SpringBoot为例
  • 生产者
    1. 引入maven依赖
    2. 修改项目配置文件,配置rabbitmq的连接信息、是否开启消息确认模式等
    3. 编写rabbitmq配置文件,注册交换机,注册队列,绑定交换机和队列的关系
    4. 注入RabbitTemplate对象,通过rabbitTemplate.send()或者rabbitTemplate.convertAndSend()方法发送消息
  • 消费者
    1. 引入maven依赖
    2. 修改项目配置文件,配置rabbitmq的连接信息、是否手动ack等
    3. 使用@RabbitListener、@RabbitHandler监听处理消息
  1. rabbitTemplate.send()和rabbitTemplate.convertAndSend()的区别
    1. send直接发送Message对象,可以定义各种消息属性;convertAndSend对消息对象按照指定序列化器进行序列化之后再发送
    2. convertAndSend默认序列化是SimpleMessageConverter(使用JDK序列化),发送的Object对象,对象需要实现Serializable接口,需要无参构造函数,getter/setter方法
    3. 也可以选择自定义序列化器,使用@Bean注册一个MessageConverter,一旦自定义序列化,生产者和消费者都需要使用一样的序列化配置
       @Configuration
       public class MqConfig {
           @Bean
           public MessageConverter messageConverter() {
               return new Jackson2JsonMessageConverter();
           }
       }
    
  2. 交换机有哪些类型
    1. Direct:精准匹配,根据路由key精准匹配队列
    2. Topic:模糊匹配,根据路由key通配符匹配,*表示0个或者1个字符串,#表示0个或者n个字符串,字符串按照.分割
    3. Fanout:广播模式,不考虑路由key,所有绑定的队列都能收到消息
    4. Header: 请求头模式,不考虑路由key,根据请求头信息进行消息投递
  3. 如何实现手动ack
    1. 配置文件开启手动ack模式,SpringAMQP提供了3种确认方式,实际Rabbitmq只有2种确认方式(手动ack和自动ack)
      • none:自动ack,rabbitmq消息进入发送缓存区就直接删掉,这时候消费者还没获取完整数据或者数据没处理完就挂掉会导致消息丢失
      • manual:手动ack,结合try catch+channel.ack(deliveryTag)、channel.nack(deliveryTag),实现消息的手动确认;deliveryTag是消息的唯一标识,可以通过@Header(AmqpHeader.DELIVERY_TAG) long deliveryTag获取;注意nack重回队列容易造成死循环,一般会设置一个重试次数,代码手动维护这个重试次数,达到阈值就直接丢弃消息
      • auto:由spring判断如何提交,方法正常处理自动ack,方法出现异常自动nack(默认nack是重回队列,容易造成死循环)
    2. 代码中使用try catch+channel.ack()、channel.nack()实现消息的手动确认
  4. 如何实现延时队列?2种方式,ttl队列+死信队列、ttl消息+死信队列
1.ttl队列+死信队列
  1.先创建死信交换机,死信队列,绑定死信交换机和队列的关系
  2.创建一个交换机;再创建一个具有ttl的普通队列,同时设置对应的死信交换机、路由key即可;然后绑定交换机和ttl队列即可
  3.消息发送到普通队列,时间超时会自动进入死信队列,业务逻辑监听死信队列处理即可

2.ttl消息+死信队列
  1.先创建死信交换机,死信队列,绑定死信交换机和队列的关系
  2.创建一个交换机;再创建一个普通队列,同时设置对应的死信交换机、路由key即可;然后绑定交换机和普通队列即可
  3.消息发送到普通队列的时候指定消息ttl,时间超时会自动进入死信队列,业务逻辑监听死信队列处理即可
  1. SpringAMQP的两种容器模式,simple和direct
1.simple模式:使用线程池,一个队列可以被多个线程并发轮询,支持批处理(批量ack)
2.direct模式:没有使用线程池,一个client连接中只有一个线程处理所有队列,不支持批处理(无法批量ack)
  1. rabbitTemplate.send()和rabbitTemplate.convertAndSend()的区别
1.send直接发送Message对象,可以定义各种消息属性;convertAndSend对消息对象按照指定序列化器进行序列化之后再发送
2.convertAndSend默认序列化是SimpleMessageConverter(使用JDK序列化),发送的Object对象,对象需要实现Serializable接口
3.也可以选择自定义序列化器,使用@Bean注册一个MessageConverter,一旦自定义序列化,生产者和消费者都需要使用一样的序列化配置
@Configuration
public class MqConfig {
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}