Golang-RabbitMQ 延迟队列的两种实现方法【详解】

RabbitMQ延迟队列

通过查询相关资料,现在通过 RabbitMQ 实现延迟队列的方式有两种:

RabbitMQ死信实现

通过死信实现延迟队列,RabbitMQ 有死信队列,我们要先理解两个概念

概念

  • TTL:Time To Live 存活时间,当消息没有配置消费者,消息就一直停留在队列中,停留时间超过存活时间后,消息就会被自动移动到死信交换机
  • DLX:Dead Letter Exchanges 死信交换机,死信交换机会绑定在其他队列上,当这个队列的消息变成死信消息后,死信消息就会发送到死信交换机上

通过死信实现延迟队列的原理

原理

绑定了死信交换机的队列内的消息满足成为死信的条件,被推送到死信交换机上后,被路由到死信队列,然后由消费者从死信队列中消费

满足成为死信的条件

  • 消息被拒绝basic.reject/basic.nack&&requeue=false
  • 消息的过期时间到了TTL
  • 队列达到最大长度

利用死信实现延迟队列就要为需要延迟处理的消息设置 TTL 存活时间,且一直不进行消费,让其在经过一段时间过期后称为死信,经由死信交换机路由到死信队列后被消费者消费。

设置 TTL 存活时间的方式

  • 对消息本身设置 TTL 存活时间,每条消息的存活时间都可以不一样
  • 对绑定死信交换机的队列设置存活时间,每条传递到此队列的消息过期时间都相同

使用流程

image-20230818112405235

  1. 为生产者发送的消息配置 TTL,使其成为延迟消息
  2. 绑定普通队列到死信交换机且让消息仅通过 TTL 到期的形式成为死信发送到死信交换机,路由到死信队列
  3. 消费者消费死信队列中的延迟消息

实现

生产者

建立链接与通道——将消息发送到延迟队列

package main

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

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

func main() {
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

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

	body := bodyFrom(os.Args)
	// 将消息发送到延时队列上
	err = ch.Publish(
		"logs",           // exchange 这里为空则不选择 exchange
		"test_delay", // routing key
		false,        // mandatory
		false,        // immediate
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(body),
			Expiration:  "5000", // 设置五秒的过期时间
		})
	failOnError(err, "Failed to publish a message")

	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
}
消费者

建立连接通道——声明普通交换机——(声明普通队列)——声明延迟队列——绑定普通队列和交换机——监听延迟队列——进行消费

package main

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

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

func main() {
	// 建立链接
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

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

	// 声明一个主要使用的 exchange
	err = ch.ExchangeDeclare(
		"logs",   // name
		"fanout", // type
		true,     // durable
		false,    // auto-deleted
		false,    // internal
		false,    // no-wait
		nil,      // arguments
	)
	failOnError(err, "Failed to declare an exchange")

	// 声明一个常规的队列, 其实这个也没必要声明,因为 exchange 会默认绑定一个队列
	queue, err := ch.QueueDeclare(
		"test_logs", // name
		false,       // durable
		false,       // delete when unused
		true,        // exclusive
		false,       // no-wait
		nil,         // arguments
	)
	failOnError(err, "Failed to declare a queue")

	// 声明一个延时队列, ß我们的延时消息就是要发送到这里
	_, errDelay := ch.QueueDeclare(
		"test_delay", // name
		false,        // durable
		false,        // delete when unused
		true,         // exclusive
		false,        // no-wait
		amqp.Table{
			// 当消息过期时把消息发送到 logs 这个 exchange
			"x-dead-letter-exchange": "logs",
		}, // arguments
	)
	failOnError(errDelay, "Failed to declare a delay_queue")

	err = ch.QueueBind(
		queue.Name, // queue name, 这里指的是 test_logs
		"",         // routing key
		"logs",     // exchange
		false,
		nil)
	failOnError(err, "Failed to bind a queue")

	// 这里监听的是 test_logs
	msgs, err := ch.Consume(
		queue.Name, // queue name, 这里指的是 test_logs
		"",         // consumer
		true,       // auto-ack
		false,      // exclusive
		false,      // no-local
		false,      // no-wait
		nil,        // args
	)
	failOnError(err, "Failed to register a consumer")

	forever := make(chan bool)

	go func() {
		for d := range msgs {
			log.Printf(" [x] %s", d.Body)
		}
	}()

	log.Printf(" [*] Waiting for logs. To exit press CTRL+C")
	<-forever
}

生产者将消息设定好 TTL 发送到延迟队列前,会先由主交换机发送到默认的普通队列里存活设定好的时间,时间到后由死信交换机路由到死信队列,消费者只需对死信队列进行监听,取出消息消费即可。

缺陷

普通队列里的消息是有顺序的,后入的消息必须等待先入的消息出队才可以,即使后入消息存活时间要比先入的消息长……

例如:

terminal-consumer

pillow@Mac-Pro delayed3 % go run consumer.go 
2023/08/18 16:36:47  [*] Waiting for logs. To exit press CTRL+C

terminal-15000

pillow@Mac-Pro delayed3 % go run productor.go "这是一条存活时间为 15s 的消息"
2023/08/18 16:39:00  [x] Sent 这是一条存活时间为 15s 的信息

terminal-5000

pillow@Mac-Pro delayed3 % go run productor.go "这是一条存活时间为 5s 的消息"
2023/08/18 16:39:46  [x] Sent 这是一条存活时间为 5s 的信息

我们在运行消费者之后,先发送一个存活时间为 15s 的消息,随后立刻发送一个存活时间为 5s 的消息,按照功能来讲,5s 消息应该先成为死信然后被消费,随后是 15s 消息,但实际是 15s 后,15s 消息和 5s 消息几乎同时一前一后同时被消费。

RabbitMQ 队列内部维护的数据结构为队列,先入先出,后入后出,实际上按照我们的功能来讲应当维护一个以消息 TTL 大小为依据的小顶堆或是功能类似的一个更好的数据结构。

rabbitmq_delayed_message_exchange 插件实现

插件安装

插件下载地址:rabbitmq-delayed-message-exchange

下载时要提前查看自己的 RabbitMQ 的版本,下载对应版本的插件,否则可能会报错

image-20230819145904154

将下载好的的插件包放在 RabbitMQ 安装主目录的 plugins 文件夹下。

然后执行rabbitmq-plugins enable rabbitmq_delayed_message_exchange

然后进入RabbitMQ:managementAdd a New exchange选择type时能够看到x-delayed-message交换机代表插件安装成功。

image-20230819151856151

Docker 安装的 RabbitMQ 也是在容器根目录找到plugins文件夹将插件放进去执行启动插件命令即可。

image-20230819152026513

插件使用

生产者的实现很简单,只需要在消息的header中加入"x-delay"字段并用其值表示消息的 TTL,最后将其发送到延迟队列即可。

生产者
package main

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

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

func main() {
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

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

	var (
		exchange   = "x-delayed-message"
		queue      = "delay_queue"
		routingKey = "log_delay"
		body       string
	)
	// 申请交换机
	err = ch.ExchangeDeclare(exchange, exchange, true, false, false, false, amqp.Table{
		"x-delayed-type": "direct",
	})
	if err != nil {
		failOnError(err, "交换机申请失败!")
		return
	}
	if err = ch.QueueBind(queue, routingKey, exchange, false, nil); err != nil {
		failOnError(err, "绑定交换机失败!")
		return
	}

	body = "==========10000=================" + time.Now().Local().Format("2006-01-02 15:04:05")
	// 将消息发送到延时队列上
	err = ch.Publish(
		exchange,   // exchange 这里为空则不选择 exchange
		routingKey, // routing key
		false,      // mandatory
		false,      // immediate
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(body),
			Headers: map[string]interface{}{
				"x-delay": "10000", // 消息从交换机过期时间,毫秒(x-dead-message插件提供)
			},
		})
	failOnError(err, "Failed to publish a message")
	log.Printf(" [x] Sent %s", body)

	body = "==========20000=================" + time.Now().Local().Format("2006-01-02 15:04:05")
	// 将消息发送到延时队列上
	err = ch.Publish(
		exchange,   // exchange 这里为空则不选择 exchange
		routingKey, // routing key
		false,      // mandatory
		false,      // immediate
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(body),
			Headers: map[string]interface{}{
				"x-delay": "20000", // 消息从交换机过期时间,毫秒(x-dead-message插件提供)
			},
		})
	failOnError(err, "Failed to publish a message")
	log.Printf(" [x] Sent %s", body)

	body = "==========5000=================" + time.Now().Local().Format("2006-01-02 15:04:05")
	// 将消息发送到延时队列上
	err = ch.Publish(
		exchange,   // exchange 这里为空则不选择 exchange
		routingKey, // routing key
		false,      // mandatory
		false,      // immediate
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(body),
			Headers: map[string]interface{}{
				"x-delay": "5000", // 消息从交换机过期时间,毫秒(x-dead-message插件提供)
			},
		})
	failOnError(err, "Failed to publish a message")
	log.Printf(" [x] Sent %s", body)
}

消费者

对应生产者,消费者应该声明正确的交换机类型和对应的队列即可

package main

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

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

func main() {
	// 建立链接
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

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

	var (
		exchange   = "x-delayed-message"
		queue      = "delay_queue"
		routingKey = "log_delay"
	)

	// 申请交换机
	err = ch.ExchangeDeclare(
		exchange, // name
		exchange, // type
		true,     // durable
		false,    // auto-deleted
		false,    // internal
		false,    // no-wait
		amqp.Table{ // arguments
			"x-delayed-type": "direct",
		})
	if err != nil {
		failOnError(err, "交换机申请失败!")
		return
	}

	// 声明一个常规的队列, 其实这个也没必要声明,因为 exchange 会默认绑定一个队列
	q, err := ch.QueueDeclare(
		queue, // name
		true,  // durable
		true,  // delete when unused
		false, // exclusive
		false, // no-wait
		nil,   // arguments
	)
	failOnError(err, "Failed to declare a queue")

	err = ch.QueueBind(
		q.Name,     // queue name
		routingKey, // routing key
		exchange,   // exchange
		false,
		nil)
	failOnError(err, "Failed to bind a queue")

	// 这里监听的是 test_logs
	msgs, err := ch.Consume(
		q.Name, // queue name
		"",     // consumer
		true,   // auto-ack
		false,  // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	failOnError(err, "Failed to register a consumer")

	forever := make(chan bool)

	go func() {
		for d := range msgs {
			log.Printf("接收数据 [x] %s", d.Body)
		}
	}()

	log.Printf(" [*] Waiting for logs. To exit press CTRL+C")
	<-forever
}

测试

producer

pillow@Mac-Pro delayed4 % go run producer.go 
2023/08/19 11:14:27  [x] Sent ==========10000=================2023-08-19 11:14:27
2023/08/19 11:14:27  [x] Sent ==========20000=================2023-08-19 11:14:27
2023/08/19 11:14:27  [x] Sent ==========5000=================2023-08-19 11:14:27

consumer

2023/08/19 11:14:32 接收数据 [x] ==========5000=================2023-08-19 11:14:27
2023/08/19 11:14:37 接收数据 [x] ==========10000=================2023-08-19 11:14:27
2023/08/19 11:14:47 接收数据 [x] ==========20000=================2023-08-19 11:14:27

引用链接

RabbitMQ-Community-Plugins

rabbitmq-delayed-message-exchange

golang 使用 rabbitmq 延迟队列

Golang 实现 RabbitMQ 的延迟队列

[golang rabbitmq实现的延时队列_golang rabbitmq 延迟队列_流年诠释一切的博客-CSDN博客](

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值