RabbitMQ 基础篇 - 面试常问

934 阅读7分钟

前言

随着项目业务不断扩展,在全面微服务化的环境下,消息队列已经成为项目中非常重要的角色。如果你还不清楚为什么要引入消息队列。那么可以阅读 为什么使用消息队列?。身为 RabbitMQ 的钟爱粉,本篇文章讲解 RabbitMQ 面试常问的基础。

RabbitMQ 简介

RabbitMQ 是最受欢迎的开源消息中间件之一,基于 erlang 语言开发,支持多种消息传输协议,拥有极低的消息延迟,并发能力和性能很好,几乎提供了所有语言的 Client API。更是拥有丰富的 web 管理界面。在 为什么使用消息队列? 一文中已经列出了几种开源消息中间件的优缺点比较。除了吞吐量稍微次之(不过对于绝大多数中小型公司来说已经足够),相对来说是一个非常棒的消息中间件,广泛用于中小型企业。

RabbitMQ 架构图

image.png

先了解一下 RabbitMQ 发送和消费消息的过程,生产者先和 RabbitMQ 服务器建立 TCP 连接 ConnectionConnection 中有许多 channel (这其实和 IO 多路复用类似,多个 channel 复用一个 Connection ,如果你不理解 IO 多路复用请阅读 Redis 线程模型 — 怎样理解 IO 多路复用?),通过 channel 传递消息到 RabbitMQ 服务器,服务器内部通过交换机来转发消息,消息最终落入哪个队列由 Binding 来决定,Binding 代表交换机和队列之间的一个绑定,这样不同的交换机就能匹配到对应的队列。而交换机和队列又被一个个的虚拟机 Virtual Host 分隔开,这个 Virtual Host 和 RabbitMQ Server 可以理解为 MySQL 和 MySQL 中的不同数据库。消费者和生产者一样需要先和 RabbitMQ 服务器建立连接 Connection ,内部也是通过 channel 来通信,去订阅不同的队列来消费消息。

RabbitMQ 工作模式

Publish/Subscribe 发布订阅

发布订阅模式就是通俗的广播类型,用于把消息发送到所有与之绑定的 Queue

image.png

发布订阅模式在开发中应用非常广泛,例如电商业务下单成功,此时需要广播一个订单创建的消息,库存、优惠券、商户等系统都需要订阅这个消息去处理下游业务。

Work queues 竞争消费

image.png

想一想生产环境一个微服务一般都是多台实例集群。我们要保证一个微服务的多个实例只能消费一次消息,否则就发生重复消费了。如上图:假设 C1、C2 是库存系统的两个实例,那么两个库存系统的实例只能消费一次消息。很简单,只要两个微服务监听的是同一个队列,它们就只能有一个微服务能消费成功。RabbitMQ 已经帮我们做好了控制,也正因为如此,RabbitMQ 不支持队列层面的广播,而是交换机层面的广播。

RoutingKey 路由键匹配

image.png

交换机和队列通过路由键建立关系,形成一个 Binding 对象绑定在一起。交换机把消息发送到 与之 RoutingKey 相匹配的所有队列。有些地方可能会把 BingdingKeyRoutingkey 分开,但是据官方提供的客户端依赖包以及 SpirngBoot 提供的依赖包都把这两个都当成 RoutingKey 来理解。如果一定要分开,可以这么理解。BingdingKey 是将交换机和队列绑定时需要的,RoutingKey 是交换机转发消息的时候需要的,当 RoutingKeyBindingKey 匹配上,消息就能正确发送到队列。

上图交换机通过路由键 key1 绑定了两个队列,那么当交换机发送消息时指定 key1 路由键的话,两个队列都能收到消息。如果指定 key2 或者 key3 为路由键只有下面的队列能收到消息。实际业务场景中很常见,有时我们发送一个消息希望多个系统去消费,但有时发送多个消息只希望其中一个系统消费,就可以使用这种路由匹配模式。

topics 通配符匹配

image.png

这种工作模式和 Routing 路由键匹配几乎一样,不同的是 topics 可以使用通配符 “*” 和 “#” 来模糊匹配路由键。“*” 可以恰好代替一个单词;“#” 可以恰好代替零个或多个单词。为路由键匹配提供了高扩展性。

消息持久化

由于 RabbitMQ 是一个独立于两个系统之外的第三方服务,我们必须得考虑的一种情况就是 RabbitMQ 服务宕机怎么办?为了解决 RabbitMQ 重启后,内存队列消息丢失问题,RabbitMQ 提供了持久化功能。

值得注意的是必须队列和消息都要设置持久化,单独设置队列持久化,消息不持久化,重启之后队列还在,但是消息就没了。单独设置消息持久化,不设置队列持久化,重启之后队列都没了,消息自然也没了。

但是一旦开启持久化就意味着这对 RabbitMQ 的性能会带来损耗,因为持久化消息要写入磁盘文件,所以是否所有的消息和队列都开启持久化需要我们在实际场景中去权衡。

TTL 消息过期设置

TTL 全称 Time To Live —— 存活时间。RabbitMQ 中既可以对队列中整个消息设置存活时间,也可以对单条消息设置存活时间。如果两者都设置,那么将以时间小的为准。

给整个队列设置消息存活时间,构建队列的时候设置 ttl 即可,单位毫秒。所有发送到该队列的消息指定时间之后将会过期。

值得注意的是,当对某一条消息设置过期时间之后,这条消息并不一定在设置的时间之后就会消失,而是惰性删除。当消息处于队列顶端,也就是即将被消费时才会判断是否过期,类似于 Redis 的 key 过期策略之一 惰性删除。否则要开启一个调度每隔一定时间循环队列中的消息判断是否过期,效率很差。

RabbitMQ 死信队列

死信队列在 RabbitMQ 中叫做 Dead Letter Exchange 简称 DLX,实际上死信队列是死信交换机和死信队列的简称。如果我们把普通队列绑定一个死信交换机,那么当消息成为死信之后不会被丢弃,而是发送给这个死信交换机,进而路由到死信交换机绑定的队列。

image.png

死信交换机和死信队列并不是一个特殊的组件,它和正常的交换机和队列一样声明。使用时只要设置它的属性参数即可 x-dead-letter-exchange

正如上图所示,如果队列绑定了死信交换机,那么当消息成为死信之后不会被丢弃,而是转到死信交换机路由到对应的队列。那么消息如何成为死信呢?通常会在这三种情况下

  • 队列消息到达长度限制。RabbitMQ 是可以设置队列长度的,比如设置队列长度为 10,当队列中积满 10 条消息,再过来一条到达最大长度限制,那么这条消息就会成为死信
  • 消费者拒绝签收消息,并且不重回队列,requeue = false
  • 消息超过存活时间没有被消费

RabbitMQ 延迟消息

延迟消息在业务中使用非常广泛,比如用户下单 30 分钟内未支付,订单关闭。发货之后用户 15 天未收货自动确认收货等业务场景。RabbitMQ 中可以通过设置 消息存活时间+死信队列 来实现发送延迟消息。

image.png

如上图,我们可以将消息发送到一个没有消费者订阅的队列,给这个队列和消息设置存活时间 30 分钟,当 30 分钟后消息过期,成为死信消息,会到达死信交换机 DLX 进而路由到死信队列 Queue,然后由消费者订阅消费。这样就实现了延迟消息功能!

如果这篇文章对你有帮助,记得点赞加关注!你的支持就是我继续创作的动力!