Golang学习笔记_RabbitMQ的原理架构和使用

RabbitMQ 简介

  1. 实现了高级消息队列协议(Advanced Message Queuing Protcol)AMQP
  2. 消息队列中间件的作用(Redis实现MQ里面有写过,这里简单带过)
    1. 解耦
    2. 削峰
    3. 异步处理
    4. 缓存
    5. 消息通信
    6. 提高扩展性

RabbitMQ 架构理解

channel binding channel channel channel Producer 生产者 Exchange交换机 Queue消息队列 Consumer消费者 Consumer消费者 Consumer消费者

  1. binding(绑定):交换机将消息路由给Queue所遵循的规则,可以定义一个路由键,用于交换机筛选特定的Queue
    1. Routing_Key(路由键):Producer 和 Consumer 协商一致的 key 策略。主要在交换机的 direct(直连)和 topic(主题) 模式下使用,fanout(广播)模式下不使用Routing_Key
  2. Exchange(交换机):主要功能是分发消息给特定的Queue,只负责转发,不具备存储消息的功能。Exchange有以下四种模式:
    1. direct(直连模式),根据携带的Routing_Key来筛选特定的Queue进行消息投递。是RabbitMQ的默认类型,可以不指定Routing_Key,在创建时会默认生成与Queue重名。
    2. hander(头模式),使用场景不多,消息路由涉及多个属性的时候,交换机使用多属性来代替Routing_key建立路由规则,还可以定义匹配单词的个数,例如any为有一个单词满足条件就匹配成功。all为所有单词都满足条件才匹配成功。
    3. fanout(广播模式),不看Routing_Key。只根据Exchange和Queue的binding情况来分发信息。所有与之binding的queue都将接收到同一条消息。
    4. topic(主题模式),相当于模糊查询。topic的routing_key是使用 . 来进行隔断的。有两种匹配方法:
      1. " * " 匹配一个单词,例子如下
      2. " # " 匹配0个~多个单词,例子如下
go 复制代码
rabbitMQ.* == rabbitMQ.topic != rabbitMQ.topic.topic
rabbitMQ.# == rabbit.topic == rabbit.topic.topic
  1. Queue(消息队列的存储数据结构):
    1. 存储方式:
      • 持久化,在Server本地硬盘存储一份
      • 临时队列,重启后丢失数据
      • 自动删除,不存在用户连接则删除queue
    2. 队列对ACK请求的不同情况
      • consumer 接收并 ack,queue 删除数据并向 consumer 发送新消息
      • consumer 接收但是未 ack 就断开了连接,queue 会认为消息并未传送成功,consumer 再次连接时会重新发送消息
      • 如果consumer 接收消息成功 ,但是忘记 ack 则 queue 不会重复发送消息
      • 如果 consumer 拒收消息,则 queue 会向另外满足条件的 consumer 继续发送这条消息

RabbitMQ 工作流程

Producer方向
  1. Producer 与 RabbitMQ Broker 建立连接,开启一个信道 channel
  2. 声明交换机并设置属性(交换机类型、持久化等)
  3. 声明Queue并设置属性(持久化,自动删除等)
  4. 通过Routing_key来binding交换机和Queue
  5. 发送信息给交换,交换机根据Routing_key来确认投递的queue
  6. 查找成功后将消息存到queue
  7. 查找失败将消息丢弃或抛回给生产者
  8. 关闭channel
Consumer方向
  1. 与 queue 建立连接,开启channel
  2. 向queue请求队列中的msg
  3. 等待queue回应,开始接收消息
  4. 消息处理完成后 返回回调确认ack
  5. queue 将确认的消息从队列中删除
  6. 关闭channel

RabbitMQ的两种部署方式

Meta Data : 元数据(描述数据的数据)

  • vhost meta data : 为Queue、Exchange、Binding提供命名空间级别的隔离
  • exchange meta data:记录路由的名称类型和属性
  • binding mate data:映射 routing_key和queue之间的绑定关系
  • queue mate data:表队列名称和属性
普通模式

对于该模式的两个节点,消息只会存在其中一个节点,另一个节点只保存mate data,当consumer 连接节点2访问节点1的数据信息时,消息会在两个节点中传递。

该模式下p和c应尽量连接每个节点,这样起到线性拓展的作用。

但存在一个问题,如果节点上还有未消费的消息,但是节点挂了。如果节点设置了持久化,则需要在节点重启的时候消息才会恢复。如果未设置持久化,则消息会丢失。

镜像模式

消息存在多个节点中,消息会在节点与节点之间同步,可实现高可用(当一个节点挂了,另一个节点可以接替其位置,继续工作)但会降低性能,因为大量消息进入和同步,会占用大量带宽,但是为了保证高可靠性需要取舍。

面试题

  • Q:如何保证消息不被重复消费?

    • A:MQ通过确认机制ACK,进行确认。确认后消息从queue中删除,保证消息不被重复消费的。如果因为网络原因ack没有成功发出,导致消息重新投递。可以使用全局唯一消息id来避免。
    1. 消息发送者发送消息时携带一个全局唯一的消息id
    2. 消费者监听到消息后,根据id在redis或者db中查询是否存在消费记录
    3. 如果没有消费就正常消费,消费完毕后,写入redis或者db
    4. 如果消息消费过则直接丢弃
  • Q:如何保证消息的消费顺序?

    • A:RabbitMQ中存在一个设置,叫独占队列。即在同一时间只有一个消费者会消费消息。从而制止了异步操作,保证消费顺序。或者一个Producer对一个Consumer
  • Q:如何保证数据一致性?

    • A:因为MQ的使用场景多为分布式系统,所以一般不追求强一致性。而保证最终一致性就可以。
    • 而保证数据最终一致性,可以采用消息补偿机制。即消息在消费者处理完之后调用生产者的API修改数据状态。如未调用API则判断为消息处理失败或出错。此时间隔一段时间后重新投递消息进行再次操作。
    • 消费者收到消息,处理完毕后,发送一条响应消息给生产者也是消息补偿机制,本意是确认消费者成功消费消息。ACK也是处理方法

RabbitMQ的使用(Golang使用amqp包)

代码部分参考 upup小亮的博客

代码只是简单的操作,主要是熟悉流程。对于如何创建Queue和绑定Exchange之类的操作有个了解。

Simple(简单收发模式,只有一个Queue)

Simple运行机制与WorkQueue相似,只是一个Consumer与多个Consumer的区别。多个Consumer之间存在竞争关系,所以工作队列是创建多个Consumer,多个竞争只有一个可以获取消息消费。消费成功后ack消息删除。

演示代码放到一起了:

WorkQueue 工作队列

生产者

go 复制代码
// simple and work queue
func main2() {
	// 连接到 rabbitMQ
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil {
		log.Fatalf("无法创建连接:%s", err)
		return
	}
	// 默认关闭
	defer conn.Close()

	// 创建通道Channel
	ch, err := conn.Channel()
	if err != nil {
		log.Fatalf("无法创建channel:%s", err)
		return
	}
	// 通道关闭
	defer ch.Close()

	// 创建存储队列
	queue, err := ch.QueueDeclare(
		"hello", // 队列名称
		false, // 持久化设置,可以为true根据需求选择
		false, // 自动删除,没有用户连接删除queue一般不选用
		false, //独占
		false, //等待服务器确认
		nil)   //参数
	if err != nil {
		fmt.Println(err)
		log.Fatalf("无法声明队列:%s", err)
		return
	}

	var body string
	// 发送信息
	for i := 0; i < 10; i++ {
		fmt.Println(i)
		body = "Hello RabbitMQ" + string(i)
		err = ch.Publish(
			"",
			queue.Name,
			false, // 必须发送到消息队列
			false, // 不等待服务器确认
			amqp.Publishing{
				ContentType: "text/plain",
				Body:        []byte(body),
			})
		if err != nil {
			log.Fatalf("消息生产失败:%s", err)
			continue
		}
	}
}

消费者

go 复制代码
	// create conn
	// 如果同时运行两个这样的consumer代码,就是工作队列。只有一个consumer就是simple
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil {
		log.Fatalf("无法创建连接:%s", err)
		return
	}
	defer conn.Close()

	// create channel
	ch, err := conn.Channel()
	if err != nil {
		log.Fatalf("无法创建channel:%s", err)
		return
	}
	defer ch.Close()
	// create queue
	queue, err := ch.QueueDeclare(
		"hello",
		false,
		false,
		false,
		false,
		nil)
	if err != nil {
		log.Fatalf("无法创建queue:%s", err)
		return
	}
	
	// 消费信息

	msgs, err := ch.Consume(
		queue.Name,
		"",
		true,
		false,
		false,
		false,
		nil)
	if err != nil {
		log.Fatalf("无法消费信息:%s", err)
		return
	}
	for msg := range msgs {
		log.Println(string(msg.Body))
	}
	return

pub/sub 发布订阅模式

发布订阅模式可以创建两个Queue,绑定到同一个Exchange中

生产者这边只需要跟交换机对接,而交换机类型为fanout

go 复制代码
func main() {
	// 连接到 rabbitMQ
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil {
		log.Fatalf("无法创建连接:%s", err)
	}
	// 默认关闭
	defer conn.Close()

	// 创建通道Channel
	ch, err := conn.Channel()
	if err != nil {
		log.Fatalf("无法创建channel:%s", err)
	}
	defer ch.Close()

	// create exchange
	ex := ch.ExchangeDeclare(
		"exchange1", // 交换机名称
		"fanout",    // 交换机类型
		true,        // 是否持久化
		false,       // 是否自动删除
		false,       // 是否内部使用
		false,       // 是否等待服务器响应
		nil,         // 其他属性
	)
	fmt.Println(ex)

	body := "Hello RabbitMQ for Pub/Sub"
	err = ch.Publish(
		"exchange1",
		"", // routing key 可以为空,因为fanout不看routing key
		false,
		false,
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(body),
		})
	if err != nil {
		log.Fatalf("err %s:", err)
	}
	log.Println(body)
}

消费者:创建交换机,类型为fanout,创建队列,绑定交换机(创建多个consumer绑定同一个queue和同一个交换机。这样发送一个消息,所有的consumer都能收到。== 发布订阅模型)

go 复制代码
	// Pub/Sub
	// Create conn
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil{
		log.Fatalf(err)
	}
	defer conn.Close()

	// channel create
	ch, err := conn.Channel()
	if err != nil{
		log.Fatalf(err)
	}
	defer ch.Close()

	// exchange create
	ex := ch.ExchangeDeclare(
		"exchange1",
		"fanout",
		true,
		false,
		false,
		false,
		nil)

	fmt.Println(ex)

	// queue create
	queue, err := ch.QueueDeclare(
		"hello",
		false,
		false,
		false,
		false,
		nil)
	if err != nil{
		log.Fatalf(err)
	}
	err = ch.QueueBind(
		queue.Name,
		"",
		"exchange1",
		false,
		nil)
	if err != nil{
		log.Fatalf(err)
	}

	msgs, err := ch.Consume(
		queue.Name,
		"",
		true,
		false,
		false,
		false,
		nil)
	if err != nil{
		log.Fatalf(err)
	}
	go func() {
		for d := range msgs {
			log.Printf("Received a message: %s", d.Body)
		}
	}()

	log.Printf("Waiting for messages. To exit press CTRL+C")
	<-make(chan struct{}) // 阻塞主goroutine
}

Routing 模式(对特定的队列投递消息)

生产者

go 复制代码
func main() {
	// 连接到 rabbitMQ
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil {
		log.Fatalf("无法创建连接:%s", err)
	}
	// 默认关闭
	defer conn.Close()

	// 创建通道Channel
	ch, err := conn.Channel()
	if err != nil {
		log.Fatalf("无法创建channel:%s", err)
	}
	defer ch.Close()

	// create exchange
	ex := ch.ExchangeDeclare(
		"exchange1", // 交换机名称
		"direct",    // 交换机类型
		true,        // 是否持久化
		false,       // 是否自动删除
		false,       // 是否内部使用
		false,       // 是否等待服务器响应
		nil,         // 其他属性
	)
	fmt.Println(ex)
	body := "Hello RabbitMQ for direct routing"
		// 发布消息到交换机,并指定路由键
	err = ch.Publish(
		"logs_direct", // 交换机名称
		"routing_key", // 路由键
		false,         // 是否等待服务器响应
		false,         // 是否立即将消息写入磁盘
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(body),
		},
	)
	if err != nil{
		log.Fatalf("无法创建send msg:%s", err)
	}
	log.Printf("Sent message: %s", message)

消费者

go 复制代码
func main() {
	// 连接到RabbitMQ服务器
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil{
		log.Fatalf("无法创建send msg:%s", err)
	}
	defer conn.Close()

	// 创建一个通道
	ch, err := conn.Channel()
	if err != nil{
		log.Fatalf("无法创建send msg:%s", err)
	}
	defer ch.Close()

	// 声明一个交换机
	err = ch.ExchangeDeclare(
		"logs_direct", // 交换机名称
		"direct",      // 交换机类型
		true,          // 是否持久化
		false,         // 是否自动删除
		false,         // 是否内部使用
		false,         // 是否等待服务器响应
		nil,           // 其他属性
	)
	if err != nil{
		log.Fatalf("无法创建send msg:%s", err)
	}

	// 声明一个临时队列
	q, err := ch.QueueDeclare(
		"",    // 队列名称,留空表示由RabbitMQ自动生成,因为定义了key所以队列名可以是随意的,毕竟是依靠key来进行匹配的
		false, // 是否持久化
		false, // 是否自动删除(当没有任何消费者连接时)
		true,  // 是否排他队列(仅限于当前连接)
		false, // 是否等待服务器响应
		nil,   // 其他属性
	)
	// 将队列绑定到交换机上,并指定要接收的路由键
	err = ch.QueueBind(
		q.Name,        // 队列名称
		"routing_key",      // 路由键
		"logs_direct", // 交换机名称
		false,         // 是否等待服务器响应
		nil,           // 其他属性
	)
	if err != nil{
		log.Fatalf("无法创建send msg:%s", err)
	}

	// 订阅消息
	msgs, err := ch.Consume(
		q.Name, // 队列名称
		"",     // 消费者标识符,留空表示由RabbitMQ自动生成
		true,   // 是否自动应答
		false,  // 是否独占模式(仅限于当前连接)
		false,  // 是否等待服务器响应
		false,  // 其他属性
		nil,    // 其他属性
	)
	failOnError(err, "Failed to register a consumer")

	// 接收消息的goroutine
	go func() {
		for d := range msgs {
			log.Printf("Received a message: %s", d.Body)
		}
	}()

	log.Printf("Waiting for messages. To exit press CTRL+C")
	<-make(chan struct{}) // 阻塞主goroutine

topic

go 复制代码
func main() {
	// 连接到RabbitMQ服务器
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil{
		log.Fatalf(err)
	}
	defer conn.Close()

	// 创建一个通道
	ch, err := conn.Channel()
	if err != nil{
		log.Fatalf(err)
	}
	defer ch.Close()

	// 声明一个交换机
	err = ch.ExchangeDeclare(
		"logs_topic", // 交换机名称
		"topic",      // 交换机类型
		true,         // 是否持久化
		false,        // 是否自动删除
		false,        // 是否内部使用
		false,        // 是否等待服务器响应
		nil,          // 其他属性
	)
	if err != nil{
		log.Fatalf(err)
	}

	// 定义要发送的消息的路由键和内容
	routingKey := "example.key.das"
	message := "Hello, RabbitMQ!"

	// 发布消息到交换机,并指定路由键
	err = ch.Publish(
		"logs_topic", // 交换机名称
		routingKey,   // 路由键
		false,        // 是否等待服务器响应
		false,        // 是否立即发送
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(message),
		},
	)
	if err != nil{
		log.Fatalf(err)
	}

	log.Printf("Sent message: %s", message)
}

消费者

go 复制代码
func main() {
	// 连接到RabbitMQ服务器
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if err != nil{
		log.Fatalf(err)
	}
	defer conn.Close()

	// 创建一个通道
	ch, err := conn.Channel()
	if err != nil{
		log.Fatalf(err)
	}
	defer ch.Close()

	// 声明一个交换机
	err = ch.ExchangeDeclare(
		"logs_topic", // 交换机名称
		"topic",      // 交换机类型
		true,         // 是否持久化
		false,        // 是否自动删除
		false,        // 是否内部使用
		false,        // 是否等待服务器响应
		nil,          // 其他属性
	)
	if err != nil{
		log.Fatalf(err)
	}

	// 声明一个临时队列
	q, err := ch.QueueDeclare(
		"",    // 队列名称,留空表示由RabbitMQ自动生成
		false, // 是否持久化
		false, // 是否自动删除(当没有任何消费者连接时)
		true,  // 是否排他队列(仅限于当前连接)
		false, // 是否等待服务器响应
		nil,   // 其他属性
	)
	if err != nil{
		log.Fatalf(err)
	}

	// 将队列绑定到交换机上,并指定要接收的路由键
	err = ch.QueueBind(
		q.Name,       // 队列名称
		"example.#",  // 路由键,可以使用通配符*匹配一个单词
		"logs_topic", // 交换机名称
		false,        // 是否等待服务器响应
		nil,          // 其他属性
	)
	if err != nil{
		log.Fatalf(err)
	}
	// 创建一个消费者通道
	msgs, err := ch.Consume(
		q.Name, // 队列名称
		"",     // 消费者标识符,留空表示由RabbitMQ自动生成
		true,   // 是否自动应答
		false,  // 是否排他消费者
		false,  // 是否阻塞
		false,  // 是否等待服务器响应
		nil,    // 其他属性
	)
	if err != nil{
		log.Fatalf(err)
	}
	// 接收和处理消息
	forever := make(chan bool)

	go func() {
		for d := range msgs {
			log.Printf("Received a message: %s", d.Body)
		}
	}()

	log.Printf("Waiting for messages...")
	// 阻塞
	<-forever
}
相关推荐
跃渊Yuey2 分钟前
【C++笔记】vector使用详解及模拟实现
c++·笔记
lcintj8 分钟前
【WPF】Prism库学习(一)
学习·wpf·prism
lcintj15 分钟前
【WPF】Prism学习(二)
学习·wpf·prism
绵绵细雨中的乡音25 分钟前
第21课-C++[set和map学习和使用]
c++·学习
GIS 数据栈39 分钟前
博客摘录「 pyqt 为新建子线程传参以及子线程返回数据到主线程」2023年12月7日
笔记·python·pyqt·多线程·多线程通信
yz-俞祥胜42 分钟前
杨中科 .Net Core 笔记 DI 依赖注入2
笔记·.netcore
朝九晚五ฺ1 小时前
【Linux探索学习】第十三弹——进程状态:深入理解操作系统进程状态与Linux操作系统中的进程状态
linux·运维·学习
程序员劝退师_1 小时前
Vue学习笔记
vue.js·笔记·学习
爱数学的程序猿1 小时前
联邦学习的未来:深入剖析FedAvg算法与数据不均衡的解决之道
深度学习·学习·机器学习
祭の1 小时前
IDEA旗舰版编辑器器快速⼊门(笔记)
java·笔记·intellij-idea