浅谈Go与RabbitMQ(二)

105 阅读4分钟

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

今天来学习下RabbitMQ中的任务队列,并且使用Go来模拟多消费者的情况

任务队列-工作队列

image.png 在上一篇文章中,介绍了如何通过Go来创建一个工作队列,从创建的队列中发送数据和接收数据,这篇文章主要来介绍创建一个工作队列之后,多个消费者之间分配耗时的关系。

工作队列也叫任务队列,主要是用来避免立即执行某些密集型任务时候,需要等待这些任务完成,这样就造成了工作效率的下降,所以我们可以通过异步安排当前的任务,将任务封装成消息发送到队列中,在后台运行的协程就可以取出消息并且执行任务,后台有多个协程工作的时候,这些消息在他们之间是共享的。

我们需要修改上一个文章中的代码,如何模拟发送复杂的任务呢,我们可以通过time.sleep来模拟一些耗时的任务,可以发送带.的字符串封装成消息,每个字符串有多少个.就代表需要执行多少秒,eg: YYQQ....表示执行一个耗时4秒的任务

修改send.go

func getParam(args []string) (res string) {
   if (len(args) < 2) || os.Args[1] == "" {
      res = "YYQQ"
   } else {
      res = strings.Join(args[1:], "")
   }
   return
}
content := getParam(os.Args)//从命令行中返回数据
err = ch.Publish(
   "", 
   queue.Name,
   false,
   false,
   amqp.Publishing{
      DeliveryMode: amqp.Persistent,
      ContentType: "text/plain",
      Body:        []byte(content),
   })

修改receive.go

var wait chan struct{}
go func() {
   for msg := range content {
      log.Printf("Received a message %s", msg.Body)
      dot_cnt := bytes.Count(msg.Body,[]byte(".")) //计算有多少,
      t := time.Duration(dot_cnt)
      time.Sleep(t*time.Second) //运行t秒
      log.Println("Done")
   }
}()

接着打开两个终端,分别执行send.goreceive.go

go run send.go
go run receive.go

image.png

循环调度

在上文的基础上,我们可以通过添加消费者的数量来达到处理多任务的能力,同时运行两个receive.go脚本,它们都可以从工作队列中获取消息,启动后会在等待消息,可以通过按CTRL+C来退出程序

启动send.go脚本,发布5个不同的任务,如下所示:

go run .\goroutines.go msg1.
go run .\goroutines.go msg2..
go run .\goroutines.go msg3...
go run .\goroutines.go msg4....
go run .\goroutines.go msg5.....

第一个消费者: image.png

第二个消费者 image.png

这种多个消费者接收到平均数量的消息属于轮询,一般情况,RabbitMQ将按顺序将每个消息发送给下一个消费者

消息确认

send.go每个任务都有不同程度的耗时,如果一个消费者在执行任务的过程中挂掉了,该怎么处理呢? 在当前的代码中,RabbitMQ一旦将任务发送给消费者,就会标记为已经执行,如果消费者挂掉了,就会丢失这个任务,并且还会丢失尚未处理的消息。

我们可以通过RabbitMQ的消息确认,来确保消息不会丢失。消费者在完成一个任务之后会返回一个确认(AK) 信号,来告知RabbitMQ可以删除任务了。

我们可以手动消息确认,Ack()传入一个false,从消费者中发送一个正确的确认

go func() {
   for msg := range content {
      log.Printf("Received a message %s", msg.Body)
      dot_cnt := bytes.Count(msg.Body, []byte(".")) //计算有多少,
      t := time.Duration(dot_cnt)
      time.Sleep(t * time.Second) //运行t秒
      log.Println("Done")
      msg.Ack(false) //手动确认消息
   }
}()

如果是RabbitMQ退出或者崩溃,就要将队列和消息都标记为持久的

queue, err := ch.QueueDeclare(
	"YYQQ", // name
	true,    // 声明为持久队列
	false,   // delete when unused
	false,   // exclusive
	false,   // no-wait
	nil,     // arguments
)

将消息标记为持久的: 通过使用amqp.Publishing中的持久性选项amqp.Persistent

err = ch.Publish(
   "",
   queue.Name,
   false,
   false,
   amqp.Publishing{
      DeliveryMode: amqp.Persistent, //持久 交付模式
      ContentType:  "text/plain",
      Body:         []byte(content),
   })

公平分发

在两个消费者的情况下,当所有的奇数消息都是耗时长的消息而偶数消息都是耗时短的消息时,一个消费者持续忙碌,而另一个消费者几乎不做任何工作,为了避免这种情况发生,我们可以将预取计数设置为1。这告诉RabbitMQ不要一次向一个消费者发出多个消息

err = ch.Qos(
  1,     // prefetch count
  0,     // prefetch size
  false, // global
)

总结

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