【RabbitMQ #5】 | 三大交换机 Fanout ,Direct ,Topic

前言:生产环境消息一般都经过 Exchange(交换机)转发,不会直接发送到队列。交换机主要分为 3 种:Fanout(广播)、Direct(定向)、Topic(话题)。

一、FanoutExchange 广播交换机

原理

publisher ---> exchange ---> queue1 ---> consumer1           

---> queue2 ---> consumer2

Fanout 会把消息广播转发给所有绑定到此交换机的队列,RoutingKey 无效。

场景:多个实例绑定同一个队列可以提升消费速度;消息需要推送给多个微服务。

案例:SpringAMQP 演示 FanoutExchange(Go 实现)

需求:

  1. RabbitMQ 控制台创建队列 fanout.queue1、fanout.queue2
  2. 创建交换机 hmall.fanout,将两个队列绑定交换机
  3. 消费者监听两个队列
  4. 生产者向 hmall.fanout 发送消息
消费者代码
Go 复制代码
package main

import (
	"fmt"
	"log"
	"github.com/streadway/amqp"
)

func failOnError(err error, msg string) {
	if err != nil {
		log.Fatalf("%s: %s", msg, err)
	}
}

// listenFanoutQueue1,监听 fanout.queue1
func listenFanoutQueue1(ch *amqp.Channel) {
	qName := "fanout.queue1"
	msgs, err := ch.Consume(
		qName,
		"",
		true,  //自动ack,和spring默认行为一致
		false,
		false,
		false,
		nil,
	)
	failOnError(err, "监听fanout.queue1失败")
	go func() {
		for d := range msgs {
			fmt.Printf("消费者1 收到了 fanout.queue的消息:【%s】\n", d.Body)
		}
	}()
	fmt.Println("消费者1 已启动,监听 fanout.queue1")
}

// listenFanoutQueue2,监听 fanout.queue2
func listenFanoutQueue2(ch *amqp.Channel) {
	qName := "fanout.queue2"
	msgs, err := ch.Consume(
		qName,
		"",
		true,
		false,
		false,
		false,
		nil,
	)
	failOnError(err, "监听fanout.queue2失败")
	go func() {
		for d := range msgs {
			log.Printf("消费者2 收到了 fanout.queue的消息......:【%s】\n", d.Body)
		}
	}()
	fmt.Println("消费者2 已启动,监听 fanout.queue2")
}

func main() {
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "连接MQ失败")
	defer conn.Close()
	ch, err := conn.Channel()
	failOnError(err, "打开通道失败")
	defer ch.Close()

	listenFanoutQueue1(ch)
	listenFanoutQueue2(ch)

	// 阻塞,持续监听
	select {}
}
生产者代码
Go 复制代码
package main

import (
	"fmt"
	"log"
	"github.com/streadway/amqp"
)

func failOnError(err error, msg string) {
	if err != nil {
		log.Fatalf("%s: %s", msg, err)
	}
}

func main() {
	// 连接RabbitMQ
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "连接MQ失败")
	defer conn.Close()
	ch, err := conn.Channel()
	failOnError(err, "打开通道失败")
	defer ch.Close()

	exchangeName := "hmall.fanout"
	msg := "hello, everyone!"

	// 声明fanout交换机
	err = ch.ExchangeDeclare(
		exchangeName,
		amqp.ExchangeFanout,
		false,
		false,
		false,
		false,
		nil,
	)
	failOnError(err, "声明交换机失败")

	// 发送消息,fanout routingKey填空字符串
	err = ch.Publish(
		exchangeName,
		"", // fanout模式 routingKey无效
		false,
		false,
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(msg),
		},
	)
	failOnError(err, "发送消息失败")
	fmt.Printf("发送成功:%s\n", msg)
}

运行效果:两个消费者都收到消息,实现广播。

二、DirectExchange 定向交换机

原理

Direct 交换机:根据 RoutingKey 定向路由消息到匹配的队列。 规则:

  1. 每个 Queue 与 Exchange 绑定,指定BindingKey(暗号)
  2. 生产者发消息时指定消息的RoutingKey
  3. 交换机把消息路由到 BindingKey == RoutingKey 的队列

例子:

  • queue1 绑定 key:blue、red
  • queue2 绑定 key:yellow、red
  • 发送 routingKey=blue → 只有 queue1 接收
  • 发送 routingKey=yellow → 只有 queue2 接收
  • 发送 routingKey=red → 两个队列都收到(可模拟广播效果)

案例

需求:

  1. RabbitMQ 控制台创建 direct.queue1、direct.queue2
  2. 创建 hmall.direct 交换机,绑定 key:red/yellow/blue
  3. 消费者监听两个队列
  4. 生产者发送不同 RoutingKey 测试
消费者代码
Go 复制代码
package main

import (
	"fmt"
	"log"
	"github.com/streadway/amqp"
)

func failOnError(err error, msg string) {
	if err != nil {
		log.Fatalf("%s: %s", msg, err)
	}
}

// listenDirectQueue1,监听 direct.queue1
func listenDirectQueue1(ch *amqp.Channel) {
	qName := "direct.queue1"
	msgs, err := ch.Consume(
		qName,
		"",
		true, //自动ACK,和Spring默认行为一致
		false,
		false,
		false,
		nil,
	)
	failOnError(err, "监听direct.queue1失败")
	go func() {
		for d := range msgs {
			fmt.Printf("消费者1 收到了 direct.queue1的消息:【%s】\n", d.Body)
		}
	}()
	fmt.Println("消费者1 已启动,监听 direct.queue1")
}

// listenDirectQueue2,监听 direct.queue2
func listenDirectQueue2(ch *amqp.Channel) {
	qName := "direct.queue2"
	msgs, err := ch.Consume(
		qName,
		"",
		true,
		false,
		false,
		false,
		nil,
	)
	failOnError(err, "监听direct.queue2失败")
	go func() {
		for d := range msgs {
			fmt.Printf("消费者2 收到了 direct.queue2的消息:【%s】\n", d.Body)
		}
	}()
	fmt.Println("消费者2 已启动,监听 direct.queue2")
}

func main() {
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "连接MQ失败")
	defer conn.Close()
	ch, err := conn.Channel()
	failOnError(err, "打开通道失败")
	defer ch.Close()

	listenDirectQueue1(ch)
	listenDirectQueue2(ch)
	select {}
}
生产者代码
Go 复制代码
package main

import (
	"fmt"
	"log"
	"github.com/streadway/amqp"
)

func failOnError(err error, msg string) {
	if err != nil {
		log.Fatalf("%s: %s", msg, err)
	}
}

func main() {
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "连接MQ失败")
	defer conn.Close()
	ch, err := conn.Channel()
	failOnError(err, "打开通道失败")
	defer ch.Close()

	exchangeName := "hmall.direct"
	msg := "hello direct"
	routingKey := "blue" // 修改为 red / yellow 测试不同效果

	err = ch.ExchangeDeclare(exchangeName, amqp.ExchangeDirect, false, false, false, false, nil)
	failOnError(err, "声明交换机")

	err = ch.Publish(
		exchangeName,
		routingKey,
		false,
		false,
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(msg),
		},
	)
	failOnError(err, "发送消息失败")
	fmt.Printf("消息发送成功:%s\n", msg)
}

三、TopicExchange 话题交换机

原理

Topic 和 Direct 很像,区别在于支持通配符。

  • RoutingKey 由多个单词组成,单词之间用 . 分割
  • BindingKey 支持通配符:
    • #:匹配0 个或多个单词
    • *:匹配恰好 1 个单词

示例绑定规则:

  • queue1 绑定 china.#:匹配所有以 china 开头的 routingKey(china.news、china.weather、china.news.beijing)
  • queue2 绑定 #.news:匹配所有以.news 结尾的 routingKey(japan.news、china.news)

测试场景:

  1. routingKey = japan.news → 只有 queue2 收到
  2. routingKey = china.news → queue1、queue2 都收到
  3. routingKey = china.weather → 只有 queue1 收到

Topic 是三种交换机里最灵活的。

案例

需求:

  1. RabbitMQ 控制台创建队列 topic.queue1、topic.queue2
  2. 创建交换机 hmall.topic,绑定队列
  3. 消费者监听两个队列
  4. 生产者使用不同 RoutingKey 发送消息
生产者代码
Go 复制代码
package main

import (
	"fmt"
	"log"
	"github.com/streadway/amqp"
)

func failOnError(err error, msg string) {
	if err != nil {
		log.Fatalf("%s: %s", msg, err)
	}
}

func main() {
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "连接MQ失败")
	defer conn.Close()
	ch, err := conn.Channel()
	failOnError(err, "打开通道失败")
	defer ch.Close()

	exchangeName := "hmall.topic"
	msg := "蓝色通知,警报解除,哥斯拉是放的气球"
	routingKey := "japan.news" // 修改为 china.news / china.weather 测试

	err = ch.ExchangeDeclare(
		exchangeName,
		amqp.ExchangeTopic,
		false,
		false,
		false,
		false,
		nil,
	)
	failOnError(err, "声明topic交换机失败")

	err = ch.Publish(
		exchangeName,
		routingKey,
		false,
		false,
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(msg),
		},
	)
	failOnError(err, "发送消息失败")
	fmt.Printf("消息发送成功:%s\n", msg)
}
消费者代码
Go 复制代码
package main

import (
	"fmt"
	"log"
	"github.com/streadway/amqp"
)

func failOnError(err error, msg string) {
	if err != nil {
		log.Fatalf("%s: %s", msg, err)
	}
}

func listenTopicQueue1(ch *amqp.Channel) {
	qName := "topic.queue1"
	msgs, err := ch.Consume(qName, "", true, false, false, false, nil)
	failOnError(err, "监听topic.queue1失败")
	go func() {
		for d := range msgs {
			fmt.Printf("topic消费者1 收到消息:【%s】\n", d.Body)
		}
	}()
	fmt.Println("topic消费者1 已启动,监听 topic.queue1")
}

func listenTopicQueue2(ch *amqp.Channel) {
	qName := "topic.queue2"
	msgs, err := ch.Consume(qName, "", true, false, false, false, nil)
	failOnError(err, "监听topic.queue2失败")
	go func() {
		for d := range msgs {
			fmt.Printf("topic消费者2 收到消息:【%s】\n", d.Body)
		}
	}()
	fmt.Println("topic消费者2 已启动,监听 topic.queue2")
}

func main() {
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "连接MQ失败")
	defer conn.Close()
	ch, err := conn.Channel()
	failOnError(err, "打开通道失败")
	defer ch.Close()

	listenTopicQueue1(ch)
	listenTopicQueue2(ch)
	select {}
}

四、Direct 和 Topic 对比

  • Topic 交换机接收的消息 RoutingKey 可以是多个单词,以 . 分割
  • Topic 交换机与队列绑定时的 bindingKey 可以指定通配符
  • #:代表 0 个或多个词
  • *:代表 1 个词

Direct:RoutingKey 必须完全相等匹配;Topic:支持通配符模糊匹配,灵活性更高。

五、总结

  1. Fanout:广播,消息发给所有绑定队列,routingKey 无效。适合全局通知。
  2. Direct:定向,完全匹配 BindingKey。适合精准推送。
  3. Topic:话题,支持通配符模糊匹配,功能最强,业务中最常用。
相关推荐
PingCAP5 小时前
善战者无赫赫之功:消费金融核心系统的数据库选型与升级路径
数据库·分布式·架构
库库的数据库笔记6 小时前
金融核心交易系统的数据库国产信创改造该如何做?
大数据·数据库·分布式·mysql·oracle·金融·数据库架构
爱吃火鸡面呀12 小时前
深入理解 Spark RDD:五大特性与实战解析
大数据·分布式·spark
Linux实战笔记13 小时前
记一次消息队列选型对比线上排查:Kafka/RabbitMQ/Pulsar 技术选型
kafka·rabbitmq·高并发·pulsar·消息队列选型
Norris Huang14 小时前
Kafka Connect 与 Flink CDC 写入 Iceberg:Upsert 的代价,以及 S3 Tables 的真正价值
分布式·flink·kafka
张彦峰ZYF14 小时前
一条消息的丢与重:Kafka 与 RabbitMQ 可靠投递的确认、位点、死信与幂等全链路
后端·kafka·消息队列·rabbitmq·isr·解耦与削峰·丢失与重复
我就是不信14 小时前
集群高并发环境下如何保证分布式唯一全局ID生成?
分布式
黄俊懿1 天前
【架构师从入门到进阶】第六章:服务集群优化——第八节:线程数公式
java·服务器·数据库·分布式·性能优化·架构·系统架构
大菠萝爱上小西瓜2 天前
基于 Spark + Iceberg 湖仓一体的 Netflix 流媒体用户行为全链路数仓与 ALS 智能推荐决策系统(含穿透下钻大屏)
大数据·分布式·spark
吴建旭 智宅焕2 天前
智能家居售后服务体系的架构设计:分布式售后风险的集中式兜底方案
分布式·智能家居