发布确认
用于消息发布到broker之后,broker确认有没有收到,这样做可以防止rabbitMQ突然崩溃,消息还没有持久化,从而导致的消息丢失问题
package main
import (
"context"
"fmt"
"log"
"strconv"
"sync"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
func main() {
// This example acts as a bridge, shoveling all messages sent from the source
// exchange "log" to destination exchange "log".
// Confirming publishes can help from overproduction and ensure every message
// is delivered.
// Setup the source of the store and forward
source, err := amqp.Dial("amqp://guest:guest@192.168.31.110:5672/")
if err != nil {
log.Fatalf("connection.open source: %s", err)
}
defer source.Close()
ch, err := source.Channel()
if err != nil {
log.Fatalf("channel.open source: %s", err)
}
//信道开启发布确认模式
if err := ch.Confirm(false); err != nil {
log.Fatalf("confirm.select destination: %s", err)
}
q, err := ch.QueueDeclare("", true, true, false, false, nil)
if err != nil {
log.Fatalf("queue.declare source: %s", err)
}
//确认的消息将返回这个chan中以
confirms := ch.NotifyPublish(make(chan amqp.Confirmation, 1))
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
dict := sync.Map{}
go func() {
for confirm := range confirms {
//确认成功的话就删除map中保存的数据
if confirm.Ack{
dict.Delete(confirm.DeliveryTag)
}
}
}()
for i := 0; i < 1000; i++ {
dict.Store(ch.GetNextPublishSeqNo(), "message "+strconv.Itoa(i))
err = ch.PublishWithContext(
ctx,
"",
q.Name,
false,
false,
amqp.Publishing{
// Copy all the properties
ContentType: "text/plain",
Body: []byte("message " + strconv.Itoa(i)),
})
}
time.Sleep(1 * time.Second)
dict.Range(walk)
var forever chan struct{}
<-forever
}
func walk(key, value interface{}) bool {
fmt.Println("Key =", key, "Value =", value)
return true
}
死信队列
当消费者无法正常消费消息、消息发生异常时,为了保证数据不丢失,将异常的消息置为死信,放入死信队列。在死信队列中的消息,将启动单独的消费程序特殊处理。
成为死信消息的三种原因:
- 消息被拒(Reject /Nack) 且 requeue = false。
- 消息 TTL 过期。
- 队列满了,无法再添加。
producer.go
package main
import (
"context"
amqp "github.com/rabbitmq/amqp091-go"
"log"
"os"
"strings"
"time"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
// # ========== 1.创建连接 ==========
conn, err := amqp.Dial("amqp://guest:guest@192.168.31.110: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()
// # ========== 2.设置队列(队列、交换机、绑定) ==========
// 声明队列
_, err = ch.QueueDeclare("normal-queue", true, false, false, false, amqp.Table{
"x-message-ttl": 5000, // 消息过期时间,毫秒
"x-dead-letter-exchange": "dead-exchange", // 指定死信交换机
"x-dead-letter-routing-key": "dead-key", // 指定死信routing-key
"x-max-length": 6, //指定队列最大长度,如果超过,消息也会变成死信
})
failOnError(err, "创建normal队列失败")
// 声明交换机
err = ch.ExchangeDeclare("normal-exchange", amqp.ExchangeDirect, true, false, false, false, nil)
failOnError(err, "创建normal交换机失败")
// 队列绑定(将队列、routing-key、交换机三者绑定到一起)
err = ch.QueueBind("normal-queue", "normal-key", "normal-exchange", false, nil)
failOnError(err, "normal:队列、交换机、routing-key 绑定失败")
// # ========== 3.设置死信队列(队列、交换机、绑定) ==========
// 声明死信队列
// args 为 nil。切记不要给死信队列设置消息过期时间,否则失效的消息进入死信队列后会再次过期。
_, err = ch.QueueDeclare("dead-queue", true, false, false, false, nil)
failOnError(err, "创建dead队列失败")
// 声明交换机
err = ch.ExchangeDeclare("dead-exchange", amqp.ExchangeDirect, true, false, false, false, nil)
failOnError(err, "创建dead队列失败")
// 队列绑定(将队列、routing-key、交换机三者绑定到一起)
err = ch.QueueBind("dead-queue", "dead-key", "dead-exchange", false, nil)
failOnError(err, "dead:队列、交换机、routing-key 绑定失败")
// # ========== 4.发布消息 ==========
body := bodyFrom(os.Args)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 发布消息
err = ch.PublishWithContext(ctx, "normal-exchange", "normal-key", false, false, amqp.Publishing{
ContentType: "text/plain",
Body: []byte(body),
//Expiration: "5000", //也可以在消息里设置TTL,更加灵活
})
failOnError(err, "消息发布失败")
}
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
}
normalConsumer.go
此程序不允许,可以导致消息因为TTL和长度限制而成为死信,运行可以通过拒绝消息让消息成为死信
package main
import (
amqp "github.com/rabbitmq/amqp091-go"
"log"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
// # ========== 1.创建连接 ==========
conn, err := amqp.Dial("amqp://guest:guest@192.168.31.110: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()
// # ========== 2.消费消息 ==========
msgsCh, err := ch.Consume("normal-queue", "", false, false, false, false, nil)
failOnError(err, "消费normal队列失败")
forever := make(chan struct{})
go func() {
for d := range msgsCh {
// 要实现的逻辑
log.Printf("接收的消息: %s", d.Body)
// 手动应答
//d.Ack(false)
d.Reject(false)
}
}()
log.Printf("[*] Waiting for message, To exit press CTRL+C")
<-forever
}
deadConsumer.go
package main
import (
amqp "github.com/rabbitmq/amqp091-go"
"log"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
// # ========== 1.创建连接 ==========
conn, err := amqp.Dial("amqp://guest:guest@192.168.31.110: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()
// # ========== 2.消费消息 ==========
msgsCh, err := ch.Consume("dead-queue", "", false, false, false, false, nil)
failOnError(err, "消费normal队列失败")
forever := make(chan struct{})
go func() {
for d := range msgsCh {
// 要实现的逻辑
log.Printf("接收的消息: %s", d.Body)
// 手动应答
d.Ack(false)
//d.Reject(false)
}
}()
log.Printf("[*] Waiting for message, To exit press CTRL+C")
<-forever
}
延迟队列
通过死信队列的介绍,我们可以很容易的实现一个延迟队列。
- 将正常队列的消费者取消;
- 发消息时设置TTL;
通过上面两点,正常队列的消息始终不会被消费,而是等待消息TTL到期,进入死信队列,让死信消费者进行消费,从而达到延迟队列的效果。
上面看上去似乎没什么问题,实测一下就会发现消息不会“如期死亡”。
当先生产一个TTL为60s的消息,再生产一个TTL为5s的消息,第二个消息并不会再5s后过期进入死信队列,而是需要等到第一个消息TTL到期后,与第一个消息一同进入死信队列。这是因为RabbitMQ 只会判断队列中的第一个消息是否过期。
对于上文的问题,自然有解决方法,那就是通过 RabbitMQ 的 rabbitmq_delayed_message_exchange 插件来解决
docker版安装教程:
https://blog.csdn.net/m0_67393827/article/details/126643162
生产者实现的关键点:
1.在声明交换机时不在是direct类型,而是x-delayed-message类型,这是由插件提供的类型;
2.交换机要增加"x-delayed-type": "direct"参数设置;
3.发布消息时,要在 Headers 中设置x-delay参数,来控制消息从交换机过期时间;
4.只需要一个队列即可
producer.go
package main
import (
"context"
amqp "github.com/rabbitmq/amqp091-go"
"log"
"os"
"strings"
"time"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
// # ========== 1.创建连接 ==========
conn, err := amqp.Dial("amqp://guest:guest@192.168.31.110: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()
// # ========== 2.设置队列(队列、交换机、绑定) ==========
// 声明队列
_, err = ch.QueueDeclare("delay-queue", true, false, false, false, amqp.Table{})
failOnError(err, "创建normal队列失败")
// 声明交换机
err = ch.ExchangeDeclare("delay-exchange", "x-delayed-message", true, false, false, false, amqp.Table{
"x-delayed-type": "direct",
})
failOnError(err, "创建normal交换机失败")
// 队列绑定(将队列、routing-key、交换机三者绑定到一起)
err = ch.QueueBind("delay-queue", "delay-key", "delay-exchange", false, nil)
failOnError(err, "normal:队列、交换机、routing-key 绑定失败")
// # ========== 3.发布消息 ==========
body := bodyFrom(os.Args)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 发布消息
err = ch.PublishWithContext(ctx, "delay-exchange", "delay-key", false, false, amqp.Publishing{
ContentType: "text/plain",
Body: []byte(body),
//Expiration: "5000",
Headers: map[string]interface{}{
"x-delay": "5000", // 消息从交换机过期时间,毫秒(x-dead-message插件提供)
},
})
failOnError(err, "消息发布失败")
}
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 (
amqp "github.com/rabbitmq/amqp091-go"
"log"
)
func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}
func main() {
// # ========== 1.创建连接 ==========
conn, err := amqp.Dial("amqp://guest:guest@192.168.31.110: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()
// # ========== 2.消费消息 ==========
msgsCh, err := ch.Consume("delay-queue", "", false, false, false, false, nil)
failOnError(err, "消费normal队列失败")
forever := make(chan struct{})
go func() {
for d := range msgsCh {
// 要实现的逻辑
log.Printf("接收的消息: %s", d.Body)
// 手动应答
d.Ack(false)
//d.Reject(false)
}
}()
log.Printf("[*] Waiting for message, To exit press CTRL+C")
<-forever
}