【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:话题,支持通配符模糊匹配,功能最强,业务中最常用。
相关推荐
.冰块.3 小时前
Ceph 分布式存储实战(二):存储池管理与 cephx 认证授权
分布式·ceph·rados·纠删码·复本池·cephx认证与用户权限
我们从未走散5 小时前
从内存字典到租约内核:分布式上游账号池如何做到不超卖
分布式
.冰块.6 小时前
Ceph 分布式存储实战(一):架构入门与 cephadm 集群部署
分布式·ceph·架构·cephadm
j7~7 小时前
【Redis】《一文走完服务端高并发架构演进:从单机架构到 K8S 容器编排》
分布式·架构演进·服务端高并发分布式结构演进之路·架构基本概念·架构评价指标
Flynt7 小时前
把 bug tracker 塞进 git 仓库,同步和冲突怎么解决?我把 git-bug 拆开实测了一遍
分布式·git·开源
灯澜忆梦1 天前
【RabbitMQ #3】 | Go 客户端 + SpringAMQP
分布式·golang·rabbitmq
Q26433650231 天前
【有源码】基于Spark的电商客户细分与盈利洞察分析系统-面向精准营销的电商客户细分模型构建与盈利能力可视化研究
大数据·hadoop·分布式·数据挖掘·数据分析·spark·毕业设计
晴空蓝天1 天前
RabbitMQ 消息可靠投递:confirm 确认 + 消费重试 + 死信队列
rabbitmq