RabbitMq系列-工作队列

294 阅读5分钟

工作队列简介

工作队列是最简单的队列工作模式,一个生产者生产消息,一个或者多个消费者进行消费。

生产者代码(golang)

队列的配置可以暂时不用详细考虑后续会慢慢介绍

package main

import (
	"github.com/streadway/amqp"
	"log"
)

func main() {
	simpleQueue()
}

func simpleQueue() {
	//创建一个连接
	conn, err := amqp.Dial("amqp://test:123456@192.168.222.136:5672/test")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

	/**
	  声明一个通道 在通道的基础上我们能够声明我们发布消息的队列配置
	  队列的声明是幂等的 意味着假如声明的队列名不存在的话就会创建队列
	*/
	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")
	defer ch.Close()

	queue, err := ch.QueueDeclare(
		"hello",
		false,
		false,
		false,
		false,
		nil, )

	failOnError(err, "Failed to declare a queue")

	body := "Hello World"

	//通过之前创建的通道发布消息
	err = ch.Publish(
		"",
		queue.Name,
		false,
		false,
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(body),
		})

	log.Printf(" [x] Sent %s", body)
	failOnError(err, "Failed to publish a message")
}
func failOnError(err error, msg string) {
	if err != nil {
		log.Fatalf("%s: %s", msg, err)
	}
}

消费者代码

package main

import (
	"github.com/streadway/amqp"
	"log"
)

func main() {
	simpleRec()
}

func simpleRec() {
	conn, err := amqp.Dial("amqp://test:123456@192.168.222.136:5672/test")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

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

	q, err := ch.QueueDeclare(
		"hello", // name
		false,   // durable
		false,   // delete when usused
		false,   // exclusive
		false,   // no-wait
		nil,     // arguments
	)
	failOnError(err, "Failed to declare a queue")
	msgs, err := ch.Consume(q.Name, "", true, false, false, false, nil)
	failOnError(err, "Failed to register a consumer")

	//开启一个管道但是没有协程往其中生产数据
	forever := make(chan bool)

	go func() {
		for d := range msgs {
			log.Printf("Received a message: %s", d.Body)
		}
	}()

	log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
	//因为是一个无缓冲通道且没有协程往通道中写入数据 所以main函数会阻塞在这里
	//从而消费消息的协程不会因为main函数的结束而被回收
	<-forever
}

func failOnError(err error, msg string) {
	if err != nil {
		log.Fatalf("%s: %s", msg, err)
	}
}

演示

开启两个消费者

生成者依次发布消息观察消费者的接收情况

RabbitMq 的消息确认机制

如果我们注意观察生产者和两个消费者的消息时间的话会发现消息会依次平均的分配到两个消费者上,这种分配机制被称作罗宾轮询(Round-robin)

注意到我们在注册一个消费者的代码

msgs, err := ch.Consume(
		q.Name,
		"",
		true,
		false,
		false,
		false,
		nil)

其中第三个参数如果看方法定义的话

可以看到是 autoAck 当其为true时,消费者每拿到一个消息则自动告知队列该消息已成功消费(处理),这样队列就可以将消息删除了,而不用管消费者拿到消息后是否真正的处理完任务。这种情况在消费者出现故障时,比如程序异常退出之类的,消息拿到后并没有按照程序处理完,但是因为自动确认机制,队列服务器已经将此消息删除,此消息相当于丢失了。

要想避免此类情况的发生, RabbitMq 支持手动确认消息模式, 即消费者在拿到消息后不立刻自动确认消息,而是在处理完业务之后,手动发送确认信息,服务器在收到消费者的确认消息前会一直保留该消息(无论消费者处理这个消息花费了多久的时间),如果检测到消费者断开连接或者取消注册之后,服务器会将此消息重新推送给其他在线(或订阅)的消费者。

我们将autoAck设为false实践一下

修改消费者的代码:

msgs, err := ch.Consume(
		q.Name,
		"",
		false,
		false,
		false,
		false,
		nil)
	failOnError(err, "Failed to register a consumer")

	//开启一个管道但是没有协程往其中生产数据
	forever := make(chan bool)

	go func() {
		for d := range msgs {
			log.Printf("Received a message: %s", d.Body)
		}
	}()

	log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
	//因为是一个无缓冲通道且没有协程往通道中写入数据 所以main函数会阻塞在这里
	//从而消费消息的协程不会因为main函数的结束而被回收
	<-forever

可以看到我们的消费者已经收到了消息 但是由于我们没有进行手动确认,在RabbitMq的控制台我们可以看到消息仍未确认

我们将消费者进程停止掉,再去观察控制台发现消息重新变成了Ready状态

那么如何手动确认消息呢,可以看到在Consume的方法里,返回的主要参数是一个Delivery类型的通道

Delivery的定义中有Ack(multiple bool)方法,如下:

参数 multipletrue时,会将此通道内之前所有未确认的消息一起确认掉,通常用于批量确认的场景;为false的时候则只确认一条消息。所以修改一下之前消费者的代码:

go func() {
    for d := range msgs {
	log.Printf("Received a message: %s", d.Body)
	d.Ack(false)
    }
}()

启动消费者进程,发现成功消费了上次恢复为Ready的消息

此时去控制台观察该队列的状态发现所有消息已经都被成功消费并确认:

总结

RabbitMq以这种消费者确认的机制来保证消息的不丢失,这样就不必考虑每一个消费者处理消息的花费时间。

在我公司因为使用的是AWS的服务器,且没有什么特别耗时的消费者任务处理所以使用的是AWSSQS消息队列,其消息确认机制与RabbitMq的不同之处在于,每一个队列会为本队列的消息设置一个消息可见期,一个消息被消费者拿到并未删除之前对其他的消费者会处于不可见状态,当拿到消息的消费者在规定的时间内仍未自己手动删除消息的话,那么此消息就会对其他的消费者可见。详见文档

个人感觉AWS的这种机制限制了每个消费者的处理消息的时间,与之对比的话RabbitMq的消息确认机制则对消费者来说相对更加灵活一些。