AMQP 0-9-1 模型详解
概述
本指南概述了 AMQP 0-9-1 协议,这是 RabbitMQ 支持的协议之一。
AMQP 0-9-1 与 AMQP 模型高层概述
什么是 AMQP 0-9-1?
AMQP 0-9-1(高级消息队列协议)是一种消息传递协议,它使符合规范的客户端应用程序能够与符合规范的消息中间件代理(Broker)进行通信。
代理及其角色
消息代理从 发布者(发布消息的应用程序,也称为生产者)接收消息,并将它们路由到 消费者(处理这些消息的应用程序)。
由于这是一个网络协议,发布者、消费者和代理都可以位于不同的机器上。
AMQP 0-9-1 模型简介
AMQP 0-9-1 模型有如下世界观:消息被发布到 交换机(Exchanges),通常将其比作邮局或邮箱。交换机随后使用称为 绑定(bindings) 的规则将消息副本分发给 队列(queues)。接着,代理会将消息投递给订阅了队列的消费者,或者由消费者根据需要从队列中获取/拉取消息。

发布消息时,发布者可以指定各种 消息属性(消息元数据)。其中一些元数据可能由代理使用,其余部分对代理而言则是完全透明的,仅供接收消息的应用程序使用。
网络是不可靠的,应用程序处理消息可能会失败,因此 AMQP 0-9-1 模型引入了 消息确认(message acknowledgements) 的概念:当消息投递给消费者时,消费者会 通知代理(自动或由应用开发者决定何时执行)。启用消息确认后,代理只有在收到该消息(或一组消息)的确认通知时,才会彻底从队列中删除该消息。
在某些情况下,例如当消息无法路由时,消息可能会被 退回 给发布者、丢弃,或者如果代理实现了扩展,则会被放入所谓的“死信队列”。发布者通过在发布消息时使用特定参数来选择如何处理此类情况。
队列、交换机和绑定统称为 AMQP 实体。
AMQP 0-9-1 是一种可编程协议
AMQP 0-9-1 是一种可编程协议,这意味着 AMQP 0-9-1 实体和路由方案主要由应用程序自身定义,而非由代理管理员定义。因此,协议提供了用于声明队列和交换机、定义它们之间的绑定、订阅 队列 等操作。
这赋予了应用开发者很大的自由度,但也要求他们注意潜在的定义冲突。实际上,定义冲突很少见,且通常意味着配置错误。
应用程序声明所需的 AMQP 0-9-1 实体,定义必要的路由方案,并可以选择在不再使用时删除这些 AMQP 0-9-1 实体。
交换机与交换机类型
交换机 是 AMQP 0-9-1 中接收消息的实体。交换机接收一条消息并将其路由到零个或多个队列中。所使用的路由算法取决于 交换机类型 和称为 绑定 的规则。AMQP 0-9-1 代理提供四种交换机类型:
| 交换机类型 | 预声明的默认名称 |
|---|---|
| 直连交换机 | (空字符串) 和 amq.direct |
| Fanout 交换机 | amq.fanout |
| Topic 交换机 | amq.topic |
| Headers 交换机 | amq.match (以及 RabbitMQ 中的 amq.headers) |
除了交换机类型外,交换机在声明时还包含许多属性,其中最重要的是:
- 名称
- 持久性(Durability,交换机在代理重启后依然存在)
- 自动删除(Auto-delete,当最后一个队列解绑时,交换机会被删除)
- 参数(Arguments,可选,供插件和特定代理功能使用)
交换机可以是持久的或临时的。持久交换机在代理重启后依然存在,而临时交换机则不会(它们必须在代理重新上线后重新声明)。并非所有场景和用例都要求交换机是持久的。
默认交换机
默认交换机是一个由代理预先声明的、没有名称(空字符串)的直连交换机。它有一个使其对简单应用非常实用的特性:每个创建的队列都会自动以队列名称作为路由键绑定到该交换机上。
例如,当你声明一个名为“search-indexing-online”的队列时,AMQP 0-9-1 代理会自动将其绑定到默认交换机,并使用“search-indexing-online”作为路由键(在此上下文中也称为绑定键)。因此,发布到默认交换机且路由键为“search-indexing-online”的消息将被路由到“search-indexing-online”队列。换句话说,默认交换机使得看起来可以直接向队列发送消息,尽管从技术上讲并非如此。
在 RabbitMQ 中,默认交换机不允许绑定/解绑操作。向默认交换机执行绑定操作将导致错误。
直连交换机 (Direct Exchange)
直连交换机根据消息的路由键将消息投递到队列。直连交换机非常适合消息的单播路由,但也适用于多播路由。
其工作原理如下:
- 队列以路由键 K 绑定到交换机
- 当带有路由键 R 的新消息到达直连交换机时,如果 K = R,交换机将其路由到该队列
- 如果多个队列以相同的路由键 K 绑定到直连交换机,交换机会将消息路由到所有满足 K = R 的队列
直连交换机的图形化表示如下:

扇形交换机 (Fanout Exchange)
扇形交换机将消息路由到所有绑定到它的队列,路由键会被忽略。如果 N 个队列绑定到扇形交换机,当有新消息发布到该交换机时,消息的副本会发送给所有 N 个队列。扇形交换机非常适合消息的广播路由。
因为扇形交换机会将消息副本投递给每个绑定的队列,其用例通常包括:
- 大型多人在线 (MMO) 游戏可将其用于排行榜更新或其他全局事件
- 体育新闻网站可以使用扇形交换机向移动客户端近乎实时地分发比分更新
- 分布式系统可以广播各种状态和配置更新
- 群聊可以使用扇形交换机在参与者之间分发消息(尽管 AMQP 没有内置在线状态的概念,因此 XMPP 可能是更好的选择)
扇形交换机的图形化表示如下:

主题交换机 (Topic Exchange)
主题交换机根据消息路由键与用于绑定队列的模式之间的匹配,将消息路由到一个或多个队列。主题交换机常用于实现各种发布/订阅模式的变体,通常用于消息的多播路由。
主题交换机有着非常广泛的应用场景。当问题涉及多个需要选择性接收特定类型消息的消费者/应用程序时,应考虑使用主题交换机。
使用示例:
- 分发与特定地理位置相关的数据,例如销售点
- 由多个 worker 完成的后台任务处理,每个 worker 能够处理特定的一组任务
- 股票价格更新(以及其他金融数据更新)
- 涉及分类或标记的新闻更新(例如,仅针对特定运动或球队)
- 云中各种服务的编排
- 分布式架构/特定操作系统软件构建或打包,其中每个构建器只能处理一种架构或操作系统
头部交换机 (Headers Exchange)
头部交换机旨在根据多个属性进行路由,这些属性以消息头部而非路由键的形式表达更为简单。头部交换机忽略路由键属性。取而代之的是,用于路由的属性取自 headers 属性。如果头部值与绑定时指定的值相等,则认为该消息匹配。
可以使用多个头部来绑定队列以进行匹配。在这种情况下,代理需要从开发者处获取额外信息,即:是要求匹配任意一个头部,还是匹配所有头部?这就是“x-match”绑定参数的作用。当“x-match”设置为“any”时,只需一个匹配的头部值即可。或者,将“x-match”设置为“all”则要求所有值必须匹配。
对于“any”和“all”,以 x- 开头的头部不会用于评估匹配。将“x-match”设置为“any-with-x”或“all-with-x”也会使用以 x- 开头的头部进行匹配评估。
头部交换机可以被看作是“增强版的直连交换机”。由于它们根据头部值进行路由,它们可以用作直连交换机,其中路由键不必是字符串;例如,它可以是一个整数或哈希(字典)。
队列
AMQP 0-9-1 模型中的 队列 与其他消息和任务队列系统中的队列非常相似:它们存储供应用程序消费的消息。队列与交换机共享一些属性,但也具有一些额外属性:
- 名称
- 持久性(Durable,队列在代理重启后依然存在)
- 排他性(Exclusive,仅由一个连接使用,并在该连接关闭时删除队列)
- 自动删除(Auto-delete,至少有一个消费者的队列,在最后一个消费者取消订阅时被删除)
- 参数(Arguments,可选;用于插件和特定代理功能,如消息 TTL、队列长度限制等)
在使用队列之前必须对其进行声明。如果队列尚不存在,声明操作会创建它。如果队列已存在且属性与声明内容一致,声明操作则无效。如果现有队列属性与声明内容不符,将引发代码为 406 (PRECONDITION_FAILED) 的信道级异常。
队列名称
应用程序可以选择队列名称,或要求代理为其生成名称。队列名称最多可包含 255 字节的 UTF-8 字符。AMQP 0-9-1 代理可以代表应用程序生成唯一的队列名称。要使用此功能,请将空字符串作为队列名称参数传入。生成的名称将在队列声明响应中返回给客户端。
以“amq.”开头的队列名称保留供代理内部使用。尝试声明违反此规则的队列名称将导致代码为 403 (ACCESS_REFUSED) 的信道级异常。
队列持久性
在 AMQP 0-9-1 中,队列可以声明为持久或临时的。持久队列的元数据存储在磁盘上,而临时队列的元数据尽可能存储在内存中。
在 发布消息时,对于消息也有同样的区分。
在持久性非常重要的环境和用例中,应用程序必须使用持久队列,并确保发布者将发布的消息标记为已持久化。
该主题在 队列指南 中有更详细的介绍。
绑定
绑定是交换机用于(除其他事项外)将消息路由到队列的规则。要指示交换机 E 将消息路由到队列 Q,必须将 Q 绑定 到 E。绑定可以具有一个可选的 路由键 属性,供某些交换机类型使用。路由键的目的是从发布到交换机的消息中筛选出特定部分路由到已绑定的队列。换句话说,路由键就像一个过滤器。
打个比方:
- 队列就像你在纽约市的目的地
- 交换机就像肯尼迪机场 (JFK)
- 绑定是通往你目的地的线路。可以有零条或多条路径通往目的地
拥有这一间接层使得实现直接向队列发布消息时无法实现或很难实现的路由场景成为可能,同时也减少了应用开发者需要重复做的工作。
如果消息无法路由到任何队列(例如,因为所发布到的交换机没有绑定),则取决于发布者设置的消息属性,消息要么被 丢弃,要么被退回给发布者。
消费者
除非应用程序能够 消费 消息,否则将消息存储在队列中是没有用的。在 AMQP 0-9-1 模型中,应用程序有两种方式来执行此操作:
- 订阅以获取投递的消息(“推送 API”):这是推荐的选择
- 轮询(“拉取 API”):这种方式 效率极低,在大多数情况下 应避免使用
使用“推送 API”时,应用程序必须表明对消费特定队列中的消息感兴趣。执行此操作时,我们称它们 注册了一个消费者,或者简称为 订阅了队列。可以为一个队列设置多个消费者,或者注册一个 排他性消费者(在消费时排除队列中的所有其他消费者)。
每个消费者(订阅)都有一个名为 消费者标签(consumer tag) 的标识符。它可用于取消消息订阅。消费者标签只是字符串。
消息确认
消费者应用程序(即接收并处理消息的应用程序)偶尔会无法处理个别消息、丢失与服务器的连接或以其他多种方式失败。
网络问题也可能导致问题。这就引出了一个问题:代理应该何时从队列中删除消息?AMQP 0-9-1 规范允许消费者对此进行控制。有两种 确认模式:
- 在代理向应用程序发送消息后(使用
basic.deliver或basic.get-ok方法)。 - 在应用程序返回确认信息后(使用
basic.ack方法)。
前者称为自动确认模式,后者称为显式确认模式。使用显式模式,应用程序可以选择何时发送确认信息。这可以在收到消息后立即执行,或者在处理前将其持久化到数据存储中后执行,或者在完全处理完消息(例如成功获取网页、处理并存储到某个持久化数据存储)后执行。
如果消费者在未发送确认的情况下死亡,代理会将消息重新投递给另一个消费者。如果当时没有其他消费者可用,代理将等待,直到至少有一个消费者注册到同一队列,然后再尝试重新投递。
拒绝消息
当消费者应用程序收到消息时,对该消息的处理可能会成功,也可能会失败。应用程序可以通过拒绝消息来向代理指示处理失败(或暂时无法完成)。拒绝消息时,应用程序可以要求代理丢弃该消息或将其重新入队。当队列中只有一个消费者时,请确保不要通过反复从同一消费者拒绝并重新入队消息,从而创建无限的消息投递循环。
否定确认 (Negative Acknowledgements)
消息通过 basic.reject 方法被拒绝。basic.reject 有一个局限性:无法像确认消息那样同时拒绝多条消息。但是,如果你使用的是 RabbitMQ,则有一个解决方案。RabbitMQ 提供了一种称为 否定确认(nacks) 的 AMQP 0-9-1 扩展。有关更多信息,请参阅 确认 和 basic.nack 扩展 指南。
预取消息
对于多个消费者共享队列的情况,能够指定在发送下一个确认之前可以一次性给每个消费者发送多少条消息是非常有用的。这可以用作简单的负载均衡技术,或者如果消息倾向于成批发布,则可以用来提高吞吐量。例如,如果生产应用程序因为其工作性质而每分钟发送一次消息。
请注意,RabbitMQ 仅支持信道级别的预取计数(prefetch-count),不支持基于连接或大小的预取。
消息属性与载荷
AMQP 0-9-1 模型中的消息具有 属性。某些属性非常通用,以至于 AMQP 0-9-1 规范定义了它们,开发者无需考虑确切的属性名称。示例包括:
- 内容类型
- 内容编码
- 路由键
- 投递模式(持久或非持久)
- 消息优先级
- 消息发布时间戳
- 过期时间
- 发布者应用程序 ID
某些属性由 AMQP 代理使用,但大多数属性由接收它们的应用程序自行解释。某些属性是可选的,称为 头部(headers),类似于 HTTP 中的 X-Headers。消息属性在发布消息时设置。
消息还有一个 载荷(payload,即消息携带的数据),AMQP 代理将其视为不透明的字节数组。代理不会检查或修改载荷。消息可以只包含属性而没有载荷。使用 JSON、Thrift、Protocol Buffers 和 MessagePack 等序列化格式来序列化结构化数据,并将其作为消息载荷进行发布是很常见的。协议对端通常使用“content-type”和“content-encoding”字段来传达这些信息,但这仅是约定俗成。
消息可以作为持久化消息发布,这使得代理将其保存到磁盘。如果服务器重启,系统确保收到的持久化消息不会丢失。仅仅将消息发布到持久化交换机,或者其被路由到的队列是持久化的,并不能使消息本身变成持久化的:这完全取决于消息本身的持久化模式。以持久化方式发布消息会影响性能(就像数据存储一样,持久性是以一定的性能代价为代价的)。
在 发布者指南 中了解更多信息。
AMQP 0-9-1 方法
AMQP 0-9-1 结构化为许多 方法。方法是操作(如 HTTP 方法),与面向对象编程语言中的方法毫无共同之处。AMQP 0-9-1 中的协议方法被分组到 类(classes) 中。类只是 AMQP 0-9-1 方法的逻辑分组。AMQP 0-9-1 参考 包含所有 AMQP 0-9-1 方法(协议操作)的完整详细信息。
让我们看看 exchange 类,这是一组与交换机操作相关的方法。它包括以下操作:
exchange.declareexchange.declare-okexchange.deleteexchange.delete-ok
(注意:RabbitMQ 站点参考还包括本指南中不会讨论的 exchange 类的 RabbitMQ 特定扩展)。
上述操作构成逻辑对:exchange.declare 和 exchange.declare-ok,exchange.delete 和 exchange.delete-ok。这些操作是“请求”(由客户端发送)和“响应”(由代理作为对上述“请求”的回应发送)。
例如,客户端使用 exchange.declare 方法请求代理声明一个新的交换机

如上图所示,exchange.declare 携带多个 参数。它们允许客户端指定交换机名称、类型、持久性标志等。
如果操作成功,代理将以 exchange.declare-ok 方法作为响应

exchange.declare-ok 除了信道编号(信道将在本指南后面描述)外,不携带任何参数。
对于 AMQP 0-9-1 queue 方法类上的另一个方法对,事件序列非常相似:queue.declare 和 queue.declare-ok


并非所有 AMQP 0-9-1 方法都有对应项。有些(basic.publish 是使用最广泛的一个)没有相应的“响应”方法,而另一些(例如 basic.get)有多个可能的“响应”。
连接
AMQP 0-9-1 连接通常是长连接。AMQP 0-9-1 是使用 TCP 进行可靠交付的应用层协议。连接使用身份验证,并可以使用 TLS 进行保护。当应用程序不再需要连接到服务器时,应优雅地关闭其 AMQP 0-9-1 连接,而不是突然关闭底层的 TCP 连接。
信道 (Channels)
一些应用程序需要多个到代理的连接。然而,同时保持大量 TCP 连接处于打开状态是不可取的,因为这样做会消耗系统资源,并使防火墙配置变得更加困难。AMQP 0-9-1 连接通过 信道 进行多路复用,信道可以被认为是“共享单一 TCP 连接的轻量级连接”。
客户端执行的每个协议操作都发生在信道上。特定信道上的通信与另一信道上的通信完全隔离,因此每个协议方法还携带一个信道 ID(也称为信道编号),这是代理和客户端用来判断方法归属于哪个信道的整数。
信道仅存在于连接的上下文中,不能独立存在。当连接关闭时,其上的所有信道也会关闭。
对于使用多个线程/进程进行处理的应用程序,每个线程/进程打开一个新的信道而不共享信道是非常常见的。
虚拟主机 (Virtual Hosts)
为了使单个代理能够托管多个隔离的“环境”(用户组、交换机、队列等),AMQP 0-9-1 包含了 虚拟主机 (vhosts) 的概念。它们类似于许多流行 Web 服务器使用的虚拟主机,并提供了 AMQP 实体驻留的完全隔离的环境。协议客户端在连接协商期间指定它们想要使用的 vhost。
AMQP 是可扩展的
AMQP 0-9-1 有几个扩展点:
- 自定义交换机类型 让开发者能够实现现成交换机类型未覆盖的路由方案,例如基于地理数据的路由。
- 交换机和队列的声明可以包含代理可以使用的附加属性。例如,RabbitMQ 中的 队列级消息 TTL 就是这样实现的。
- 针对协议的特定代理扩展。例如,请参阅 RabbitMQ 实现的扩展。
- 不时会有新的 AMQP 0-9-1 方法类被引入,以应对特定情况,例如 OAuth 2 (JWT) 令牌刷新。
- 代理可以通过 附加插件 进行扩展,例如 RabbitMQ 管理 前端和 HTTP API 就是作为插件实现的。
这些特性使得 AMQP 0-9-1 模型更加灵活,适用于非常广泛的问题领域。
AMQP 0-9-1 客户端生态系统
有针对许多流行编程语言和平台的 许多 AMQP 0-9-1 客户端。其中一些严格遵循 AMQP 术语,仅提供 AMQP 方法的实现。其他一些则具有附加功能、便利方法和抽象。部分客户端是异步(非阻塞)的,部分是同步(阻塞)的,有些同时支持两种模型。部分客户端支持供应商特定扩展(例如 RabbitMQ 特定扩展)。
由于 AMQP 的主要目标之一是互操作性,开发者最好理解协议操作,不要将自己限制在特定客户端库的术语中。这样,与使用不同库的开发者沟通将变得容易得多。