【RabbitMQ(二)】队列使用

引言

在上一节【RabbitMQ(一)】docker-compose安装rabbitmq及简单使用Hello World中,我们介绍了Rabbitmq的安装和简单操作。达到了生产者发送一个Hello World之后,消费者便能接收到一个Hello World的效果。

  1. 如果有多个消费者同时在接收消息,消息会如何分发呢?
  2. 消费者在处理消息的时候宕机了,那这个消息怎么办呢?
  3. rabbitmq宕机了,其中的队列和消息怎么办?
  4. 两个消费者情况下,消息分发不合理,一个特忙,一个特闲怎么办?

带着这些问题,开始RabbitMQ第二节的学习内容,队列的使用及策略

循环调度

使用任务队列的优点之一是能够轻松并行化工作。如果我们的工作正在积压,我们可以增加更多的工人,这样就可以轻松扩展。

这里我们利用上一节中consumer.go开启两个consumer在准备接收消息,然后不断运行producer.go,会发现结果是怎样的呢?

代码部分就不做展示运行了,不难发现两个consumer是一次循环接收到了消息,也就是第一个消息被consumer1接收,第二个消息便会被xonsumer2接收,依次循环下去。也可以开启3个consumer甚至更多看一样,结果都是一样的。

这样我们可以得出结果:

默认情况下,RabbitMQ将按顺序将每个消息发送给下一个消费者。平均而言,每个消费者都会收到相同数量的消息。这种分发消息的方式称为轮询

消息确认

consumer 完成任务可能需要耗费几秒钟,如果一个consumer在任务执行过程中宕机了该怎么办呢?我们当前的代码中,RabbitMQ一旦向消费者传递了一条消息,便立即将其标记为删除。在这种情况下,如果你终止一个consumer那么你就可能会丢失这个任务,我们还将丢失所有已经交付给这个consumer的尚未处理的消息。

我们不想丢失任何任务,如果一个consumer意外宕机了,那么我们希望将任务交付给其他consumer来处理。

为了确保消息永不丢失,RabbitMQ支持 消息确认消费者发送回一个确认(acknowledgement),以告知RabbitMQ已经接收,处理了特定的消息,并且RabbitMQ可以自由删除它。

如果使用者在不发送确认的情况下死亡(其通道已关闭,连接已关闭或TCP连接丢失),RabbitMQ将得知消息未完全处理,并将对其重新排队。如果同时有其他消费者在线,它将很快将其重新分发给另一个消费者。这样,您可以确保即使工人偶尔死亡也不会丢失任何消息。

没有任何消息超时;RabbitMQ将在消费者死亡时重新传递消息。即使处理一条消息需要很长时间也没关系。

手动确认模式

在这里,我们将使用手动消息确认,方法是为“auto-ack”参数传递一个false,然后在完成任务后,使用d.Ack(false)从consumer发送一个正确的确认(这将确认一次传递)。

将第一节中的consumer.go在接收消息时由自动消息确认模式切换成手动传递消息确认,完整代码如下:

package main

import (
	"log"
	"time"

	"github.com/streadway/amqp"
)

func main() {
    
    
	// 建立连接
	conn, err := amqp.Dial("amqp://root:[email protected]:5672/")
	if err != nil {
    
    
		log.Fatalf("%s", "Failed to connect to RabbitMQ", err)
		return
	}
	defer conn.Close()

	// 获取channel
	ch, err := conn.Channel()
	if err != nil {
    
    
		log.Fatalf("%s", "Failed to open a channel", err)
		return
	}
	defer ch.Close()

	// 声明队列
	//请注意,我们也在这里声明队列。因为我们可能在发布者之前启动使用者,所以我们希望在尝试使用队列中的消息之前确保队列存在。
	q, err := ch.QueueDeclare(
		"hello", // name
		false,   // durable
		false,   // delete when unused
		false,   // exclusive
		false,   // no-wait
		nil,     // arguments
	)
	if err != nil {
    
    
		log.Fatalf("%s", "Failed to declare a queue", err)
		return
	}
	// 获取接收消息的Delivery通道
	msgs, err := ch.Consume(
		q.Name, // queue
		"",     // consumer
		false,  // auto-ack  //注意这里传false,关闭自动消息确认
		false,  // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	if err != nil {
    
    
		log.Fatalf("%s", "Failed to register a consumer", err)
		return
	}
	go func() {
    
    
		for d := range msgs {
    
    
			log.Printf("Received a message: %s", d.Body)
			time.Sleep(3 * time.Second) //模拟任务处理耗时
			log.Printf("Done")
			d.Ack(false) // 手动传递消息确认
		}
	}()
	log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
	select {
    
    }
}

使用这段代码,可以确保即使在处理消息时使用CTRL+C杀死一个worker,也不会丢失任何内容。在worker死后不久,所有未确认的消息都将被重新发送。

消息确认必须在接收消息的同一通道(Channel)上发送。尝试使用不同的通道(Channel)进行消息确认将导致通道级协议异常。有关更多信息,请参阅确认的文档指南

忘记确认

在上面的代码中来使用队列是没有任何问题的,但如果我们在代码忘记添加d.Ack(false)这一行会这么样呢?

忘记确认是一个常见的错误。这是一个简单的错误,但后果是严重的。当你的消费端退出时,消息将被重新传递到别的消费者(这看起来像随机重新传递),但是RabbitMQ在忘记确认的消费者退出前将消耗越来越多的内存,因为它无法释放任何未确认的消息(所有未被该消费者手动确认的消息都得不到删除,会一直存在内存中)。

那这种情况发生,我们应该如何调试呢?

rabbitmqctl list_queues name messages_ready messages_unacknowledged
//使用这个命令可以打印出未被确认的消息
  1. 只有一个消费者时,忘记确认的消费者未退出前:

在这里插入图片描述
messages_unacknowledged的个数会随着producer发送消息个数增加,这些消息因为未得到确认,故而一直占用内存

  1. 忘记确认的消费者退出时

在这里插入图片描述
之前未被确认的消息转为ready状态,在有消费者连接时,会被重新发送,此时状态下依然占用内存

  1. 有消费者连接时。如果是忘记确认的消费者,则重复上面的步骤。连接上正常确认的消费者时,正常结果如下:
    在这里插入图片描述

消息持久化

我们已经学会了如何确保即使消费者死亡,消息也不会丢失。但是如果RabbitMQ服务器停止运行,我们的消息仍然会丢失。

当RabbitMQ退出或崩溃时,它将忘记队列和消息,除非您告诉它不要这样做。要确保消息不会丢失,需要做两件事:我们需要将队列和消息都标记为持久的

队列持久化

首先,我们需要确保队列能够在RabbitMQ节点重新启动后继续运行。为此,我们需要声明它是持久的:

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

虽然这个命令本身是正确的,但它在我们当前的设置中不起作用。这是因为我们已经定义了一个名为hello的队列,它不是持久的RabbitMQ不允许你使用不同的参数重新定义现有队列,并将向任何尝试重新定义的程序返回错误。但是有一个快速的解决方法——让我们声明一个具有不同名称的队列,例如task_queue:

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

这种持久的选项更改需要同时应用于生产者代码和消费者代码。

在这一点上,我们确信即使RabbitMQ重新启动,任务队列队列也不会丢失。

消息持久化

现在我们需要将消息标记为持久的——通过使用amqp.Publishing中的持久性选项amqp.Persistent。

err = ch.Publish(
	"",     // exchange
	q.Name, // routing key
	false,  // 立即
	false,  // 强制
	amqp.Publishing{
    
    
		DeliveryMode: amqp.Persistent, // 持久(交付模式:瞬态/持久)
		ContentType:  "text/plain",
		Body:         []byte(body),
	})

有关消息持久性的说明(没有高可靠性):

将消息标记为持久性并不能完全保证消息不会丢失。尽管它告诉RabbitMQ将消息保存到磁盘上,但是RabbitMQ接受了一条消息并且还没有保存它时,仍然有一个很短的时间窗口。而且,RabbitMQ并不是对每个消息都执行fsync(2)——它可能只是保存到缓存中,而不是真正写入磁盘。持久性保证不是很强,但是对于我们的简单任务队列来说已经足够了。如果您需要更强有力的担保,那么您可以使用publisher confirms

公平分发

你可能已经注意到调度仍然不能完全按照我们的要求工作。例如,在有两个consumer的情况下,当所有的奇数消息都是重消息而偶数消息都是轻消息时,一个consumer将持续忙碌,而另一个consumer几乎不做任何工作。嗯,RabbitMQ对此一无所知,仍然会均匀地发送消息。

这是因为RabbitMQ只是在消息进入队列时发送消息。它不考虑消费者未确认消息的数量。只是盲目地向消费者发送信息。
在这里插入图片描述
为了避免这种情况,我们可以将预取计数设置为1。这告诉RabbitMQ不要一次向一个consumer发出多个消息。或者,换句话说,在处理并确认前一条消息之前,不要向consumer发送新消息。相反,它将把它发送给下一个不忙的comsumer

在consumer端添加:

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

关于队列大小的说明

如果所有的worker都很忙,你的queue随时可能会满。你会想继续关注这一点,也许需要增加更多的worker,或者有一些其他的策略。

完整代码如下:

  1. 开启队列和消息持久化
  2. 切换到手动确认模式
  3. 开启公平分发模式

producer.go

package main

import (
	"log"
	"os"
	"strings"

	"github.com/streadway/amqp"
)

func main() {
    
    
	// 1. 尝试连接RabbitMQ,建立连接
	// 该连接抽象了套接字连接,并为我们处理协议版本协商和认证等。
	conn, err := amqp.Dial("amqp://root:[email protected]:5672/")
	if err != nil {
    
    
		log.Fatalf("%s", "Failed to connect to RabbitMQ", err)
		return
	}
	defer conn.Close()
	// 2. 接下来,我们创建一个通道,大多数API都是用过该通道操作的。
	ch, err := conn.Channel()
	if err != nil {
    
    
		log.Fatalf("%s", "Failed to open a channel", err)
		return
	}
	defer ch.Close()
	// 3. 声明消息要发送到的队列
	q, err := ch.QueueDeclare(
		"task_queue", // name
		true,         // 开启队列持久化
		false,        // delete when unused
		false,        // 独有的
		false,        // no-wait
		nil,          // arguments
	)
	if err != nil {
    
    
		log.Fatalf("%s", "Failed to declare a queue", err)
		return
	}
	body := bodyFrom(os.Args) // 从参数中获取要发送的消息正文
	// 4.将消息发布到声明的队列
	err = ch.Publish(
		"",     // exchange
		q.Name, // routing key
		false,  // 强制
		false,  // 立即
		amqp.Publishing{
    
    
			DeliveryMode: amqp.Persistent, // 开启消息持久化
			ContentType:  "text/plain",
			Body:         []byte(body),
		})
	if err != nil {
    
    
		log.Fatalf("%s", "Failed to publish a message", err)
		return
	}
	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
}

consumer.go

package main

import (
	"log"
	"time"

	"github.com/streadway/amqp"
)

func main() {
    
    
	// 建立连接
	conn, err := amqp.Dial("amqp://root:[email protected]:5672/")
	if err != nil {
    
    
		log.Fatalf("%s", "Failed to connect to RabbitMQ", err)
		return
	}
	defer conn.Close()
	// 获取channel
	ch, err := conn.Channel()
	if err != nil {
    
    
		log.Fatalf("%s", "Failed to open a channel", err)
		return
	}
	defer ch.Close()
	// 声明队列
	//请注意,我们也在这里声明队列。因为我们可能在发布者之前启动使用者,所以我们希望在尝试使用队列中的消息之前确保队列存在。
	q, err := ch.QueueDeclare(
		"task_queue", // name
		true,         // 开启队列持久化
		false,        // delete when unused
		false,        // exclusive
		false,        // no-wait
		nil,          // arguments
	)
	// 开启公平分发
	err = ch.Qos(
		1,     // prefetch count
		0,     // prefetch size
		false, // global
	)
	if err != nil {
    
    
		log.Fatalf("%s", "Failed to declare a queue", err)
		return
	}
	// 获取接收消息的Delivery通道
	msgs, err := ch.Consume(
		q.Name, // queue
		"",     // consumer
		false,  // auto-ack  //注意这里传false,关闭自动消息确认
		false,  // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	if err != nil {
    
    
		log.Fatalf("%s", "Failed to register a consumer", err)
		return
	}

	go func() {
    
    
		for d := range msgs {
    
    
			log.Printf("Received a message: %s", d.Body)
			time.Sleep(3 * time.Second) //模拟任务处理耗时
			log.Printf("Done")
			d.Ack(false) // 手动传递消息确认
		}
	}()

	log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
	select {
    
    }
}

猜你喜欢

转载自blog.csdn.net/csdniter/article/details/113384145