读本文之前,你应该已经了解 RabbitMQ 的一些概念,如队列、交换机之类。
延迟队列简介
一个队列中的消息在延迟一段时间后才被消费者消费,这样的队列可以称之为延迟队列。
延迟队列的应用场景十分广泛,如:下单后30分钟内未付款则取消订单;在某个时间下发一条通知等。
通过死信实现延迟队列
通过Golang 实现 RabbitMQ 的死信队列的介绍,我们可以很容易的实现一个延迟队列。
- 将正常队列的消费者取消;
- 发消息时设置TTL;
通过上面两点,正常队列的消息始终不会被消费,而是等待消息TTL到期,进入死信队列,让死信消费者进行消费,从而达到延迟队列的效果。
上面看上去似乎没什么问题,实测一下就会发现消息不会“如期死亡”。
当先生产一个TTL为60s的消息,再生产一个TTL为5s的消息,第二个消息并不会再5s后过期进入死信队列,而是需要等到第一个消息TTL到期后,与第一个消息一同进入死信队列。这是因为RabbitMQ 只会判断队列中的第一个消息是否过期。
通过插件实现延迟队列
架构
对于上文的问题,自然有解决方法,那就是通过 RabbitMQ 的 rabbitmq_delayed_message_exchange 插件来解决。本文不赘述 RabbitMQ和插件的安装,你可以参考此文安装或使用Docker来安装。
此插件的原理是将消息在交换机处暂存储在mnesia(一个分布式数据系统)表中,延迟投递到队列中,等到消息到期再投递到队列当中。
简单了解了插件的原理,我们便可以如此设计延迟队列。
实现
生产者实现的关键点:
1.在声明交换机时不在是direct
类型,而是x-delayed-message
类型,这是由插件提供的类型;
2.交换机要增加"x-delayed-type": "direct"
参数设置;
3.发布消息时,要在 Headers 中设置x-delay
参数,来控制消息从交换机过期时间;
err = mqCh.Publish(constant.Exchange1, constant.RoutingKey1, false, false, amqp.Publishing{ | |
ContentType: "text/plain", | |
Body: []byte(message), | |
//Expiration: "10000", // 消息过期时间(消息级别),毫秒 | |
Headers: map[string]interface{}{ | |
"x-delay": "5000", // 消息从交换机过期时间,毫秒(x-dead-message插件提供) | |
}, | |
}) |
生产者完整代码:
// 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.创建连接 ========== | |
mq := util.NewRabbitMQ() | |
defer mq.Close() | |
mqCh := mq.Channel | |
// # ========== 2.设置队列(队列、交换机、绑定) ========== | |
// 声明队列 | |
var err error | |
_, err = mqCh.QueueDeclare(constant.Queue1, true, false, false, false, amqp.Table{ | |
// "x-message-ttl": 60000, // 消息过期时间(队列级别),毫秒 | |
}) | |
util.FailOnError(err, "创建队列失败") | |
// 声明交换机 | |
//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, "创建交换机失败") | |
// 队列绑定(将队列、routing-key、交换机三者绑定到一起) | |
err = mqCh.QueueBind(constant.Queue1, constant.RoutingKey1, constant.Exchange1, false, nil) | |
util.FailOnError(err, "队列、交换机、routing-key 绑定失败") | |
// # ========== 4.发布消息 ========== | |
message := "msg" + strconv.Itoa(int(time.Now().Unix())) | |
fmt.Println(message) | |
// 发布消息 | |
err = mqCh.Publish(constant.Exchange1, constant.RoutingKey1, false, false, amqp.Publishing{ | |
ContentType: "text/plain", | |
Body: []byte(message), | |
//Expiration: "10000", // 消息过期时间(消息级别),毫秒 | |
Headers: map[string]interface{}{ | |
"x-delay": "5000", // 消息从交换机过期时间,毫秒(x-dead-message插件提供) | |
}, | |
}) | |
util.FailOnError(err, "消息发布失败") | |
} |
由于在生产者端建立队列和交换机,所以消费者并不需要特殊的设置,直接附代码。
消费者完整代码:
// consumer.go | |
package main | |
import ( | |
"learn_gin/go/rabbitmq/delayletter/constant" | |
"learn_gin/go/rabbitmq/util" | |
"log" | |
) | |
func main() { | |
// # ========== 1.创建连接 ========== | |
mq := util.NewRabbitMQ() | |
defer mq.Close() | |
mqCh := mq.Channel | |
// # ========== 2.消费消息 ========== | |
msgsCh, err := mqCh.Consume(constant.Queue1, "", false, false, false, false, nil) | |
util.FailOnError(err, "消费队列失败") | |
forever := make(chan bool) | |
go func() { | |
for d := range msgsCh { | |
// 要实现的逻辑 | |
log.Printf("接收的消息: %s", d.Body) | |
// 手动应答 | |
d.Ack(false) | |
//d.Reject(true) | |
} | |
}() | |
log.Printf("[*] Waiting for message, To exit press CTRL+C") | |
<-forever | |
} |
end!