Golang implements the delay queue of RabbitMQ

Posted by SargeZT on Tue, 08 Feb 2022 18:08:38 +0100

Before reading this article, you should have understood some concepts of RabbitMQ, such as queues, switches and so on.

Introduction to delay queue

Messages in a queue are consumed by consumers after being delayed for a period of time. Such a queue can be called delay queue.

The application scenario of delay queue is very wide, such as: if there is no payment within 30 minutes after placing an order, the order will be cancelled; Give a notice at a certain time, etc.

Delay queue through dead letter

adopt Golang implements the dead letter queue of RabbitMQ We can easily implement a delay queue.

  1. Cancel the normal queue of consumers;
  2. Set TTL when sending messages;

Through the above two points, the messages in the normal queue will not be consumed, but wait for the message TTL to expire and enter the dead letter queue, so that the dead letter consumers can consume, so as to achieve the effect of delaying the queue.

There seems to be nothing wrong with the above. If you measure it, you will find that the message will not "die as scheduled".

When a message with a TTL of 60s is produced and a message with a TTL of 5s is produced, the second message will not expire after 5s and enter the dead letter queue. Instead, it needs to wait until the TTL of the first message expires and enter the dead letter queue together with the first message. This is because RabbitMQ only determines whether the first message in the queue has expired.

Implement delay queue through plug-in

framework

There is a natural solution to the above problem, that is, through RabbitMQ of rabbitmq_delayed_message_exchange plug-in. This article does not cover the installation of RabbitMQ and plug-ins. You can refer to Installation of this article Or use Docker To install.

The principle of this plug-in is to temporarily store messages in mnesia (a distributed data system) table at the switch, delay delivery to the queue, and wait until the message expires before delivery to the queue.

With a simple understanding of the principle of plug-ins, we can design delay queues in this way.

realization

Key points of producer realization:

1. When declaring the switch, it is no longer the direct type, but the x-delayed-message type, which is provided by the plug-in;

2. The switch should add "x-delayed-type": "direct" parameter setting;

3. When publishing a message, set the x-delay parameter in Headers to control the expiration time of the message from the switch;

err = mqCh.Publish(constant.Exchange1, constant.RoutingKey1, false, false, amqp.Publishing{
    ContentType: "text/plain",
    Body:        []byte(message),
    //Expiration: "10000", / / message expiration time (message level), MS
    Headers: map[string]interface{}{
        "x-delay": "5000", // Message expiration time from switch, MS (provided by x-dead-message plug-in)
    },
})

Producer complete code:

// producter.go
package main

import (
    "fmt"
    "github.com/streadway/amqp"
    "learn_gin/go/rabbitmq/delayletter/constant"
    "learn_gin/go/rabbitmq/util"
    "strconv"
    "time"
)

func main() {
    // # ========== 1. Create connection==========
    mq := util.NewRabbitMQ()
    defer mq.Close()
    mqCh := mq.Channel

    // # ========== 2. Set queue (queue, switch, binding)==========
    // Declaration queue
    var err error
    _, err = mqCh.QueueDeclare(constant.Queue1, true, false, false, false, amqp.Table{
        // "x-message-ttl": 60000, / / message expiration time (queue level), MS
    })
    util.FailOnError(err, "Failed to create queue")

    // Declaration switch
    //err = mqCh.ExchangeDeclare(Exchange1, amqp.ExchangeDirect, true, false, false, false, nil)
    err = mqCh.ExchangeDeclare(constant.Exchange1, "x-delayed-message", true, false, false, false, amqp.Table{
        "x-delayed-type": "direct", 
    })
    util.FailOnError(err, "Failed to create switch")

    // Queue binding (binding queue, routing key and switch together)
    err = mqCh.QueueBind(constant.Queue1, constant.RoutingKey1, constant.Exchange1, false, nil)
    util.FailOnError(err, "Queues, switches routing-key Binding failed")

    // # ========== 4. Release news==========
    message := "msg" + strconv.Itoa(int(time.Now().Unix()))
    fmt.Println(message)
    // Release news
    err = mqCh.Publish(constant.Exchange1, constant.RoutingKey1, false, false, amqp.Publishing{
        ContentType: "text/plain",
        Body:        []byte(message),
        //Expiration: "10000", / / message expiration time (message level), MS
        Headers: map[string]interface{}{
            "x-delay": "5000", // Message expiration time from switch, MS (provided by x-dead-message plug-in)
        },
    })
    util.FailOnError(err, "Message publishing failed")
}

Since queues and switches are established on the producer side, consumers do not need special settings and attach codes directly.

Consumer complete code:

// consumer.go
package main

import (
    "learn_gin/go/rabbitmq/delayletter/constant"
    "learn_gin/go/rabbitmq/util"
    "log"
)

func main() {
    // # ========== 1. Create connection==========
    mq := util.NewRabbitMQ()
    defer mq.Close()
    mqCh := mq.Channel

    // # ========== 2. Consumption news==========
    msgsCh, err := mqCh.Consume(constant.Queue1, "", false, false, false, false, nil)
    util.FailOnError(err, "Consumption queue failed")

    forever := make(chan bool)
    go func() {
        for d := range msgsCh {
            // Logic to implement
            log.Printf("Received messages: %s", d.Body)

            // Manual response
            d.Ack(false)
            //d.Reject(true)
        }
    }()
    log.Printf("[*] Waiting for message, To exit press CTRL+C")
    <-forever
}

end!

Source code Mr-houzi/go-demo

Topics: PHP Go Back-end