浅谈Go与RabbitMQ(三)

132 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情

今天接着来学习下RabbitMQ中的发布和订阅,上一篇文章中介绍了如何创建一个工作队列,每个任务只能发送一个消费者,那么接下来就要实现一个任务多个消费者都可以接收到,这就是今天要介绍的订阅/发布模式

发布和订阅

通过搭建一个简单的日志系统来说明这个模式,这个日志系统由两个程序组成:一个发送日志,另一个接收消息并且输出日志信息,相当于已经发布的日志消息将被广播到所有消费者手里。

Exchanges-交换器

之前的文章中讲到,生产者是发送消息的程序,队列是消息存储的缓冲区,消费者是接收消息的程序 RabbitMQ消息传递模型中核心思想是不将消息直接发送到队列上,生产者不知道消息是否传递到队列上

真实情况下,生产者只能将消息发送到Exchanges交换器,交换器的作用一是接收来自生产者的消息,另一面方面将消息送入队列中,不同类型的交换器定义有不同的处理消息的规则,例如是直接添加到特定的队列,还是添加其他多个队列,或者直接丢弃等。

image.png

交换器类型:

  • direct
  • topic
  • headers
  • fanout

下面以fanout类型交换器为例子,它的作用是将接收到的所有消息广播到所有已知的队列中

err = ch.ExchangeDeclare(
  "logs",   // name
  "fanout", // type
  true,     // durable
  false,    // auto-deleted
  false,    // internal
  false,    // no-wait
  nil,      // arguments
)

回想下之前是怎么发布消息的,使用了一个默认的交换器,该交换器没有名称,以route_key参数指定的名称路由到队列

 err = ch.Publish(
   "",     // exchange
   q.Name, // routing key
   false,  // mandatory
   false,  // immediate
   amqp.Publishing{
    ContentType: "text/plain",
     Body:        []byte(body),
 })

现在可以改为发布到我们自己的定义的交换器

err = ch.ExchangeDeclare(
  "logs",   // 使用命名的交换器
  "fanout", // 交换器类型
  true,     // durable
  false,    // auto-deleted
  false,    // internal
  false,    // no-wait
  nil,      // arguments
)
DoError(err, "Failed to declare an exchange")

content := bodyFrom(os.Args)
err = ch.Publish(
  "logs", // exchange
  "",     // routing key
  false,  // mandatory
  false,  // immediate
  amqp.Publishing{
          ContentType: "text/plain",
          Body:        []byte(content),
  })

临时队列

之前使用的具有特定名称的队列,传递一个空字符串作为队列名称时,会随机生成的名称创建一个非持久队列,一旦消费者断开连接,队列就会自动删除

q, err := ch.QueueDeclare(
  "",    // 空字符串作为队列名称
  false, // 非持久队列
  false, // delete when unused
  true,  // 独占队列(当前声明队列的连接关闭后即被删除)
  false, // no-wait
  nil,   // arguments
)

绑定

我们已经创建了一个fanout类型的交换器和一个队列。现在我们需要告诉交换器将消息发送到我们的队列,交换器和队列之间的联系叫做绑定

err = ch.QueueBind(
  q.Name, // queue name
  "",     // routing key
  "logs", // exchange
  false,
  nil,
)

生产者的代码最重要的是将消息发布到logs交换器,而不是空的消息交换器。 发送时,我们需要提供一个routingKey,但是对于fanout型交换器,它的值可以被忽略(传空字符串)

func main() {
   conn, err := amqp.Dial("amqp://admin:admin@localhost:5672/")
   DoError(err, "Failed connect to RabbitMQ!")
   defer conn.Close()
   ch, err := conn.Channel()
   DoError(err, "Failed to open a channel")
   defer ch.Close()

   //这里注释
   //queue, err := ch.QueueDeclare(
   // "YYQQ",
   // true,
   // false,
   // false,
   // false,
   // nil,
   //)  
   //DoError(err, "Failed to declare a queue")

   err = ch.ExchangeDeclare(
      "logs",
      "fanout",
      true,
      false,
      false,
      false,
      nil)
   DoError(err, "Failed to declare a exchange")
   
   content := getParam(os.Args) //从命令行中返回数据
   err = ch.Publish(
      "logs",
      "",
      false,
      false,
      amqp.Publishing{
         DeliveryMode: amqp.Persistent,
         ContentType:  "text/plain",
         Body:         []byte(content),
      })
   DoError(err, "Failed to publish a message")
}

注意: 禁止发布到未声明的交换器,如果没有队列绑定交换器则消息丢失,这个做法是安全的,因为没有消费者接收就说明没有人需要订阅这个消息,直接丢弃即可。

func main() {
   conn, err := amqp.Dial("amqp://admin:admin@localhost:5672/")
   DoError(err, "Failed to connect to RabbitMQ")
   defer conn.Close()

   ch, err := conn.Channel()
   DoError(err, "Failed to open a channel")
   defer ch.Close()

   err = ch.ExchangeDeclare(
      "logs",
      "fanout",
      true,
      false,
      false,
      false,
      nil)
   DoError(err, "Failed to declare a exchange")

   queue, err := ch.QueueDeclare(
      "",    // name
      false, // durable
      false, // delete when unused
      true,  // exclusive
      false, // no-wait
      nil,   // arguments
   )
   DoError(err, "Failed to declare a queue")
   fmt.Println("fmt.name",queue.Name)
   
    //绑定交换器
   err = ch.QueueBind(
      queue.Name,
      "",
      "logs",
      false,
      nil)
   DoError(err, "Failed to bind a queue")
   
   content, err := ch.Consume(
      queue.Name, // queue
      "",    // consumer
      true,  // auto-ack
      false, // exclusive
      false, // no-local
      false, // no-wait
      nil,   // args
   )
   DoError(err, "Failed to register a consumer")
   var wait chan struct{}
   go func() {
      for msg := range content {
         log.Printf("Received a message %s", msg.Body)       
      }
   }()
   <-wait

}

如果将日志保存到文件,就可以运行下面代码

go run send.go : 发送日志
go run receive.go > logs.log

如果想直接在控制台上查看日志,可以终端上输入

go run receive.go 

使用rabbitmqctl list_bindings命令,可以查看绑定关系和队列,开启两个receive.go程序会有两个logs绑定信息,如下图所示

image.png 消费者一 image.png 消费者二 image.png

总结

今天浅谈了Go与RabbitMQ(三),还有很多相关的知识后面会继续深入,对于刚入门go语言的我来说,还有许多地方需要学习,有错误的地方欢迎大家指出,共同进步!!