RabbitMq系列-路由,主题模式

584 阅读4分钟

简介

前一篇总结中总结了 RabbitMq 的发布订阅模式以及交换机概念,并熟悉了名为fanout类型的交换机,但是有的业务场景下并不是需要将消息分发给所有的消费者,而是需要按照某些规则将消息分发到符合规则的消费者手中,RabbitMq 中的路由模式就比较适合这种场景,主题模式则是对前者的一种优化,其实前面所有的模式都可以由主题模式设置合适的主题规则来衍化。

路由模式

在前面总结的fanout类型时是这么绑定队列与交换机的

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

可以看到这里 只指定了队列的名称,在key字段传了空字符串,这是因为当交换机被声明为fanout的时候,绑定队列所声明的key无论传什么值都会被忽略,交换机会将消息分发到所有绑定在交换机上的队列。但是在主题模式中这个key就会作为筛选队列的一个很重要的参数了。主题模式下交换机的类型定义为direct

主题模式模型图

比如上图的绑定关系完成后,当生产者推送一条包含 black消息时,交换机会将此消息转发给C2,生产者推送除了 orangr,black,green之外的消息将会被交换机丢弃。

当多个消费者绑定同一个Route,如下图:

这种情况下 direct交换机在实际效果上又变成了fanout类型的交换机。在代码上只需要将交换机的类型声明为direct并在发送和绑定的时候指定routing key即可。

生产者

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 main() {
	route()
}

func route() {
	//开启连接
	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(
		"log_direct",
		"direct",
		true, //持久化,即使RabbitMq重启交换机也不会丢失
		false,
		false,
		false,
		nil)
	failOnError(err, "Failed to declare an exchange")

	level := getLevel(os.Args)

	body := getBody(os.Args)

	msg := amqp.Publishing{ContentType: "text/plain", Body: []byte(body)}
	ch.Publish("log_direct", level, false, false, msg)

	log.Printf(" [x] Sent %s", body)

}

func getLevel(args []string) string {
	var s string
	if (len(args) < 2) || args[1] == "" {
		s = "info"
	} else {
		s = args[1]
	}

	return s
}

func getBody(args []string) string {
	var s string
	if (len(args) < 3) || args[2] == "" {
		s = "hello"
	} else {
		s = strings.Join(args[2:], ",")
	}
	return s
}

这里生产者的代码和前面没什么大的区别,只是改变交换机的类型,以及在Publish时除了交换机的名字还指定了路由名称level

消费者

package main

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

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

func main() {
	route()
}

func route() {
	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(
		"log_direct",
		"direct",
		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")

	if len(os.Args) < 2 {
		log.Printf("Usage: %s [info] [warning] [error]", os.Args[0])
		return
	}

	for _, s := range os.Args[1:] {
		log.Printf("Binding queue %s to exchange %s with routing key %s",
			q.Name, "log_direct", s)
		err = ch.QueueBind(q.Name, s, "log_direct", 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
}

消费者也只需在指定交换机类型derict时指定路由key即可收到使用相同key生产者发送的消息。

结果

开启两个消费者进程,一个绑定keyinfo的消息另一个绑定keywarningerror的消息,使用生产者发送info级别的消息:

可以看到只有绑定了keyinfo的消费者收到了消息

使用生产者发送keywarningerror的消息,发现只有绑定了相应key的消费者成功收到了消息。

主题模式

主题模式是对路由模式的精简,生产者发送的key与消费者绑定的key无需完全一致而只需要消费者绑定的主题规则与生产者消息规则匹配则认为绑定成功。

图中描述的*#代表以下含义:

  1. *代表着一个单词
  2. #代表多个单词

例如 lazy.orange.dog这个消息会被分发给Q1,Q2因为满足 Q1 的 *.orange.* 和 Q2 的lazy.#规则。 在绑定队列指定 topic 时我们可以不使用任何的*,# 这样就变成了路由模式,我们也可以指定所有的 topic 都是#这样就变成了 fanout模式。