引言
在上一节【RabbitMQ(一)】docker-compose安装rabbitmq及简单使用Hello World中,我们介绍了Rabbitmq的安装和简单操作。达到了生产者发送一个Hello World之后,消费者便能接收到一个Hello World的效果。
- 如果有多个消费者同时在接收消息,消息会如何分发呢?
- 消费者在处理消息的时候宕机了,那这个消息怎么办呢?
- rabbitmq宕机了,其中的队列和消息怎么办?
- 两个消费者情况下,消息分发不合理,一个特忙,一个特闲怎么办?
带着这些问题,开始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
//使用这个命令可以打印出未被确认的消息
- 只有一个消费者时,忘记确认的消费者未退出前:
messages_unacknowledged的个数会随着producer发送消息个数增加,这些消息因为未得到确认,故而一直占用内存
- 忘记确认的消费者退出时
之前未被确认的消息转为ready状态,在有消费者连接时,会被重新发送,此时状态下依然占用内存
- 有消费者连接时。如果是忘记确认的消费者,则重复上面的步骤。连接上正常确认的消费者时,正常结果如下:
消息持久化
我们已经学会了如何确保即使消费者死亡,消息也不会丢失。但是如果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,或者有一些其他的策略。
完整代码如下:
- 开启队列和消息持久化
- 切换到手动确认模式
- 开启公平分发模式
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 {
}
}