RabbitMQ 5大核心模式详解(二):发布订阅 & 路由模式,精准控制消息流向

2025博客之星年度评选已开启 10w+人浏览 1.4k人参与

在RabbitMQ的核心通信模式中,简单模式、工作队列模式更适用于“一对一”或“一对多竞争”的基础场景,而当业务需要实现“一条消息多消费者共享”或“按条件筛选消息”时,发布订阅模式(Publish/Subscribe)与路由模式(Routing)就成为了关键技术支撑。本文将深入剖析这两种模式的设计思想、实现细节及适用场景,带你掌握RabbitMQ精准控制消息流向的核心能力。

一、前置认知:为什么需要这两种模式?

在实际业务中,我们常会遇到这样的需求:

  • 电商订单创建后,既要触发库存扣减,又要发送短信通知、生成物流单——此时需要“一条订单消息被多个消费者同时处理”;

  • 日志系统中,需要将“ERROR级别日志”存入数据库,“INFO级别日志”仅输出到控制台——此时需要“按消息内容筛选,精准分发到对应消费者”。

简单模式和工作队列模式无法满足上述需求:前者仅支持单消费者,后者多个消费者会竞争同一消息(一条消息仅被一个消费者处理)。而发布订阅模式和路由模式通过对“交换机(Exchange)”的灵活使用,完美解决了“消息广播”与“条件路由”的问题。

这里必须先明确一个核心概念:交换机是RabbitMQ消息分发的核心枢纽。生产者不再将消息直接发送到队列,而是发送到交换机,由交换机根据预设规则(绑定键、路由键)将消息路由到对应的队列中,消费者再从队列中获取消息。这两种模式的核心差异,本质上是交换机类型及路由规则的差异。

二、发布订阅模式:消息广播,多消费者共享

2.1 模式核心:扇形交换机(Fanout Exchange)

发布订阅模式的核心是“扇形交换机”,也称为“广播交换机”。其路由规则极为简单:忽略路由键(Routing Key),将生产者发送的消息复制到所有与该交换机绑定的队列中。只要队列与扇形交换机建立了绑定关系,就一定能接收到交换机转发的消息,实现“一条消息,多队列共享”。

该模式的架构如下:

  1. 生产者创建扇形交换机,并将消息发送到该交换机;

  2. 多个队列与该扇形交换机绑定(绑定键可忽略,通常设为空字符串);

  3. 交换机将消息广播到所有绑定的队列;

  4. 每个队列对应的消费者,从队列中获取消息并处理。

2.2 代码实现(基于Java + Spring AMQP)

我们以“订单创建后多模块联动”为例,实现发布订阅模式:

步骤1:配置交换机与队列

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class PubSubConfig {
    // 1. 定义扇形交换机
    @Bean
    public FanoutExchange orderFanoutExchange() {
        // 参数:交换机名称、是否持久化、是否自动删除
        return new FanoutExchange("order.fanout.exchange", true, false);
    }

    // 2. 定义3个队列:库存队列、短信队列、物流队列
    @Bean
    public Queue inventoryQueue() {
        return new Queue("order.inventory.queue", true);
    }

    @Bean
    public Queue smsQueue() {
        return new Queue("order.sms.queue", true);
    }

    @Bean
    public Queue logisticsQueue() {
        return new Queue("order.logistics.queue", true);
    }

    // 3. 将队列与扇形交换机绑定
    @Bean
    public Binding bindInventoryQueue(FanoutExchange exchange, Queue inventoryQueue) {
        return BindingBuilder.bind(inventoryQueue).to(exchange);
    }

    @Bean
    public Binding bindSmsQueue(FanoutExchange exchange, Queue smsQueue) {
        return BindingBuilder.bind(smsQueue).to(exchange);
    }

    @Bean
    public Binding bindLogisticsQueue(FanoutExchange exchange, Queue logisticsQueue) {
        return BindingBuilder.bind(logisticsQueue).to(exchange);
    }
}
步骤2:实现生产者(发送订单消息)

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderPublisher {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 发送订单创建消息
    public void sendOrderCreatedMsg(String orderId) {
        String msg = "订单创建成功,订单ID:" + orderId;
        // 参数:交换机名称、路由键(扇形交换机可忽略,设为空)、消息内容
        rabbitTemplate.convertAndSend("order.fanout.exchange", "", msg);
        System.out.println("生产者发送消息:" + msg);
    }
}
步骤3:实现3个消费者(处理不同业务)

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

// 库存消费者
@Component
public class InventoryConsumer {
    @RabbitListener(queues = "order.inventory.queue")
    public void handleInventory(String msg) {
        System.out.println("库存模块接收消息:" + msg + ",执行库存扣减逻辑");
    }
}

// 短信消费者
@Component
public class SmsConsumer {
    @RabbitListener(queues = "order.sms.queue")
    public void handleSms(String msg) {
        System.out.println("短信模块接收消息:" + msg + ",执行短信发送逻辑");
    }
}

// 物流消费者
@Component
public class LogisticsConsumer {
    @RabbitListener(queues = "order.logistics.queue")
    public void handleLogistics(String msg) {
        System.out.println("物流模块接收消息:" + msg + ",执行物流单生成逻辑");
    }
}
步骤4:测试效果

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest
@RunWith(SpringRunner.class)
public class PubSubTest {
    @Autowired
    private OrderPublisher orderPublisher;

    @Test
    public void testSendOrderMsg() {
        orderPublisher.sendOrderCreatedMsg("ORDER_20251218001");
    }
}
输出结果

生产者发送消息:订单创建成功,订单ID:ORDER_20251218001
库存模块接收消息:订单创建成功,订单ID:ORDER_20251218001,执行库存扣减逻辑
短信模块接收消息:订单创建成功,订单ID:ORDER_20251218001,执行短信发送逻辑
物流模块接收消息:订单创建成功,订单ID:ORDER_20251218001,执行物流单生成逻辑

可见,一条消息被三个消费者同时接收并处理,完美实现了“发布订阅”的核心需求。

2.3 适用场景

  • 消息需要被多个独立模块共享的场景,如订单联动、支付结果通知;

  • 日志收集的初步分发(如将所有日志广播到不同处理队列,再做后续筛选);

  • 分布式系统中的“事件通知”(如服务启动成功后,通知其他依赖服务)。

三、路由模式:精准筛选,按规则分发消息

3.1 模式核心:直连交换机(Direct Exchange)

发布订阅模式的“广播”特性虽然灵活,但无法实现“消息筛选”——所有绑定的队列都会收到消息。而路由模式通过“直连交换机”解决了这一问题,其核心规则是:消息的路由键(Routing Key)与队列和交换机的绑定键(Binding Key)完全匹配时,消息才会被路由到该队列

该模式的核心逻辑:

  1. 生产者发送消息时,必须指定一个明确的路由键(如“log.error”“log.info”);

  2. 队列与直连交换机绑定时,需设置一个绑定键(如“log.error”);

  3. 直连交换机接收消息后,对比消息的路由键与所有绑定的绑定键:仅当两者完全一致时,才将消息转发到对应队列;

  4. 消费者从绑定了目标绑定键的队列中获取消息。

此外,路由模式支持“一个绑定键对应多个队列”——如果多个队列都绑定了“log.error”的绑定键,那么路由键为“log.error”的消息会被转发到所有这些队列,实现“按规则广播”。

3.2 代码实现(基于Java + Spring AMQP)

我们以“日志分级处理”为例,实现路由模式:ERROR日志存入数据库,INFO日志输出到控制台,WARN日志同时输出到控制台和文件。

步骤1:配置交换机与队列

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RoutingConfig {
    // 1. 定义直连交换机
    @Bean
    public DirectExchange logDirectExchange() {
        return new DirectExchange("log.direct.exchange", true, false);
    }

    // 2. 定义3个队列:ERROR日志队列、INFO日志队列、WARN日志队列
    @Bean
    public Queue errorLogQueue() {
        return new Queue("log.error.queue", true);
    }

    @Bean
    public Queue infoLogQueue() {
        return new Queue("log.info.queue", true);
    }

    @Bean
    public Queue warnLogQueue() {
        return new Queue("log.warn.queue", true);
    }

    // 3. 绑定队列与交换机(指定绑定键)
    // ERROR队列绑定键:log.error
    @Bean
    public Binding bindErrorQueue(DirectExchange exchange, Queue errorLogQueue) {
        return BindingBuilder.bind(errorLogQueue).to(exchange).with("log.error");
    }

    // INFO队列绑定键:log.info
    @Bean
    public Binding bindInfoQueue(DirectExchange exchange, Queue infoLogQueue) {
        return BindingBuilder.bind(infoLogQueue).to(exchange).with("log.info");
    }

    // WARN队列绑定两个键:log.warn(自身)、log.warn.file(模拟文件输出)
    @Bean
    public Binding bindWarnQueue1(DirectExchange exchange, Queue warnLogQueue) {
        return BindingBuilder.bind(warnLogQueue).to(exchange).with("log.warn");
    }

    @Bean
    public Binding bindWarnQueue2(DirectExchange exchange, Queue warnLogQueue) {
        return BindingBuilder.bind(warnLogQueue).to(exchange).with("log.warn.file");
    }
}
步骤2:实现生产者(发送不同级别日志)

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class LogPublisher {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 发送ERROR日志
    public void sendErrorLog(String content) {
        String msg = "ERROR: " + content;
        // 路由键:log.error
        rabbitTemplate.convertAndSend("log.direct.exchange", "log.error", msg);
        System.out.println("生产者发送ERROR日志:" + msg);
    }

    // 发送INFO日志
    public void sendInfoLog(String content) {
        String msg = "INFO: " + content;
        // 路由键:log.info
        rabbitTemplate.convertAndSend("log.direct.exchange", "log.info", msg);
        System.out.println("生产者发送INFO日志:" + msg);
    }

    // 发送WARN日志(同时触发两个绑定键)
    public void sendWarnLog(String content) {
        String msg = "WARN: " + content;
        // 路由键1:log.warn
        rabbitTemplate.convertAndSend("log.direct.exchange", "log.warn", msg);
        // 路由键2:log.warn.file
        rabbitTemplate.convertAndSend("log.direct.exchange", "log.warn.file", msg);
        System.out.println("生产者发送WARN日志:" + msg);
    }
}
步骤3:实现消费者(处理不同级别日志)

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

// ERROR日志消费者(存入数据库)
@Component
public class ErrorLogConsumer {
    @RabbitListener(queues = "log.error.queue")
    public void handleErrorLog(String msg) {
        System.out.println("ERROR日志处理:" + msg + ",执行数据库存储逻辑");
    }
}

// INFO日志消费者(控制台输出)
@Component
public class InfoLogConsumer {
    @RabbitListener(queues = "log.info.queue")
    public void handleInfoLog(String msg) {
        System.out.println("INFO日志处理:" + msg + ",执行控制台输出逻辑");
    }
}

// WARN日志消费者(控制台+文件输出)
@Component
public class WarnLogConsumer {
    @RabbitListener(queues = "log.warn.queue")
    public void handleWarnLog(String msg) {
        System.out.println("WARN日志处理:" + msg + ",执行控制台输出+文件写入逻辑");
    }
}
步骤4:测试效果

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest
@RunWith(SpringRunner.class)
public class RoutingTest {
    @Autowired
    private LogPublisher logPublisher;

    @Test
    public void testSendLogMsg() {
        logPublisher.sendErrorLog("数据库连接失败");
        logPublisher.sendInfoLog("用户登录成功,用户名:admin");
        logPublisher.sendWarnLog("内存使用率超过80%");
    }
}
输出结果

生产者发送ERROR日志:ERROR: 数据库连接失败
ERROR日志处理:ERROR: 数据库连接失败,执行数据库存储逻辑
生产者发送INFO日志:INFO: 用户登录成功,用户名:admin
INFO日志处理:INFO: 用户登录成功,用户名:admin,执行控制台输出逻辑
生产者发送WARN日志:WARN: 内存使用率超过80%
WARN日志处理:WARN: 内存使用率超过80%,执行控制台输出+文件写入逻辑
WARN日志处理:WARN: 内存使用率超过80%,执行控制台输出+文件写入逻辑

可见,ERROR日志仅被ERROR消费者处理,INFO日志仅被INFO消费者处理,而WARN日志因匹配两个绑定键,被WARN消费者处理了两次,实现了“按规则精准路由”的需求。

3.3 适用场景

  • 需要按消息属性进行筛选的场景,如日志分级、订单状态流转(待支付、已支付、已取消);

  • 特定业务模块仅需处理指定类型消息的场景,如财务模块仅处理“支付成功”的消息;

  • 需要“精准广播”的场景(一个路由键对应多个队列)。

四、核心差异:发布订阅 vs 路由模式

为了更清晰地掌握两种模式的适用边界,我们从核心维度进行对比:

对比维度发布订阅模式路由模式
核心组件扇形交换机(Fanout)直连交换机(Direct)
路由依据忽略路由键,仅依赖队列与交换机的绑定关系路由键与绑定键完全匹配
消息流向广播到所有绑定的队列仅流向绑定键与路由键匹配的队列
灵活性低(无法筛选,全量分发)中(支持精准匹配,可实现按规则分发)
典型场景多模块共享同一消息(如订单联动)按消息类型筛选处理(如日志分级)

五、实践技巧与注意事项

  1. 交换机与队列的持久化:生产环境中必须将交换机和队列设置为“持久化”(durable=true),避免RabbitMQ重启后组件丢失,导致消息无法路由。

  2. 路由键的命名规范:建议采用“业务.类型.操作”的格式(如“order.pay.success”“log.error.db”),提高可读性和可维护性。

  3. 消费者的幂等性处理:两种模式下,消费者都可能因网络波动等问题重复接收消息,需通过“订单ID去重”“消息ID校验”等方式实现幂等性。

  4. 交换机的类型选择:若需要“模糊匹配”(如“log.*”匹配所有日志类型),路由模式的直连交换机无法满足,需后续介绍的“主题模式(Topic)”,这也是路由模式的延伸。

六、总结

发布订阅模式和路由模式是RabbitMQ实现“消息分发灵活性”的核心基础:前者通过扇形交换机实现“广播共享”,解决多模块联动问题;后者通过直连交换机实现“精准匹配”,解决消息筛选问题。两者的本质都是通过交换机的路由规则控制消息流向,而交换机的类型选择则决定了路由的灵活性。

下一篇文章,我们将继续探讨RabbitMQ更灵活的两种模式——主题模式(Topic)与 Headers模式,带你掌握“模糊匹配”和“多条件匹配”的高级技巧,敬请期待!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

canjun_wen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值