前言:生产环境消息一般都经过 Exchange(交换机)转发,不会直接发送到队列。交换机主要分为 3 种:Fanout(广播)、Direct(定向)、Topic(话题)。
一、FanoutExchange 广播交换机
原理
publisher ---> exchange ---> queue1 ---> consumer1
---> queue2 ---> consumer2
Fanout 会把消息广播转发给所有绑定到此交换机的队列,RoutingKey 无效。
场景:多个实例绑定同一个队列可以提升消费速度;消息需要推送给多个微服务。
案例:SpringAMQP 演示 FanoutExchange(Go 实现)
需求:
- RabbitMQ 控制台创建队列
fanout.queue1、fanout.queue2 - 创建交换机
hmall.fanout,将两个队列绑定交换机 - 消费者监听两个队列
- 生产者向
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 定向路由消息到匹配的队列。 规则:
- 每个 Queue 与 Exchange 绑定,指定BindingKey(暗号)
- 生产者发消息时指定消息的RoutingKey
- 交换机把消息路由到
BindingKey == RoutingKey的队列
例子:
- queue1 绑定 key:blue、red
- queue2 绑定 key:yellow、red
- 发送 routingKey=blue → 只有 queue1 接收
- 发送 routingKey=yellow → 只有 queue2 接收
- 发送 routingKey=red → 两个队列都收到(可模拟广播效果)
案例
需求:
- RabbitMQ 控制台创建
direct.queue1、direct.queue2 - 创建
hmall.direct交换机,绑定 key:red/yellow/blue - 消费者监听两个队列
- 生产者发送不同 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)
测试场景:
- routingKey =
japan.news→ 只有 queue2 收到 - routingKey =
china.news→ queue1、queue2 都收到 - routingKey =
china.weather→ 只有 queue1 收到
Topic 是三种交换机里最灵活的。
案例
需求:
- RabbitMQ 控制台创建队列
topic.queue1、topic.queue2 - 创建交换机
hmall.topic,绑定队列 - 消费者监听两个队列
- 生产者使用不同 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:支持通配符模糊匹配,灵活性更高。
五、总结
- Fanout:广播,消息发给所有绑定队列,routingKey 无效。适合全局通知。
- Direct:定向,完全匹配 BindingKey。适合精准推送。
- Topic:话题,支持通配符模糊匹配,功能最强,业务中最常用。