RabbitMQ 工作模式与 Java 原生客户端案例
在使用 RabbitMQ 做消息通信时,最重要的不是一上来就背 API,而是先理解消息从生产者到消费者之间到底经历了什么。RabbitMQ 的核心作用可以理解为:生产者把消息交给 RabbitMQ,RabbitMQ 根据队列、交换机和路由规则,把消息交给合适的消费者。
一、RabbitMQ 的 7 种工作模式
1. Simple 简单模式
简单模式是 RabbitMQ 最基础的工作模式,结构也最直观:一个生产者、一个队列、一个消费者。
生产者把消息发送到队列中,消费者从队列中取出消息进行处理。队列在这里起到缓存消息的作用,类似一个中转邮箱。简单模式的特点是消息只会被一个消费者消费一次,因此也可以理解为点对点模式。
这种模式适合消息只需要由单个消费者处理的场景,例如某个服务只负责处理一种固定任务。
2. Work Queue 工作队列模式
工作队列模式可以看作简单模式的增强版。它仍然是一个生产者向一个队列发送消息,但消费者不再只有一个,而是有多个消费者同时监听同一个队列。
当队列中有多条消息时,RabbitMQ 会把消息分发给不同的消费者。每条消息只会被其中一个消费者拿到,不会被多个消费者重复消费。
这种模式适合在集群环境中做异步任务处理。比如用户订票成功后,订单系统把通知任务发送到 RabbitMQ,多个短信服务实例共同消费这些任务,从而提升整体处理能力。
3. Publish/Subscribe 发布订阅模式
从发布订阅模式开始,RabbitMQ 中会引入一个非常重要的角色:Exchange,也就是交换机。
生产者不再直接把消息发送给队列,而是把消息发送给交换机。交换机根据自身类型和绑定规则,把消息转发到一个或多个队列中。消费者仍然是从队列里消费消息。
在发布订阅模式中,常用的交换机类型是 fanout。它的特点是广播:只要队列绑定到了这个交换机,就能收到同一份消息。
这类模式适合一条消息需要被多个系统同时接收的场景。比如气象局发布天气数据,多个门户网站都订阅同一个交换机,每个平台都可以收到同一份天气消息。
4. Exchange、RoutingKey 与 BindingKey
理解后续模式之前,需要先分清几个概念。
Exchange 是交换机,负责接收生产者发送的消息,并把消息按规则路由到队列。交换机本身不存储消息。如果没有队列绑定到交换机,或者没有任何队列符合路由规则,消息就可能丢失。
RabbitMQ 常见交换机类型有四种:
fanout:广播,把消息转发给所有绑定的队列。direct:定向,根据完全匹配的 routing key 转发消息。topic:通配符,根据 routing pattern 转发消息。headers:根据消息头匹配,实际开发中使用较少。
RoutingKey 是生产者发送消息时指定的路由键,用来告诉交换机这条消息应该按什么规则转发。
BindingKey 是队列绑定交换机时指定的路由键,用来声明这个队列关心什么类型的消息。
可以简单记忆:发送消息时用的是 RoutingKey,绑定队列时用的是 BindingKey。很多资料和 API 中会把它们都称为 routing key,需要结合使用场景区分。
5. Routing 路由模式
路由模式是发布订阅模式的变种。它不再无条件把消息广播给所有队列,而是根据 RoutingKey 做精确匹配。
在路由模式中,交换机类型一般使用 direct。队列绑定交换机时会指定一个或多个 BindingKey,生产者发送消息时也会指定 RoutingKey。只有二者完全一致时,消息才会进入对应队列。
这个模式适合根据规则分发消息。比如系统日志按照 error、warning、info、debug 分类,不同级别的日志可以进入不同队列,再由不同服务写入不同文件或存储系统。
6. Topics 通配符模式
Topics 模式可以看作 Routing 模式的升级版。它使用 topic 类型交换机,仍然根据路由键转发消息,但匹配规则更灵活。
Topic 模式中的 routing key 通常由多个单词组成,并用点号分隔,例如:
text
order.error
order.pay.info
pay.error
BindingKey 支持两个通配符:
*表示匹配一个单词。#表示匹配零个或多个单词。
例如 *.error 可以匹配 order.error 和 pay.error,但不能匹配 order.pay.error;#.info 可以匹配 order.info,也可以匹配 order.pay.info。
Topics 模式适合需要灵活匹配和过滤消息的场景,比如根据业务模块、操作类型、日志级别组合出不同的消息路由规则。
7. RPC 通信模式
RPC 是 Remote Procedure Call,也就是远程过程调用。RabbitMQ 也可以用两个队列实现类似"请求-响应"的通信过程。
大致流程如下:
- 客户端发送请求消息到请求队列。
- 客户端在消息属性中设置
replyTo,指定一个回调队列。 - 客户端设置
correlationId,用于标识本次请求。 - 服务端消费请求队列中的消息,处理完成后把响应结果发送到
replyTo指定的队列。 - 客户端从回调队列中收到响应,再根据
correlationId确认这是不是自己等待的那次响应。
这种模式适合确实需要请求结果的场景,但使用时要注意它会让异步消息通信变得接近同步调用,不能滥用。
8. Publisher Confirms 发布确认模式
消息中间件经常需要面对消息丢失问题。消息可能丢在三个阶段:生产者发送失败,Broker 没有保存好,消费者处理失败。
Publisher Confirms 主要解决生产者到 RabbitMQ 之间的可靠发送问题。
生产者可以通过 channel.confirmSelect() 把信道设置为 confirm 模式。之后这个信道上发布的每条消息都会获得一个递增序号。当消息成功到达 RabbitMQ,并被正确处理后,RabbitMQ 会向生产者返回 ack。如果 RabbitMQ 因内部问题无法处理消息,则可能返回 nack。
发布确认的好处是生产者能知道消息是否真正送达 RabbitMQ,从而在失败时做补偿、重试或记录异常。
二、工作模式的 Java 原生客户端案例
课件第二章主要使用 RabbitMQ Java 原生客户端演示几种工作模式。共同的基础依赖是 amqp-client,核心操作通常包括创建连接、创建信道、声明队列或交换机、绑定关系、发送消息、消费消息。
1. 简单模式
简单模式已经在快速入门程序中演示过,所以第二章没有重复展开。它的核心流程是:生产者向队列发送消息,消费者监听同一个队列并消费消息。
2. Work Queues 工作队列案例
工作队列模式和简单模式的代码差别不大,关键区别是消费者数量变多了。
生产者仍然声明一个队列,然后向这个队列发送多条消息。为了观察多个消费者之间的竞争关系,可以一次发送 10 条消息:
java
for (int i = 0; i < 10; i++) {
String msg = "Hello World" + i;
channel.basicPublish("", WORK_QUEUE_NAME, null, msg.getBytes());
}
消费者代码可以复制两份,两个消费者都监听同一个队列。运行时建议先启动两个消费者,再启动生产者。这样可以清楚看到消息被两个消费者分摊处理。
典型结果是:消费者 1 收到一部分消息,消费者 2 收到另一部分消息。每条消息只会被其中一个消费者消费,这就是工作队列模式的竞争消费特点。
3. Publish/Subscribe 发布订阅案例
发布订阅模式需要先声明 fanout 类型交换机,再声明多个队列,并把这些队列绑定到同一个交换机。
核心流程是:
- 声明 fanout 交换机。
- 声明两个队列。
- 把两个队列都绑定到该交换机。
- 生产者向交换机发送消息。
- 两个消费者分别监听两个队列。
因为 fanout 交换机会广播消息,所以一条消息会被复制到所有绑定队列中。运行结果是两个消费者都能收到同一条消息。
这个案例强调了一个变化:生产者发送消息时不再直接面向队列,而是面向交换机。
4. Routing 路由案例
Routing 模式使用 direct 类型交换机。它和发布订阅模式的主要区别是:绑定队列时需要指定 routing key,发送消息时也需要指定 routing key。
例如:
- 队列 1 绑定
orange。 - 队列 2 绑定
black和green。 - 生产者分别发送 routing key 为
orange、black、green的消息。
最终结果是:
orange消息进入队列 1。black和green消息进入队列 2。
这里要特别注意:Direct 路由模式创建交换机时应使用 BuiltinExchangeType.DIRECT。只有交换机类型正确,RoutingKey 和 BindingKey 的精确匹配规则才会生效。
5. Topics 通配符案例
Topics 模式使用 topic 类型交换机。它和 Routing 模式的写法相似,区别在于队列绑定时可以使用通配符。
课件中的示例规则可以理解为:
- 队列 1 绑定
*.error,只接收某一类 error 消息。 - 队列 2 绑定
#.info和*.error,既接收 info 类型消息,也接收部分 error 类型消息。
生产者发送三类消息:
text
order.error
order.pay.info
pay.error
根据规则,order.error 和 pay.error 能匹配 *.error,所以队列 1 可以收到;order.pay.info 能匹配 #.info,所以队列 2 可以收到。由于队列 2 也绑定了 *.error,它还能收到对应的 error 消息。
Topics 模式的优势是灵活。它很适合用在复杂业务事件中,比如按"业务模块.操作类型.状态"来组织 routing key。
6. RPC 通信案例
RPC 案例分为客户端和服务端。
客户端主要做四件事:
- 声明请求队列。
- 创建临时回调队列。
- 生成唯一的
correlationId,并把replyTo设置为回调队列。 - 发送请求后阻塞等待回调队列中的响应。
服务端主要做三件事:
- 监听请求队列。
- 处理请求消息。
- 把处理结果发送到请求消息指定的
replyTo队列,并携带相同的correlationId。
在服务端消费请求消息时,课件还引出了消息确认机制。autoAck=true 表示自动确认,消息一旦投递给消费者就会被认为消费成功;autoAck=false 表示手动确认,消费者处理完成后需要调用 basicAck。
对于 RPC 这类一问一答的场景,通常会配合 basicQos(1) 控制服务端一次只处理一条消息,再手动 ack,避免请求处理混乱。
7. Publisher Confirms 发布确认案例
发布确认案例围绕三种策略展开。
第一种是单独确认。每发送一条消息,就调用 waitForConfirmsOrDie 等待 RabbitMQ 确认。这种方式最简单,也最安全直观,但性能最差,因为它本质上是串行等待。
第二种是批量确认。生产者先连续发送一批消息,比如 100 条,再统一等待确认。它比单独确认快很多,但缺点是如果这一批中有消息失败,生产者不容易判断具体是哪一条出了问题,往往需要整批重发。
第三种是异步确认。生产者通过 addConfirmListener 注册 ack 和 nack 回调,一边发送消息,一边异步接收 RabbitMQ 的确认结果。通常会维护一个有序集合保存尚未确认的消息序号,收到 ack 后移除对应序号,收到 nack 后根据业务需要重发或记录失败。
三种方式的特点可以总结为:
| 策略 | 优点 | 缺点 |
|---|---|---|
| 单独确认 | 实现简单,定位明确 | 性能差 |
| 批量确认 | 性能明显提升 | 失败时难定位具体消息 |
| 异步确认 | 性能最好,更适合大量消息 | 实现复杂,需要维护未确认消息 |
从课件运行结果也能看出,消息数量越多,异步确认的优势越明显。
三、总结
RabbitMQ 的几种工作模式其实是围绕三个问题展开的:
- 消息要不要被多个消费者同时收到?
- 消息要不要按照规则分发到不同队列?
- 消息发送和消费过程要不要保证可靠性?
简单模式解决最基础的点对点通信;工作队列模式解决多个消费者分摊任务;发布订阅模式解决广播;Routing 和 Topics 解决按规则分发;RPC 解决请求响应式通信;Publisher Confirms 解决生产者发送可靠性。
实际开发中,最常用的是 Work Queue、Fanout、Direct、Topic 和发布确认。掌握它们之后,再结合 Spring Boot 的封装,就能比较自然地完成系统间异步通信、事件通知、日志分发和订单消息处理等业务场景。