4. RabbitMQ工作模式(二):发布/订阅、路由与主题模式

上一章我们聊了简单队列和工作队列,说白了就是点对点的通信。但实际项目中,很多时候我们需要一条消息被多个消费者同时处理。比如订单创建后,既要发短信通知,又要更新库存,还要记录日志。这时候,发布/订阅模式就派上用场了。

我个人习惯把RabbitMQ的Exchange(交换机)比作一个邮局的分拣中心。生产者把消息扔给交换机,交换机根据规则分发给不同的队列。今天要讲的三种模式,其实就是交换机的三种路由策略。

4.1 发布/订阅模式(Publish/Subscribe)

这个模式的核心是Fanout交换机。它做的事情很简单——把收到的消息广播给所有绑定的队列。就像广播电台,谁在听都能收到。

我在项目中遇到过这样一个场景:用户下单后,需要同时触发邮件通知、短信通知和内部工单系统。如果用点对点的方式,得写三个生产者,太蠢了。用Fanout交换机,一条消息出去,三个队列各取所需。

核心要点:Fanout交换机不关心路由键,它只管广播。

来看代码实现:

import pika
import json

# 生产者:发送订单消息
connection = pika.BlockingConnection(
    pika.ConnectionParameters('localhost'))
channel = connection.channel()

# 声明Fanout交换机
channel.exchange_declare(
    exchange='order_fanout',
    exchange_type='fanout'
)

order = {
    'order_id': '2024001',
    'user_id': 'u1001',
    'amount': 299.00
}

channel.basic_publish(
    exchange='order_fanout',
    routing_key='',  # Fanout模式下routing_key被忽略
    body=json.dumps(order)
)

print(" [x] 订单消息已发送")
connection.close()

消费者这边,每个服务创建自己的临时队列,绑定到同一个交换机:

# 消费者:短信服务
def sms_callback(ch, method, properties, body):
    order = json.loads(body)
    print(f" [短信服务] 发送短信给用户 {order['user_id']}")
    print(f"         订单金额: {order['amount']}")

channel.exchange_declare(
    exchange='order_fanout',
    exchange_type='fanout'
)

# 创建临时队列,断开连接时自动删除
result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue

# 绑定到交换机
channel.queue_bind(
    exchange='order_fanout',
    queue=queue_name
)

channel.basic_consume(
    queue=queue_name,
    on_message_callback=sms_callback,
    auto_ack=True
)

channel.start_consuming()

避坑指南:我曾经犯过一个错误——忘记先声明交换机就直接绑定队列。RabbitMQ会默默创建一个默认的直连交换机,导致消息路由不到预期的队列。记住:先声明交换机,再绑定队列。

4.2 Routing(路由)模式

发布/订阅模式虽然好用,但有个问题——它不管消息内容,一股脑全广播。实际业务中,我们经常需要根据消息的严重程度做分级处理。比如日志系统:error级别的日志要写入数据库并报警,info级别的日志只写入文件。

这时候就需要Direct交换机了。它根据routing_key精确匹配,把消息路由到对应的队列。

你想想看,这就像快递分拣——每个包裹上贴了标签(routing_key),分拣员看到标签就扔到对应的货架(队列)。

日志级别 routing_key 处理方式
ERROR log.error 写入数据库 + 发送报警
WARNING log.warning 写入文件 + 发送通知
INFO log.info 仅写入文件

代码实现如下:

# 生产者:发送不同级别的日志
channel.exchange_declare(
    exchange='logs_direct',
    exchange_type='direct'
)

severities = ['error', 'warning', 'info']
for severity in severities:
    message = f"这是一条{severity}级别的日志"
    channel.basic_publish(
        exchange='logs_direct',
        routing_key=f'log.{severity}',
        body=message
    )
    print(f" [x] 发送 {severity}: {message}")

消费者可以灵活绑定自己关心的路由键:

# 消费者:只处理error和warning
channel.exchange_declare(
    exchange='logs_direct',
    exchange_type='direct'
)

result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue

# 绑定多个routing_key
channel.queue_bind(
    exchange='logs_direct',
    queue=queue_name,
    routing_key='log.error'
)
channel.queue_bind(
    exchange='logs_direct',
    queue=queue_name,
    routing_key='log.warning'
)

print(" [*] 等待日志消息...")

注意:Direct交换机的匹配是精确匹配。routing_key='log.error'和routing_key='log.error.001'是两个完全不同的键,不会匹配到同一个队列。我曾经因为这个踩过坑,调试了半天才发现是键名写错了。

4.3 Topics(主题)模式

Direct模式虽然解决了分级问题,但还不够灵活。比如我想匹配所有以"log."开头的消息,或者匹配"log.error"和"log.warning"但不要"log.info"。这时候就需要Topic交换机了。

Topic模式支持通配符匹配:

  • * 匹配一个单词
  • # 匹配零个或多个单词

举个例子:

  • log.* 匹配 log.error、log.info,但不匹配 log.error.db
  • log.# 匹配 log.error、log.error.db、log.info.user
  • *.error 匹配 log.error、db.error

我个人习惯用Topic模式来处理多维度分类的消息。比如一个电商系统,消息可以按"业务类型.操作.地域"来组织:

# 生产者:发送多维度消息
channel.exchange_declare(
    exchange='topic_orders',
    exchange_type='topic'
)

messages = [
    ('order.created.beijing', '北京用户下单'),
    ('order.paid.shanghai', '上海用户支付成功'),
    ('order.refund.guangzhou', '广州用户申请退款'),
    ('user.registered', '新用户注册')
]

for routing_key, message in messages:
    channel.basic_publish(
        exchange='topic_orders',
        routing_key=routing_key,
        body=message
    )
    print(f" [x] 发送 {routing_key}: {message}")

消费者可以灵活订阅:

# 消费者:订阅所有订单相关消息
channel.queue_bind(
    exchange='topic_orders',
    queue=queue_name,
    routing_key='order.#'
)

# 另一个消费者:只关注北京的订单
channel.queue_bind(
    exchange='topic_orders',
    queue=another_queue,
    routing_key='order.*.beijing'
)

实战建议:Topic模式是三种模式中最灵活的,但也是容易出错的。我建议你在设计routing_key时,用点号分隔的层级结构,每层不超过3个单词。层级太深会让匹配规则变得难以维护。

4.4 三种模式对比

最后做个总结,方便你快速选择:

模式 交换机类型 路由规则 适用场景
发布/订阅 Fanout 广播所有消息 事件通知、日志广播
路由 Direct 精确匹配routing_key 日志分级、命令分发
主题 Topic 通配符匹配 多维度消息路由

嗯,这里要注意一点:不要一上来就用Topic模式。如果业务场景简单,Fanout或Direct就够用了。过度设计反而会增加维护成本。我在早期项目中就吃过这个亏,明明只需要广播,非要用Topic,结果配置了一堆通配符,自己都看晕了。

下一章我们会聊消息确认和持久化,这是保证消息不丢的关键。到时候见。