在前面的教程中,使用 fanout类型的交换机实现了广播消息, direct 交换机对消息进行筛选。
虽然 direct 交换机已经可以帮我们实现消息分类和筛选,但还是不够灵活。
假设我们日志系统希望可以增加一个维度来区别消息,仅仅使用日志的错误严重性不太够,还需要增加一个日志来源加以区分。
来自 corn系统的消息,我们只关心错误日志,而来自 kern 系统的话,我们则需要所有的日志信息。
RabbitMQ 提供了 topic 交换机来实现这种效果。
Topic exchange
topic 交换机的 routing key的值有一定的要求 - 使用点.来分割的一组单词。一般使用和消息内容相关的一组单词。比如: stock.usd.nyse,nyse.vmw, quick.orange.rabbit ,长度只要不超过 255 个字节都可以。
binding key 和 routing key 的格式相同。topic 和 direct 类型的交换机有一点相似 - 消息的routing key 和队列绑定的binding key相匹配时,会将其加入到队列之中。
有 2 种特殊的格式需要注意:
*通配符,可以表示所有的单词#可以表示一个或者多个单词

假设我们现在需要发送所有关于动物的消息,这些消息将会根据动物的描述信息来进行区分。行动速度.颜色.物种 的格式进行区分。
上图中有三个分发消息的规则,颜色 为橙色的动物需要分发到 Q1 队列中,而物种为 兔子和 动作速度缓慢的动物都分发到 Q2 队列中。
如果此时有一个 routing key为 quick.orange.rabbit,同时满足binding key ,所以 Q1 和 Q2 都会收到该消息。同理 lazy.orange.elephant 也会同时分发到两个队列中,而 quick.orange.fox 只满足了 Q1的 binding key ,只有 Q1才会收到该消息。lazy.brown.fox 只会分发到 Q2 队列中。lazy.pink.rabbit - 虽然同时满足 Q2的两个 binding key,但只会向 Q2 队列添加一次。而 binding key 像quick.brown.fox 的消息,则会被丢弃,因为无法找到满足条件的 binding key。
当消息的 routing key 不符合 binding key 的规则时会发生什么? 单个单词orange 或者四个单词 quick.orange.new.rabbit,不符合 binding key 中的任何一个规则,那么它就会被丢弃掉。
那么lazy.orange.new.rabbit这个又会发生什么?它会被分发到 Q2 队列中,因为 Q2 的binding key 规则中有一个#,表示 lazy 后面接多少都是满足的,只要是lazy.开头即可。
Topic 类型交换机
topic类型的交换机十分灵活,可以模拟其他类型的交换机
- 将
binding key设置成#,那么就会接收所有的消息,就像是fanout类型的交换机一样binding key中不要设置*或者#,那么它又会像direct类型的交换机一样了
Putting it all together
将原来的日志系统使用topic交换机改造一下,规定binding key格式: <facility>.<severity>。
// emit_log_topic.go
package main
import (
"context"
"log"
"os"
"strings"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
// 1.建立连接
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect RabbitMQ")
defer conn.Close()
// 2.打开 channel
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close()
// 3. 声明交换机
err = ch.ExchangeDeclare(
"logs_topic",
"topic",
true,
false,
false,
false,
nil)
failOnError(err, "Failed to declare an exchange")
// 4.创建上下文
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 5.构造消息体
body := bodyFrom(os.Args)
// 6.发送消息
err = ch.PublishWithContext(ctx,
"logs_topic",
severityFrom(os.Args),
false,
false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(body),
})
failOnError(err, "Failed to publish a message")
log.Printf(" [x] Sent %s", body)
}
func bodyFrom(args []string) string {
var s string
if (len(args) < 3) || os.Args[2] == "" {
s = "hello"
} else {
s = strings.Join(args[2:], " ")
}
return s
}
func severityFrom(args []string) string {
var s string
if (len(args) < 2) || os.Args[1] == "" {
s = "anonymous.info"
} else {
s = os.Args[1]
}
return s
}
// receive_logs_topic.go
package main
import (
"log"
"os"
amqp "github.com/rabbitmq/amqp091-go"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
// 1.建立连接
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect RabbitMQ")
defer conn.Close()
// 2.打开 channel
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close()
// 3.声明交换机
err = ch.ExchangeDeclare(
"logs_topic",
"topic",
true,
false,
false,
false,
nil)
failOnError(err, "Failed to declare an exchange")
// 4.声明队列
q, err := ch.QueueDeclare("",
false,
false,
true,
false,
nil)
failOnError(err, "Failed to declare a quque")
// 5.获取参数
if len(os.Args) < 2 {
log.Printf("Usage: %s [binding_key]...", os.Args[0])
os.Exit(0)
}
// 6.绑定队列
for _, s := range os.Args[1:] {
log.Printf("Binding queue %s to exchange %s with routing key %s",
q.Name, "logs_topic", s)
err = ch.QueueBind(
q.Name,
s,
"logs_topic",
false,
nil)
failOnError(err, "Failed to bind a queue")
}
// 7.创建消费者
msgs, err := ch.Consume(
q.Name,
"",
true,
false,
false,
false,
nil)
failOnError(err, "Failed to register a consumer")
// 8.监听消息,避免退出
var forever chan struct{}
go func() {
for d := range msgs {
log.Printf(" [x] %s", d.Body)
}
}()
log.Printf(" [*] Waiting for logs. To exit press CTRL+C")
<-forever
}
完成代码: emit_log_topic.go 、 receive_logs_topic.go
启动接收程序
// 接收所有消息
go run receive_logs_topic.go "#"
// 接收来自 `kern` 的消息
go run receive_logs_topic.go "kern.*"
// 接收类型为 `critical` 的消息
go run receive_logs_topic.go "*.critical"
// 绑定多个key
go run receive_logs_topic.go "kern.*" "*.critical"
启动发送消息程序
go run emit_log_topic.go "kern.critical" "A critical kernel error"
发送消息后,上述所有的接收端都会接收到消息