RabbitMq系列-发布/订阅模式

939 阅读5分钟

RabbitMq 的工作模式

RabbitMq 的生产者并非直接和队列进行交互,而是将消息送至一个 Exchange (交换机),由交换机来决定最终将消息送至哪些队列,具体的推送规则在定义交换机的时候指定。消费者如果想要获取消息,只需要将自己声明的队列绑定到交换机上就可以收到生产者发送到交换机的消息。

但是在之前的工作队列篇中代码中并没有指定交换机的名字(第一个参数为空字符串):

err = ch.Publish(
		"",
		queue.Name,
		false,
		false,
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(body),
		})

当未指定交换机的名字时,会将消息推送到第二个参数 routing_key相同名字的队列中。

messages are routed to the queue with the name specified by routing_key parameter, if it exists.

发布订阅模式

在工作队列中,消息会通过轮询的方式送达到每个队列,但是每个消息只会被唯一送达至某一个队列,在某些场景比如说电商下单后,订单需要同步给库存系统以及邮件系统等等其他的业务模块。很明显,我们其他所有的系统都需要消费主站生产的包含订单的消息,如果继续使用前面的工作队列的消息轮询的方式,为了保证所有系统都收到所有的订单消息,我们好像只能维护多个队列,比如邮件队列,库存队列。每次主站生成订单时往每个队列里面都生产一次消息,且当有新的业务模块加进来时需要修改主站的代码,代码入侵性比较高,而且相当麻烦。

发布订阅模式能很好的解决这个场景下的问题,只要定一个交换机,主站产生订单时只需要将消息交给这个交换机,其他的业务系统如果需要的话只需要将自己定义队列绑定到交换机上即可。生产者生产的消息会根据交换机的类型被转发到绑定交换机的队列中去。(如果没有绑定的消费者 消息会被直接丢弃)。

Fanout 型交换机

描述

Fanout 类型的交换机会将他收到的消息发送给所有绑定在自己身上的队列。使用步骤总结起来有下面几步。

  1. 生产者和消费者定义好使用的交换机
  2. 生产者无需关心其他的任何消息,只需根据交换机的名字塞消息
  3. 消费者声明一个队列将队列绑定到交换机

生产者

package main

import (
	"github.com/streadway/amqp"
	"log"
	"os"
	"strings"
)

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

/**
从命令行取得内容
*/
func bodyFrom(args []string) string {
	var s string
	if (len(args) < 2) || args[1] == "" {
		s = "hello"
	} else {
		s = strings.Join(args[1:], " ")
	}
	return s
}

func main() {
	pubSub()
}

func pubSub() {
	//开启连接
	conn, err := amqp.Dial("amqp://guest:guest@localhost: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()

	//声明一个交换机
	err = ch.ExchangeDeclare(
		"logs", "fanout",
		true, //持久化,即使RabbitMq重启交换机也不会丢失
		false,
		false,
		false,
		nil)
	failOnError(err, "Failed to declare an exchange")
	//发送消息
	body := bodyFrom(os.Args)
	msg := amqp.Publishing{
		ContentType: "text/plain",
		Body:        []byte(body),
	}
	err = ch.Publish("logs",
		"", //Routing Key fanout 模式下该参数无论什么值都会被忽略掉
		false,
		false,
		msg)
	failOnError(err, "Failed to publish a message")
	log.Printf(" [x] Sent %s", body)
}

此时我们去执行

go run send.go hahahhahahahah

注意这个时候我们没有任何消费者在这个交换机上绑定队列,所以生产者将消息丢给交换机后交换机会把消息给丢掉

在RQ的控制台上也能直观的观察到(我自己生产了两次)

消费者

package main

import (
	"github.com/streadway/amqp"
	"log"
)

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

func main() {
	pubSub()
}

func pubSub() {
	conn, err := amqp.Dial("amqp://guest:guest@localhost: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()

	//声明交换机
	err = ch.ExchangeDeclare("logs",
		"fanout",
		true,
		false,
		false,
		false,
		nil)
	failOnError(err, "Failed to declare an exchange")

	q, err := ch.QueueDeclare("",
		false,
		false,
		true,
		false,
		nil)
	failOnError(err, "Failed to declare a queue")

	//队列绑定至交换机
	err = ch.QueueBind(q.Name, //队列名
		"",                    //Routing Key
		"logs",                //交换机名
		false,
		nil)
	failOnError(err, "Failed to bind a queue")

	//消费
	deliver, err := ch.Consume(q.Name,
		"",
		true,
		false,
		false,
		false,
		nil)
	failOnError(err, "Failed to register a consumer")

	forever := make(chan bool)

	go func() {
		for d := range deliver {
			log.Printf("[x] %s", d.Body)
		}
	}()

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

}

需要注意的是在定义队列时

q, err := ch.QueueDeclare("",
		false,
		false,
		true,
		false,
		nil)

我们并没有指定队列的名称。当没有指定队列名称的时候队列的名称会被 MQ 命名随机字符串。同时,第四个参数 exclusive 被定义为 true,意味着队列断开连接时会被删除。

启动两个消费者进程,我们在控制台上看到此时交换机上已经绑定了两个队列

生产发送一条消息可以看到所有的消费者都能够收到消息

我们停止消费者进程可以发现队列与交换机的绑定关系就会取消,同时队列会被删除。

小结

发布订阅模式是所有消息中间件都会使用一种模式,(貌似在 AWS SQS 中没发现🤔?️)可能名字会一些差异但意思都差不多。发布订阅模式让生产者无需 care 杂七杂八的消费者逻辑只需将消息扔给交换机就完事了,当然涉及到的有需要保障事务一致性的场景就需要另外考虑了。(个人感觉这篇文章写的不错)。

另外此篇的fanout只是一种模式,后续会有更多的模式的变种介绍。