golang RabbitMQ教程(3) 发布/订阅模式
在前一篇介绍中实现了一个工作队列,它假设队列中的每一个任务都只会被分发到一个工作者进行处理。在本篇中,我们尝试将同一个消息发送给多个消费者进行处理,这就是广为人知的发布/订阅模式。
本篇通过搭建一个日志系统来阐述发布/订阅模式,它包含两部分内容:一个用于产生日志消息的程序,另一个用于接收和打印消息。
在这个日志系统中,每一份接收者程序的拷贝都能收到消息,因此我们可以轻易地使用一个程序将日志写入磁盘,而另一个程序直接在屏幕显示。
本质上来说,当系统收到一个日志处理请求时,会把这个消息广播给所有的接收者。
Exchanges#
之前的介绍中,我们都是以队列为中介进行消息的发送和接收,现在将完整的介绍一下RabbitMQ的消息模式。
对前述内容做一个简单总结:
- 一个producter(生产者)是指用于发送消息的用户程序;
- 一个queue(队列)是用来存储消息的缓冲区;
- 一个consumer(消费者)是用来接收消息的用户程序;
RabbitMQ消息模式的核心内容是一个producter永远不会将消息直接发送给队列。
Producter甚至都不知道其产生的消息会被分发到哪一个队列,实际上,producter只会将消息发送给exchange.Exchange很好理解,类似于一个中转站,它就是将从producter中接收到的消息转发给与之绑定的队列。
当Exchange接收到消息后,它是如何来确定对消息进行处理的呢?是将消息发送到指定的一个队列,还是广播到所有队列,或者是直接将其忽略?
这一切都由Exchange定义是的类型(type)来控制。
RabbitMQ共有四种exchange类型:direct, topic, headers, fanout.我们这里使用的最后一种fanout,现在就让我们来定义一个type,取名logs:
err = ch.ExchangeDeclare(
"logs", //name
"fanout", //type
"true", //durable
false, //auto-deleted
false, //internal
false, //no-wait
nil, //arguments
)
这个fanout类型的exchange很简单,顾名思义:它将所接收到的消息广播给所有绑定的队列。这也正是日志系统说要做的工作。
查看exchange
可以通过rabbitmqctl命令来查看所有的exchanges:
sodu rabbitmqctl list_exchanges
命令执行后,会列出一些amq.*名称的exchanges,这是系统默认存在的,我们现在还用不到这些。
默认的exchange
你可能会感到奇怪,前面例子并没有提及生产者只能将消息发送给exchange,为什么程序仍能将消息发送给队列?原因在于我们使用了一个默认的exchange,代码中就是Publish函数的参数使用了空字符串"":
来看看之前的publish代码:
err = ch.Publish(
"", //exchange
q.Name, //routing key
false, //mandatory
false, //immediate
amqp.Publishing(
ContentType: "text/plain",
Body: []byte(body),
)
)
使用一个无命名的或默认的exchange,消息将会根据routing_key所指定的参数进行查找,如果存在就会分发到相应的队列。
既然讲到的exchange,那么我们可以使用前面定义的exchange来代替默认值:
err = ch.ExchangeDeclare(
"logs", //name
"fanout", //type
true, //durable
false, //auto-deleted
false, //internal
false, //no-wait
nil, //arguments
)
failOnError(err, "Failed to declare an exchange")
body:= bodyForm(os.Args)
err = ch.Publish(
"logs", //exchange
"", //routing key
false, //mandatory
false, //immediate
amqp.Publishing(
ContentType: "text/plain",
Body: []byte(body),
)
)
临时队列#
不出意外,你应该还对前面使用过的两个命名队列(hello和task_queue)有印象,在使用命名队列时必须让生产者和消费者都是用同一个名称的队列,否则消息将无法在两者之间进行传递。
但在这里名字不是关心的重点,因为我们的日志系统需要记录所有的消息,而不是其中的一部分。我们比较关心的是消费者程序接收和处理的消息都应该是未处理过的。
为了确保这一点,我们需要两个条件:
首先,无论何时当消费者连接到Rabbit时我们需要一个新的、空的队列,因此就不会存在之前的消息。我们可以通过创建一个随机名字的队列来实现,而更好的方法是:让服务器自己选择一个随机队列给我们。
再者,当我们的消费者程序断开连接时,这个队列要能自动的删除。
在amqp客户端中,当我们将空字符串指定为队列名字时,将会创建一个非持久化的、带有随机命名的队列:
q, err := ch.QueueDeclare(
"", //name
false, //durable
false, //delete when unused
true, //exclusive
false, //no-wait
nil, //arguments
)
当这个函数返回时,RabbitMQ将创建一个带有随机名字的队列,如amq.gen-JzTY20BRgKO-HjmUJj0wLg.
当这个连接被关闭时,队列将会被删除,因为其被定义为独有的(exclusive)。
绑定#
到现在,我们已经创建了一个fanout类型的exchange和一个队列,然而exchange并不知道它要哪个队列是应该被分发消息的。因此,我们需要明确的指定exchange和队列队列之间的关系,这个操作称之为绑定。
err = ch.QueueBind(
q.Name, //queue name
"", //routing key
"logs", //exchange
false,
nil
)
现在,logs exchange中的消息就会被分发到我们的队列。
查看绑定列表
仍然可以通过命令来查看:
rabbitmqctl list_bindings
完整的例子#
生产者程序看起来跟之前例子区别不大,最重要的不同就是这里将消息发送给名为logs的exchange,而不是直接发送到默认队列。在发送消息时需要提供一个routingKey,但是在fanout类型的exchange中这个值是被忽略的。
那么,emit_log.go的脚本就是:
package main
import (
"log"
"os"
"strings"
"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, "Failed to connect to RabbitMQ")
defer conn.Close()
// 创建一个channel
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close()
// 声明交换器
err = ch.ExchangeDeclare(
"logs", // name
"fanout", // type
true, // durable
false, // auto-deleted
false, // internal
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare an exchange")
body := bodyFrom(os.Args)
err = ch.Publish(
"logs", // exchange
"", // routing key
false, // mandatory
false, // immediate
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) < 2) || os.Args[1] == "" {
s = "hello"
} else {
s = strings.Join(args[1:], " ")
}
return s
}
需要注意,我们必须在建立了连接之后才能定义exchange,否则会报错。
如果exchange没有绑定任何一个队列,那么消息将会丢失而没有得到处理,但在这个例子里,这种情况是允许的,如果没有任何一个队列来消费这些消息,那么就直接忽略掉就好。
下面是receive_logs.go的代码:
package main
import (
"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, "Failed to connect to RabbitMQ")
defer conn.Close()
// 创建一个channel
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close()
// 声明交换器
err = ch.ExchangeDeclare(
"logs", // name
"fanout", // type
true, // durable
false, // auto-deleted
false, // internal
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare an exchange")
// 声明队列
q, err := ch.QueueDeclare(
"", // name
false, // durable
false, // delete when unused
true, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue")
// 绑定交换器
err = ch.QueueBind(
q.Name, // queue name
"", // routing key
"logs", // exchange
false,
nil,
)
failOnError(err, "Failed to bind a queue")
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer")
forever := make(chan bool)
go func() {
for d := range msgs {
log.Printf(" [x] %s", d.Body)
}
}()
log.Printf(" [*] Waiting for logs. To exit press CTRL+C")
<-forever
}
如果你想将日志消息保存到文件,只需在命令终端中执行下面的命令:
go run receive_logs.go > logs_from_rabbit.log
如果想直接打印到屏幕上,在另一个终端中执行:
go run receive_logs.go
当然,发送消息的命令如下:
go run emit_log.go
使用rabbitmqctl list_bindings命令可以查看上面代码所创建的绑定关系,如当运行两个receive_logs.go之后,可能会得到如下的结果:
sudo rabbitmqctl list_bindings
# => Listing bindings ...
# => logs exchange amq.gen-JzTY20BRgKO-HjmUJj0wLg queue []
# => logs exchange amq.gen-vso0PVvyiRIL2WoV3i48Yg queue []
# => ...done.
其结果是很容易理解的:从logs exchange中的消息会被转发到两个由系统命名的队列中。这也正是我们所期望的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?