work queue

主要思想: 避免立即执行资源密集型任务,而不得不等待它完成。安排任务在之后执行,我们将任务封装为消息并放送到队列。而后台的工作进程从消息队列中读取消息并执行任务。当有多个工作线程时,这些线程将一起处理这些任务。

这个概念在 Web 应用程序中特别有用,在这些应用程序中,无法在短暂的 HTTP 请求窗口中处理复杂的任务。

work queue适合在集群环境中做异步处理,能最大程度发挥每一台服务器的性能

rabbitmq queues 过滤 rabbitmq work queue_rabbitmq queues 过滤

生产者代码, 简单的往队列发送20条消息

package main

import (
	"fmt"
	"github.com/streadway/amqp"
	"log"
	"rabbit/utils"
)

func main() {
	ch, _ := utils.GetRabbitMQChannel()

	q, _ := ch.QueueDeclare(
		"hello", // name
		false,   // durable
		false,   // delete when unused
		false,   // exclusive
		false,   // no-wait
		nil,     // arguments
	)
	for i := 0; i < 20; i++ {
		msg := fmt.Sprintf("%d号消息", i+1)
		_ = ch.Publish(
			"",     // exchange
			q.Name, // routing key
			false,  // mandatory
			false,
			amqp.Publishing{
				DeliveryMode: amqp.Persistent,
				ContentType:  "text/plain",
				Body:         []byte(msg),
			})
		log.Printf(" Sent %s", msg)
	}
}

消费者代码, 开启三个goroutine作为worker从队列里读数据

package main

import (
	"github.com/streadway/amqp"
	"log"
	"rabbit/utils"
	"sync"
)

func main() {
	ch, _ := utils.GetRabbitMQChannel()
	q, _ := ch.QueueDeclare(
		"hello", // name
		false,   // durable
		false,   // delete when unused
		false,   // exclusive
		false,   // no-wait
		nil,     // arguments
	)
	var wg sync.WaitGroup
	wg.Add(3)
	for i := 0; i < 3; i++ {
		go work(i+1, q, ch, &wg)
	}
	wg.Wait()
}

func work(id int, q amqp.Queue, ch *amqp.Channel, wg *sync.WaitGroup) {
	defer wg.Done()
	msgs, _ := ch.Consume(
		q.Name, // queue
		"",     // consumer
		true,   // auto-ack
		false,  // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	forever := make(chan bool)
	go func() {
		for d := range msgs {
			log.Printf("[%d号worker]Received a message: %s", id, d.Body)
		}
	}()
	<-forever
}

运行结果

F:\go_workspace\mq\rabbit\work-queues\workers>go run worker.go
2022/04/18 22:10:12 [3号worker]Received a message: 1号消息
2022/04/18 22:10:12 [1号worker]Received a message: 2号消息
2022/04/18 22:10:12 [1号worker]Received a message: 5号消息
2022/04/18 22:10:12 [3号worker]Received a message: 4号消息
2022/04/18 22:10:12 [3号worker]Received a message: 7号消息
2022/04/18 22:10:12 [3号worker]Received a message: 10号消息
2022/04/18 22:10:12 [2号worker]Received a message: 3号消息
2022/04/18 22:10:12 [2号worker]Received a message: 6号消息
2022/04/18 22:10:12 [2号worker]Received a message: 9号消息
2022/04/18 22:10:12 [1号worker]Received a message: 8号消息
2022/04/18 22:10:12 [1号worker]Received a message: 11号消息
2022/04/18 22:10:12 [2号worker]Received a message: 12号消息
2022/04/18 22:10:12 [2号worker]Received a message: 15号消息
2022/04/18 22:10:12 [3号worker]Received a message: 13号消息
2022/04/18 22:10:12 [1号worker]Received a message: 14号消息
2022/04/18 22:10:12 [1号worker]Received a message: 17号消息
2022/04/18 22:10:12 [3号worker]Received a message: 16号消息
2022/04/18 22:10:12 [3号worker]Received a message: 19号消息
2022/04/18 22:10:12 [2号worker]Received a message: 18号消息
2022/04/18 22:10:12 [1号worker]Received a message: 20号消息

仔细观察发现,

消息号n%3 等于1的都是3号worker处理的;

消息号n%3 等于2的都是1号worker处理的;

消息号n%3 等于0的都是2号worker处理的。

就是轮询机制

消息确认[Message acknowledgment]

因为RabbitMQ一旦向消费者传递一条消息,便立即将消息标记为删除。消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个任务只完成了部分突然挂掉,这样会导致我们丢失正在处理的消息,以及后续发送给该消费者的消息。

消息应答的方法:

  1. 自动确认
msgs, err := ch.Consume(
  q.Name, // queue
  "",     // consumer
  false,  // auto-ack
  false,  // exclusive
  false,  // no-local
  false,  // no-wait
  nil,    // args
)
  1. 手动确认
go func() {
    for d := range msgs {
        log.Printf("Received a message: %s", d.Body)
        d.Ack(false) //不批量确认
    }
}()

推荐使用手动确认,并其不批量确认

消息自动重新入队

如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或TCP连接丢失), 导致消息未发送ACK确认,RabbitMQ将了解到消息未完全处理,并将其重新加入到队列中,等待其他消费者来处理。

消息持久化[message durability]

首先,我们需要确保队列能够在 RabbitMQ 节点重启后继续存在。

生产者和消费者的队列声明都需要设置durable: true

q, err := ch.QueueDeclare(
  "hello",      // name
  true,         // durable
  false,        // delete when unused
  false,        // exclusive
  false,        // no-wait
  nil,          // arguments
)

其次, 我们需要将我们的消息标记为持久 - 通过使用 amqp.Persistent 选项 amqp.Publishing 采取。

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

注意:rabbitmq中的队列是不允许同名但属性不同的

公平分发[fair dispatch]

轮询分发并不算是公平的。例如在有两个worker的情况下,当所有奇数消息很重而偶数消息都很轻时,一个worker会一直很忙,另一个worker几乎不会做任何工作。

消费者添加代码

err = ch.Qos(
  1,     // prefetch count
  0,     // prefetch size
  false, // global
)
failOnError(err, "Failed to set QoS")

prefechCount = 1表示:在处理并确认前一个消息之前,不要向worker发送新消息。相反,它将把它分派给下一个不忙的worker。

发布确认

单个确认发布
批量确认
异步批量确认