Golang-RabbitMQ-基础使用学习

RabbitMQ基础

RabbitMQ 用于消息代理,其工作就是接收和转发消息。

术语

生产:即发布消息,发送消息的程序就是一个生产者;Producing

队列:本质是一个巨大的消息缓冲区,大小只受主机内存和硬盘限制,用于存储传输而来的消息,多个生产者可以把消息发送给同一个队列,多个消费者可以从同一个队列获取消息;Queue

消费:即接收信息,接收消息的程序就是一个消费者;Consuming

基本关系:

(P) -> [|||] -> (C)

安装

go get github.com/streadway/amqp

简单操作

检查

创建辅助函数faliOnError用于检查每个 amqp调用的返回值

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

生产(发送消息)

生产者的创建

amqp.Dial()函数用于连接 RabbitMQ 服务器,链接嵌套字段用于定义链接的协议和身份验证

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

创建一个channel来传递消息

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 unused
  false,   // exclusive
  false,   // no-wait
  nil,     // arguments
)
failOnError(err, "Failed to declare a queue")

生产者选择交换机等进行生产(消息发送)

body := "Hello World!"
err = ch.Publish(
  "",     // exchange
  q.Name, // routing key
  false,  // mandatory
  false,  // immediate
  amqp.Publishing {
    ContentType: "text/plain",
    Body:        []byte(body),
  })
failOnError(err, "Failed to publish a message")

消费(接收消息)

消费者需要持续运行监听消息。

消费仍旧需要错误返回辅助函数failOnError

消费与生产相似,需要先打开一个连接和一个通道 channel,并声明我们要消费的队列(这里与发送的队列匹配)

// 连接 RabbitMQ
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close()
// 创建通道 Channel
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")

生产者会异步地向我们发送消息,在goroutine 中读取来自 channel 的消息。

msgs, err := ch.Consume(
  q.Name, // queue
  "",     // 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("Received a message: %s", d.Body)
  }
}()

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

工作队列

工作队列:也叫任务队列 Task Queues,为了避免等待一些占用大量资源时间的操作。

当我们把任务 Task 当做消息发送到队列中,运行后台的工作者 Worker 进程会取出任务处理,多个运行中的工作者可以共享任务队列里的任务。

示例中使用 time.sleep()函数模拟工作者处理任务的情况——字符串的一个.耗时一秒

示例

消费者

根据示例中的消费代码进行更改,它需要为消息体中的每一个.模拟 1s 的操作。

msgs, err := ch.Consume(
  q.Name, // queue
  "",     // 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("Received a message: %s", d.Body)
    // 获取消息体中的点的数量
    dot_count := bytes.Count(d.Body, []byte("."))
    // 模拟
    t := time.Duration(dot_count)
    time.Sleep(t * time.Second)
    log.Printf("Done")
  }
}()

log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever
生产者

根据示例中的生产代码进行更改,让其允许从命令行发送任意消息。

消息体定义bodyFrom

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
}

生产,发送消息

body := bodyFrom(os.Args)
err = ch.Publish(
  "",           // exchange
  q.Name,       // routing key
  false,        // mandatory
  false,
  amqp.Publishing {
    DeliveryMode: amqp.Persistent,
    ContentType:  "text/plain",
    Body:         []byte(body),
  })
failOnError(err, "Failed to publish a message")
log.Printf(" [x] Sent %s", body)

循环调度

工作队列能够并行处理队列,如果堆积了很多的任务,只需要添加更多的工作者就可以了。

我们同时运行两个消费者进行测试,然后发布多个任务

然后发现两个消费者会轮流对工作队列里的任务进行处理:

终端:productor

pillow@Mac-Pro rabbitMQ % go run new_task.go 
2023/08/14 17:43:45  [x] Sent hello.....................
pillow@Mac-Pro rabbitMQ % go run new_task.go First message.
2023/08/14 17:44:22  [x] Sent First message.
pillow@Mac-Pro rabbitMQ % go run new_task.go Second message..
go run new_task.go Third message...
go run new_task.go Fourth message....
go run new_task.go Fifth message.....
2023/08/14 17:44:36  [x] Sent Second message..
2023/08/14 17:44:36  [x] Sent Third message...
2023/08/14 17:44:37  [x] Sent Fourth message....
2023/08/14 17:44:37  [x] Sent Fifth message.....
pillow@Mac-Pro rabbitMQ % 

终端:consumer1

2023/08/14 17:43:45 Received a message: hello.....................
2023/08/14 17:44:06 Done
2023/08/14 17:44:36 Received a message: Second message..
2023/08/14 17:44:38 Done
2023/08/14 17:44:38 Received a message: Fourth message....
2023/08/14 17:44:42 Done

终端:consumer2

2023/08/14 17:44:22 Received a message: First message.
2023/08/14 17:44:23 Done
2023/08/14 17:44:36 Received a message: Third message...
2023/08/14 17:44:39 Done
2023/08/14 17:44:39 Received a message: Fifth message.....
2023/08/14 17:44:44 Done

消息确认

消息任务会在被发送给消费者后从内存中移除,此时若是把工作者停止,正在处理的消息任务就会消失

防止消息丢失,使用消息响应功能,消费者通过 ack 响应告诉 RabbitMQ 已经接收到并处理了某条消息,然后 RabbitMQ 就会释放并删除消息。

示例中使用手动消息确认,通过auto-ack 参数传递false,有消息完成时,d.Ack(falsez)想 RabbitMQ 发送消费完成的确认(单词传递)

msgs, err := ch.Consume(
  q.Name, // queue
  "",     // consumer
  false,  // 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("Received a message: %s", d.Body)
    dot_count := bytes.Count(d.Body, []byte("."))
    t := time.Duration(dot_count)
    time.Sleep(t * time.Second)
    log.Printf("Done")
    d.Ack(false)
  }
}()

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

注意:忘记确认会使消息任务在 RabbitMQ 缓存堆积,使其占用越来越多的内存

可以使用rabbitmqctl命令,输出messages_unacknowledged字段。

消息持久化

默认情况下,RabbitMQ 在退出或者崩溃时,将会丢失所有的队列和消息,我们可以将队列消息通过设置进行持久化存储,以避免消息丢失。

队列持久化

durable = true

q, err := ch.QueueDeclare(
  "task_queue",      // name
  true,         // durable
  false,        // delete when unused
  false,        // exclusive
  false,        // no-wait
  nil,          // arguments
)
failOnError(err, "Failed to declare a queue")
消息持久化

amqp.Publishing.DeliveryMode = amqp.Persistent

err = ch.Publish(
  "",           // exchange
  q.Name,       // routing key
  false,        // mandatory
  false,
  amqp.Publishing {
    DeliveryMode: amqp.Persistent,
    ContentType:  "text/plain",
    Body:         []byte(body),
})

此种方法使用 faync(2) 的消息会被存储到硬盘,其他可能会暂存在缓存中,强力的保证需要使用publisher confirms

公平调度

每个工作者处理的消息有轻重,我们可以告诉 RabbitMQ 一次只向一个工作者发送一条消息,并在确认前一个消息之前,不要向工作者发送新的消息。

err = ch.Qos(
  1,     // prefetch count
  0,     // prefetch size
  false, // global
)
failOnError(err, "Failed to set QoS")

发布订阅

将一个消息同时发给多个消费者,这种模式被称为发布/订阅,应用场景比如一个日志系统。

交换机

发布者的工作很简单,仅仅是将消息发出,但不会将任何消息直接发送给队列,也不知道消息是否被投递到队列。

发布者发布的消息被投递到交换机,交换机一遍接收消息,一边将其推送到队列或是忽略消息,对于消息的处理规则通过交换机定义。

rabbitmqctl:列出服务器上所有的交换器;

sudo rabbitmqctl list_exchanges:列出所有交换器包括匿名交换器(amq.*)

供选择的交换机类型:

  • direct:直连交换机
  • topic:主题交换机
  • headers:
  • fanout:将消息发送给它知道的所有队列

绑定

绑定用于描述交换机与队列之间的联系。

rabbitmqctl list_bindings:列出所有现存的绑定

routing key = ""为交换机配置绑定健,绑定建的意义取决于交换机的类型。

direct 直连交换机

直连交换机会对binding keyrouting key进行精确匹配,从而确定消息该分发到哪个队列。

img

消息推送到routing key = orange的交换机时,会被路由到队列 Q1 里,routing key = black || green会被路由到 Q2 里。

多个队列使用相同的binding key是合法的,当有与其相同的routing key的消息推送到交换机时,拥有相同的binding key的队列会同时被交换机匹配。

topic 主题交换机

direct 直连交换机无法基于多个标准执行路由操作,使用 topic主题交换机可以实现。

发送的 topic 主题交换机的消息携带的routing key可以总长度不超过 255 字节的多个字段,这些字段以.分隔开,像是user.order.cancel表示用户订单取消,runner.order.cancel表示骑手订单取消,user.errand.cancel表示用户服务取消。

接收信息的 topic 主题交换机的binding key也需要有相同的格式,但是是使用类似通配符匹配的规则来对消息进行匹配(或者叫筛选),规则为:

  • * (星号) 用来表示一个单词
  • # (井号) 用来表示任意数量(零个或多个)单词

对于每个字段的含义要提前思考清晰,例如上述的三个例子的字段含义为<table>.<table>.<cancel>第一个表对象对第二个表对象执行的操作。

根据上述规则创建三个绑定,像是:

binding key: *.order.*表示被执行操作为 order 表对象的消息被路由到此交换机指定的队列

binding key: runner.*.*表示执行操作为 runner 表对象的消息会被路由到此交换机指定的队列

若是有发送的消息的routing key字段数不正确,此消息不会被投递到任何一个队列,而是会丢失

??
  • 绑定键为 * 的队列会取到一个routing key为空的消息吗?
  • 绑定键为 #.* 的队列会获取到一个名为..的路由键的消息吗?它会取到一个routing key为单个单词的消息吗?
  • a.*.#a.#的区别在哪儿?

RPC远程过程调用

RPC(Remote Procedure Call)远程过程调用协议,一种通过网络从远程计算机上请求服务,而不需要了解底层网络技术的协议。

通过 RabbitMQ 实现 RPC 很容易——一个客户端发送请求消息,服务器端将其应用到一个回复信息中,为了接收到回复信息,客户端需要在发送请求的同时发送一个回调队列callback queue的地址:

q, err := ch.QueueDeclare(
  "",    // name
  false, // durable
  false, // delete when usused
  true,  // exclusive
  false, // noWait
  nil,   // arguments
)

err = ch.Publish(
  "",          // exchange
  "rpc_queue", // routing key
  false,       // mandatory
  false,       // immediate
  amqp.Publishing{
    ContentType:   "text/plain",
    CorrelationId: corrId,
    ReplyTo:       q.Name,
    Body:          []byte(strconv.Itoa(n)),
})

amqp协议给消息预定义了 14 个属性,常用的有:

  • persistent(持久性):将消息标记为持久性(值为true)或瞬态(false)。第二篇教程中有说明这个属性。
  • content_type(内容类型):用来描述编码的mime-type。例如在实际使用中常常使用application/json来描述JOSN编码类型。
  • reply_to(回复目标):通常用来命名回调队列。
  • correlation_id(关联标识):用来将RPC的响应和请求关联起来。

correlation_id用于分辨响应对应的请求,我们为每个请求设置唯一值,当我们从回调队列中接收到一个消息时,我们可以查看这条消息的correlation_id属性从而将响应与请求匹配起来,若是属性值是未知的,则直接销毁即可。

img

工作流程:

  • 客户端启动,创建匿名独享的回调队列
  • RPC 中,客户端发送带有两个属性的消息:reply_to指定回调队列,correlation_id设定唯一值用于匹配请求和响应。
  • RPC 中,服务端接受请求,执行请求并将结果消息发送到reply_to指定队列。
  • 客户端等待回调队列里的数据,通过correlation_id属性匹配请求并返回应用。

总结

RabbitMQ 的工作模型为:

enter image description here

配置

生产者和消费者都需要先创建连接 Conn 以及创建通道 Channel

创建连接 Conn
// 连接到 RabbitMQ 服务器
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
// 处理错误
	FailOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

MQURL 格式 amqp://账号:密码@rabbitmq服务器地址:端口号/vhost (默认是5672端口)

端口可在 /etc/rabbitmq/rabbitmq-env.conf 配置文件设置,也可以启动后通过netstat -tlnp查看

创建通道 Channel
// 配置一个 channel 传递消息
	ch, err := conn.Channel()
// 处理错误
	FailOnError(err, "Failed to open a channel")
	defer ch.Close()

生产者

声明队列 Queue

若是消费者未声明队列,消息被 Exchange 交换机接收后没有在消费者中匹配到 Queue 队列,消息会被丢弃;

若是生产者未声明队列,消费者是无法定于或者获取不存在的 MessageQueue 中的信息;

为避免以上两种可能的情况,最好两者一起声明,不必担心队列的重复声明,若是生产者尝试建立一个已经存在的消息队列,RabbitMQ 不会做任何事情,返回客户端建立成功。如果声明中的属性与已存在队列的属性有差异,那么一个错误代码为406的通道级异常就会被抛出。

// 发送前需声明一个队列供我们发送,然后才能够向队列发送消息
	q, err := ch.QueueDeclare(
		"", //name
		false,   //durable
		false,   //delete when unused
		false,   //exclusive
		false,   //no-wait
		nil,     //arguments
	)
// 处理错误
	FailOnError(err, "Failed to declare a queue")
  • name:队列名称,限制为 255 字节 utf-8 字符串
  • durable:是否持久化,RabbitMQ 关闭或者崩溃后持久化的队列仍旧在磁盘上
  • delete when unused:是否自动删除,只有在队列有消费者连接过且连接这个队列的消费者都断开时才会自动删除
  • exclusive:是否独占,仅对当前 Conn 连接可见,属于此 Conn 的队列可访问该队列
  • no-wait:是否阻塞
  • arguments:一些消息代理用他来完成类似与TTL的某些额外功能
声明交换机 Exchange
err = ch.ExchangeDeclare(
		"", // name
		"",      // type
		true,          // durable
		false,         // auto-deleted
		false,         // internal
		false,         // no-wait
		nil,           // arguments
	)
	failOnError(err, "Failed to declare an exchange")

交换机是用来发送消息的 AMQP 实体,交换机接收到消息后根据路由算法将其路由到队列,路由算法是由交换机类型和绑定规则决定的,AMQP 0-9-1 代理提供了四种交换机

  • direct:直连交换机
  • fanout:扇型交换机
  • topic:主题交换机
  • header:头交换机
直连交换机

直连交换机会根据信息携带的路由键routing key将信息投递给对应队列。

直连交换机会将一个队列绑定到某个交换机上,同时赋予该绑定一个路由键,当一个携带者路由键为R的信息被发送给直连交换机时,交换机会把它路由给绑定值同样为R的队列。

enter image description here

扇型交换机

扇形交换机会将信息路由给绑定到它身上的所有队列

当有信息发送给此扇型交换机时,交换机会将信息的拷贝分别发给这所有的 N 个队列。

enter image description here

主题交换机

主题交换机通过对信息路由健和队列到交换机的绑定模式之间的匹配,将信息路由给一个或者多个队列

发送的 topic 主题交换机的消息携带的routing key可以总长度不超过 255 字节的多个字段,这些字段以.分隔开,像是user.order.cancel表示用户订单取消,runner.order.cancel表示骑手订单取消,user.errand.cancel表示用户服务取消。

接收信息的 topic 主题交换机的binding key也需要有相同的格式,但是是使用类似通配符匹配的规则来对消息进行匹配(或者叫筛选),规则为:

  • * (星号) 用来表示一个单词
  • # (井号) 用来表示任意数量(零个或多个)单词
头交换机

头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。

当"x-match"设置为“any”时,消息头的任意一个值被匹配就可以满足条件,而当"x-match"设置为“all”的时候,就需要消息头的所有值都匹配成功。

头交换机可以视为直连交换机的另一种表现形式。头交换机能够像直连交换机一样工作,不同之处在于头交换机的路由规则是建立在头属性值之上,而不是路由键。路由键必须是一个字符串,而头属性值则没有这个约束,它们甚至可以是整数或者哈希值(字典)等。

创建 Binding

绑定用于描述交换机与队列之间的联系。

// 建立绑定,可建立多个绑定
err = ch.QueueBind(
  q.Name, // queue name
  "",     // routing key
  "logs", // exchange
  false, // no-wait
  nil, // arguments
)
failOnError(err, "Failed to bind a queue")
  • queue name:绑定的队列名称
  • routing key:用于消息路由分发
  • exchange:绑定的交换机名称
  • no-wait:是否阻塞
  • arguments:额外属性
发送消息
err = ch.Publish(
  "logs_topic",          // exchange
  severityFrom(os.Args), // routing key
  false,                 // mandatory
  false,                 // immediate
  amqp.Publishing{
    ContentType: "text/plain",
    Body:        []byte(body),
  })
failOnError(err, "Failed to publish a message")
  • exchange:交换机名称
  • routing key:用于信息路由发送
  • mandatory:是否返回信息(匹配队列)
  • immediate:是否返回信息(匹配消费者)
  • ContentType:信息内容类型
  • Body:信息内容

消费者

声明队列 Queue
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")

与生产者一样

  • name:队列名称,限制为 255 字节 utf-8 字符串
  • durable:是否持久化,RabbitMQ 关闭或者崩溃后持久化的队列仍旧在磁盘上
  • delete when unused:是否自动删除,只有在队列有消费者连接过且连接这个队列的消费者都断开时才会自动删除
  • exclusive:是否独占,仅对当前 Conn 连接可见,属于此 Conn 的队列可访问该队列
  • no-wait:是否阻塞
  • arguments:一些消息代理用他来完成类似与TTL的某些额外功能
取消息
msgs, err := ch.Consume(
  q.Name, // queue
  "",     // consumer
  true,   // auto-ack
  false,  // exclusive
  false,  // no-local
  false,  // no-wait
  nil,    // args
)
FailOnError(err, "Failed to register a consumer")
  • queue:队列名称
  • consumer:消费者名称,用来区分多个消费者,可用于实现公平分发等
  • auto-ack:是否自动应答
  • exclusive:是否独占,为 true 则队列中只能有一个消费者
  • no-local:是否为非本地,为 true 则仅接受其他 Conn 中发送的消息
  • no-wait:队列消费是否阻塞
  • args:额外属性
RabbitMQ中,可以使用优先级队列(Priority Queue)来为消息定义优先级。使用优先级队列可以确保高优先级的消息被先处理,从而提高系统的性能和可靠性。 在Golang中,使用RabbitMQ客户端库(如`github.com/streadway/amqp`)可以实现消费消费优先级队列中的消息。下面是一个使用Golang消费RabbitMQ优先级队列的例子: ```go package main import ( "fmt" "log" "github.com/streadway/amqp" ) func main() { conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/") if err != nil { log.Fatalf("failed to connect to RabbitMQ: %v", err) } defer conn.Close() ch, err := conn.Channel() if err != nil { log.Fatalf("failed to open a channel: %v", err) } defer ch.Close() q, err := ch.QueueDeclare( "priority_queue", // queue name true, // durable false, // delete when unused false, // exclusive false, // no-wait amqp.Table{ "x-max-priority": 10, // max priority level }, ) if err != nil { log.Fatalf("failed to declare a queue: %v", err) } msgs, err := ch.Consume( q.Name, // queue name "", // consumer name false, // auto-ack false, // exclusive false, // no-local false, // no-wait nil, // args ) if err != nil { log.Fatalf("failed to register a consumer: %v", err) } for msg := range msgs { log.Printf("received message with priority %d: %s", msg.Priority, string(msg.Body)) if err := msg.Ack(false); err != nil { log.Printf("failed to ack message: %v", err) } } } ``` 在上面的代码中,我们首先使用`QueueDeclare`方法创建一个名为`priority_queue`的优先级队列,其中`x-max-priority`参数指定了最大的优先级级别为10。 然后使用`Consume`方法订阅该队列中的消息,使用`msg.Priority`获取消息的优先级,然后处理消息逻辑,最后使用`msg.Ack`方法手动确认消息已经被消费。 注意,需要确保发送到该队列中的消息设置了正确的优先级。 以上就是使用Golang消费RabbitMQ优先级队列的示例。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值