RabbitMQ

1、AMQP

1.1 简介

MQ全称为Message Queue,是一种分布式应用程序的通信方法,它是消费者-生产者模型的一个典型的代表,producer不断的往消费队列中写入消息,而另一端consumer则可以读取或者订阅队列中的消息。RabbitMQ是MQ产品的典型代表,是一款基于AMQP协议可复用的企业消息系统。

AMQP(Advanced Message Queuing Protocol,高级消息队列协议),是应用层协议的一个开放标准,为面向消息的中间件设计。

1.2 核心概念

AMQP的框架图如下图所示:

  1. Broker:接收和分发消息的应用,RabbitMQ Server就是Message Broker;
  2. Virtual Host: 处于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMQ Server提供服务时,可以划分出多个vhost,每个用户可以在自己的vhost创建exchange/queue等。
  3. Connection:publisher/consumer和broker之间的TCP连接,断开连接的操作只会在client端进行,Broker不会断开连接,除非出现网络故障或broker服务出现问题。
  4. Channel:如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销将会是巨大的,效率也较低。Channel是在Connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id,帮助客户端和message broker识别channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP Connection的开销。
  5. Exchange:message到达broker 的第一站,根据分发规则匹配查询表中的routing key,分发消息到queue中。常用的类型有:direct(point-to-point)、topic(publish-publish) 和fanout(multicast)。
  6. Queue:消息最终被送到这里等到consumer取走,一个message可以同时被拷贝到多个queue中。
  7. Binding:exchange和queue之间的虚拟连接,binding中可以包含routing key(路由关键字)。binding信息被保存到exchange中的查询表中,用户message的分发依据。当exchange收到message时会解析其Header得到routing key,exchange根据routing key与exchange type将message路由到message queue。Binding key由consumer在绑定交换机与队列时指定,指定当前Exchange下什么样的routing key会被下派到当前绑定的queue中而routing key由producer发送message时指定,指定当前发送的消息被谁接收,两者的匹配方式由exchange type决定。

1.3 AMQP协议栈

AMQP的协议栈如下图所示:

  1. Module Layer:位于协议最高层,主要定义了一些提供客户端调用的命令,客户端可以利用这些命令实现自己的业务逻辑,例如客户端可以通过queue.declare声明一个队列,利用consume命令获取一个队列中的消息。
  2. Session Layer:主要负责将客户端的命令发送给服务器,再将服务器端的应答返回给客户端,主要为客户端与服务器之间通信提供可靠性、同步机制和错误处理。
  3. Transport layer:主要传输二进制数据流,提供帧的处理、信道复用、错误检测和数据表示。

2、RabbitMQ应用场景及基本原理

RabbitMQ是一个由erlang开发的AMQP的开源实现。主要应用场景有三个:

2.1 异步处理

场景说明:用户注册后,需要发送邮件和注册短信,传统的做法有两种:

1、串行方式:如下图所示,将注册信息写入数据库成功后,发送邮件,再发送注册短信,以上三个任务全部完成后,返回给客户端。这有一个问题就是邮件、短信不是必须的,它只是一个通知,而这种做法会让客户端等待没有必要等待的东西。

2、并行方式:将注册信息写入数据库之后,发送邮件的同时发送注册短信。以上三个任务完成后,返回给客户端。与串行相比,发邮件和发短信是同时进行,固而可以提高处理时间。

3、引入消息队列,将所有不是必须的业务逻辑进行异步处理,改造后的架构为:

我们可以看到,引入消息队列后,用户的响应时间久等于写入数据库的时间+写入消息队列的时间(可以忽略不计),引入消息队列处理后,响应时间大大减少。

2.2 应用解耦

场景说明:用户下单后,订单系统需要通知库存系统。传统的做法是用户下单后,订单系统直接去访问库存系统的接口,这样订单系统和库存系统高度耦合,假如库存系统无法访问,则订单减库存将失败,从而导致订单失败。(这样会导致少挣很多钱呢!!!!)

引入消息队列后的架构如下图所示:

对于订单系统:用户下单后,订单系统完成持久化处理,将订单消息写入消息队列,返回用户下单成功;对于库存系统:订阅下单的消息,采用推/拉的方式获取用户下单消息,库存系统根据下单消息进行库存的操作。假如在下单的时候库存系统不能使用,也不会影响正常下单,因为订单下单后,订单系统写入消息队列就不再关系其他的后续操作了,实现订单系统与库存系统的应用解耦。(为了保证库存肯定有,可以将队列大小设置成库存数量,或者采用其他方式解决)

2.3 流量削峰

流量削峰也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛。

应用场景:A系统其他时间每秒请求量就100个,系统可以稳定运行。每晚八点有秒杀活动,每秒并发请求量增至10000条,但是系统最大的处理能力只能每秒处理1000个请求,于是系统崩溃,服务器宕机。

对于之前的架构:大量用户(100万用户)通过浏览器在晚上八点高峰期同时参与秒杀活动,大量的请求涌入系统A中,高峰期达到每秒5000个请求,大量的请求打到MySQL上,每秒钟执行3000条SQL。但是一般的MySQL每秒钟抗住2000个请求就不错了,如果达到3000个请求的话可能MySQL直接就崩溃了,从而系统无法被使用。但是高峰期过后,可能也就10000用户访问系统,每秒的请求量也就50个左右,整个系统几乎没有任何压力。

引入MQ:100万用户在高峰期的时候,每秒请求5000个左右,将这5000请求写入MQ里面,系统A每秒最多只能处理2000请求(受限于MySQL的处理能力)。系统A从MQ中慢慢拉取请求,每秒就拉取2000个请求,不要超过自己每秒处理的请求数量上限即可。MQ中每秒进来5000个请求,结果只有2000个请求出去,所以在秒杀的时候可能会有几十万或者几百万的请求积压在MQ中。这个短暂的高峰期积压是没问题的,只要高峰期一过,系统就很快会将积压的消息消费掉。

2.4 优缺点

优点就是能够很好的解决上述几个问题;

缺点:

  1. 系统的可用性降低:系统引入的外部依赖越多,系统就越容易挂掉,本来只是A系统调用BCD三个接口就好,ABCD四个系统不报错整个系统就会正常运行,引入MQ之后,虽然ABCE没有报错,但是MQ挂了之后,整个系统也会崩溃;
  2. 系统的复杂性提高:引入MQ之后,需要考虑的问题也变多了,如果保证消息没有重复消费?如何保证消息不会丢失?怎么保证消息传递的顺序?
  3. 一致性问题:A系统发送完消息直接返回成功,但是BCD系统之中若有系统写库失败,则会产生数据不一致的问题。

参考链接

3、RabbitMQ的工作模式

RabbitMQ是一个消息队列,它的工作就是接收和转发消息。就好比是一个邮局:你可以把信件放入邮箱,邮递员就会把信件投递到你的收件人处。在这个比喻中,RabbitMQ就扮演着邮箱、邮局以及快递员的角色。RabbitMQ和邮局的主要区别就是,邮局处理纸张,RabbitMQ接收、存储和发送消息这种二进制数据。

RabbitMQ主要有六种工作模式:简单模式、工作模式、订阅模式、路由模式、话题模式和RPC远程调用。

首先我们定义一个结构体和创建RabbitMQ不同模式的实例,几种不同的模式是通过queueName、Exchange、Key的不同组合实现的。需要导入的包是:github.com/streadway/amqp

// URL格式:amqp://账号:密码@rabbitmq的服务器地址:端口号/虚拟主机
const MQURL = "amqp://guest:guest@10.10.27.41:5672/imooc_host"
type RabbitMQ struct {
	conn    *amqp.Connection
	channel *amqp.Channel
	// 队列名称
	QueueName string
	// 交换机
	Exchange string
	// Key
	Key string
	// 连接的URL
	MQUrl string
}

// 创建RabbitMQ结构体实例,几种不同的模式通过queueName、exchange、key不同组合进行实现的
func NewRabbitMQ(queueName string, exchange string, key string) *RabbitMQ {
	rabbitMQ := &RabbitMQ{
		QueueName: queueName,
		Exchange:  exchange,
		Key:       key,
		MQUrl:     MQURL,
	}

	var err error
	// 创建RabbitMQ连接
	rabbitMQ.conn, err = amqp.Dial(rabbitMQ.MQUrl)
	if err != nil {
		rabbitMQ.FailOnErr(err, "连接出现错误")
	}
	// 在连接中创建channel
	rabbitMQ.channel, err = rabbitMQ.conn.Channel()
	if err != nil {
		rabbitMQ.FailOnErr(err, "获取通道出现错误")
	}
	return rabbitMQ
}

3.1 简单模式

参考链接

简单模式只有一个消费者和一个生产者,我们只需要定义要发送队列的名称即可,一条消息只能被一个消费者使用。P代表生产者,C代表消费者。

// exchange为空表示使用匿名交换机,只需要指定队列名称即可
func NewRabbitMQSimple(queueName string) *RabbitMQ {
	return NewRabbitMQ(queueName, "", "")
}

// 简单模式下生产代码
func (r *RabbitMQ) PublishSimple(message string) {
	// 1、声明一个队列来保存消息并传递给使用者,如果队列不存在
	// 将创建一个并返回队列;保证队列存在,消息能发送到队列中
	_, err := r.channel.QueueDeclare(
		// name,
        r.QueueName,
		// durable,设置队列是否持久化,false表示服务器重启就没了
		false, 
		// delete when unused,当最后一个消费者断开连接以后,是否自动删除
		false,
		// exclusive, 是否具有排他性,(设置访问的对象)
		false,
		// no-wait, 是否阻塞等待服务器的响应
		false,
		// 额外属性
		nil,
	)
	if err != nil {
		r.FailOnErr(err, "Failed to declare a queue.")
	}

	// 对于消费发布者而言,它只负责把消息发布出去,甚至不知道消息是发到哪个queue中,消息通过exchange到达queue,
	// exchange的职责就是一边接收发布者的消息,一边把这些消息推到queue中。
	// 而exchange是通过绑定queue与exchange的routing key(publish中的第二个参数),通过代码进行绑定并制定routing key
	// 2、发送消息到队列中
	err = r.channel.Publish(
        // exchange 此时是空字符串,代表使用匿名交换机
		r.Exchange,  
        // routing key,简单和工作模式下传入队列的名称
		r.QueueName,
		// mandatory 如果为true,根据Exchange类型和routkey规则,如果无法找到符合条件的队列,那么会把发送的消息
		// 返还给发送者
		false,
		// 如果为true,当exchange发送消息到消息队列后发现队列上没有绑定消费者,则会把消息返还给发送者
		false,
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(message),
		},
	)
	if err != nil {
		r.FailOnErr(err, "Failed to publish message.")
	}
}

// 简单模式下消费代码
func (r *RabbitMQ) ConsumeSimple() {
	// 1、不管是生产还是消费,都需要申请队列
	_, err := r.channel.QueueDeclare(
		r.QueueName,
		false,
		false,
		false,
		false,
		nil,
	)
	if err != nil {
		fmt.Println(err)
	}

	// 2、接受消息
	msgs, err := r.channel.Consume(
		r.QueueName,
		// consumer, 用来区分多个消费者
		"",
		// autoAck, 是否自动应答,为false则自己写回调进行应答
		true,
		// exclusive, 是否具有排他性
		false,
		// noLocal, 如果设置为true,表示不能将同一个connection中发送的消息传递给这个connection中的消费者
		false,
		// noWait, 队列是否阻塞
		false,
		// 其他参数
		nil,
	)
	if err != nil {
		fmt.Println(err)
	}

	// 3、处理消息
	forever := make(chan bool)
	// 启用协程处理
	go func() {
		for m := range msgs {
			// 实现我们要处理的逻辑函数
			log.Printf("Received a message: %s", m.Body)
		}
	}()
	log.Printf("[*] Waiting for messages, To exit press CTRL+C")
	<-forever
}

在使用上述定义好的消费者、生产者代码时,都需要先调用NewRabbitMQSimple()来创建一个RabbitMQ实例,然后调用生产者生产、消费者消费。

// 生产者
func main()  {
	// 首先获取实例
	rabbitmq :=  RabbitMQ.NewRabbitMQSimple("imoocSimple")
	for i := 0; i < 10; i ++ {
		rabbitmq.PublishSimple("Hello Simple!!!")
		fmt.Println("发送成功!")
	}
}
// 消费者
func main() {
	rabbitmq :=  RabbitMQ.NewRabbitMQSimple("imoocSimple")
	rabbitmq.ConsumeSimple()
}

3.2 工作模式

参考链接

工作模式是为了避免等待一些占用大量资源、时间的操作。但我们把任务当做消息发送到队列中,一个运行在后台的工作者进程就会取出任务然后处理。当运行多个工作者,任务就会在他们之间共享。

工作模式中生产者和消费者的定义与简单模式下的定义没有什么区别,就是可以定义多个消费者对队列中的消息进行消费。(当生产消息速度大于消费消息的速度时,就应该启动work模式,以增加服务器的性能,起到负载均衡的作用。)

// 生产者
func main() {
	rabbit := RabbitMQ.NewRabbitMQSimple("imoocWork")
	for i := 0; i < 10; i++ {
		rabbit.PublishSimple("Hello imooc" + strconv.Itoa(i))
		//time.Sleep(time.Second)
		fmt.Println(i)
	}
}
// 消费者一
func main() {
	rabbit := RabbitMQ.NewRabbitMQSimple("imoocWork")
	rabbit.ConsumeSimple()
}
// 消费者二
func main() {
	rabbit := RabbitMQ.NewRabbitMQSimple("imoocWork")
	rabbit.ConsumeSimple()
}
// 消费者三
func main() {
	rabbit := RabbitMQ.NewRabbitMQSimple("imoocWork")
	rabbit.ConsumeSimple()
}
// 我们可以看到几个消费端轮流打印消息

使用工作模式的一个好处就是它能够并行的处理队列中的任务,当队列堆积了很多任务时,只需要添加更多的消费者就可以了。默认来说,RabbitMQ会按照顺序将消息发送给每个消费者。平均每个消费者都会收到同等数量的消息,这种发送消息的方式叫做轮询

3.3.1 消息确认

当处理一个比较耗时的任务时,可能消费者会运行到一半就挂掉。当前的代码中,当消息被RabbitMQ发送给消费者之后,马上就会在内存中移除,这种情况下,只要有一个消费者down了,正在处理的消息就会丢失。同时,所有发送到这个消费者中还未处理的消息都会丢失。为了防止消息丢失,RabbitMQ提供了消息响应机制,消费者会通过一个ack(响应)告诉RabbitMQ已经收到并且处理掉了某条消息,则RabbitMQ就会释放并删除这条消息。如果消费者挂掉了,没有发送响应,RabbitMQ会认为消息没有被完全处理掉,然后重新发送给其他消费者。这样,即使消费者偶尔挂掉了,也能够保证消息被消费掉,不会丢失。

为了实现上述目标,在消费者的代码中要做以下更改:

func (r *RabbitMQ) ConsumeSimple() {
	_, err := r.channel.QueueDeclare(
		r.QueueName,
		false,
		false,
		false,
		false,
		nil,
	)
	if err != nil {
		fmt.Println(err)
	}

	// 2、接受消息
	msgs, err := r.channel.Consume(
		r.QueueName,
		"",
		// autoAck, 在这里改为false,表明手动应答
		false,
		false,
		false,
		false,
		nil,
	)
	if err != nil {
		fmt.Println(err)
	}
	forever := make(chan bool)
	go func() {
		for m := range msgs {
			// 实现我们要处理的逻辑函数
			log.Printf("Received a message: %s", m.Body)
            // 在处理完这条消息时,使用m.Ack(false)进行确认,告诉RabbitMQ此条消息已经消费完,可以被释放。(如果为true则表示确认所有未确认的消息,一般用于批量处理)。如果忘记确认,会把队列中的消息依次消费一遍,但是服务器上的消息不会被删除,RabbitMQ将会占用越来越多的内存。为了防止接收端在消费消息的时候down掉,当消息消费完成之后再发送ack消息
            m.Ack(false)
		}
	}()
	log.Printf("[*] Waiting for messages, To exit press CTRL+C")
	<-forever
}

3.3.2 消息持久化

如果我们没有特意的告诉RabbitMQ,那么它在退出或者崩溃的时候,将会丢失所有的队列和消息。为了确保消息不会丢失,我们必须要把队列和消息设为持久化。

首先,为了不让队列消息,在生产者和消费者声明队列的时候,将durable设为true:

q, err := ch.QueueDeclare(
  rabbitMQ.queueName,      // name,同时需要注意,如果我们已经声明一个队列后,是不能对其进行修改的,只能删除后重建。
  true,         // durable
  false,        // delete when unused
  false,        // exclusive
  false,        // no-wait
  nil,          // arguments
)
failOnError(err, "Failed to declare a queue")

在发送消息的时候设置消息的持久化:

err = ch.Publish(
  "",           // exchange
  rabbitMQ.queueName,       // routing key
  false,        // mandatory
  false,
  amqp.Publishing {
    DeliveryMode: amqp.Persistent, // 设置消息的持久化
    ContentType:  "text/plain",
    Body:         []byte(body),
})

注意:将消息设为持久化并不能完全保证不会丢失。以上代码只是告诉了RabbitMq要把消息存到硬盘,但从RabbitMq收到消息到保存之间还是有一个很小的间隔时间。因为RabbitMq并不是所有的消息都使用fsync(2)——它有可能只是保存到缓存中,并不一定会写到硬盘中。并不能保证真正的持久化,但已经足够应付我们的简单工作队列。如果您需要更强的保证,那么您可以使用publisher confirms.

3.3.3 公平调度(公平分发)

RabbitMQ给消费者的分发机制并不是那么的优雅,默认状态下,RabbitMQ将第n个消息发送给第n消费者。n是取余后的,它不管消息队列中是否还有未确认的消息,只是按照这个默认的机制进行分发。

// 我们可以设置预取计数值为1。告诉RabbitMQ一次只向一个消费者发送一条消息。换句话说,在处理并确认前一个消息之前,不要向工作人员发送新消息。消费者流控,防止暴库。
err = r.channel.Qos(
		1,     // prefetch count, 当前消费者每次只消费一个消息
		0,     // prefetch size, 服务器传递的最大容量(以八位字节为单位)
		false, // global,如果为true,对channel可用
)

注意,这种方法可能会导致queue满。当然,这种情况下你可能需要添加更多的Consumer,或者创建更多的virtualHost来细化你的设计。

3.3 发布/订阅模式

参考链接

工作模式中是每个消息只发送给一个消费者,发布/订阅模式要做的就是把每个消费发送给不同的消费者。

RabbitMQ完整的消息模型如下所示:

  • 发布者(生产者,Producer):发布消息的应用程序;
  • 队列(queue):用于消息存储的缓冲;
  • 消费者(Consumer):用于接收消息的应用程序。

其核心理念就是:发布者不会直接发送任何消息给队列,事实上,发布者甚至不知道消息是否已经被投递给队列。发布者只需把消息发送给一个交换机。交换机非常简单,它一边从发布者接收消息,一边把消息推送给队列。交换机必须知道如何处理它接收到的消息,是应该推送到指定的队列还是多个队列,或者是直接忽略消息。这些规则是通过交换机类型(exchange type)来定义的。(注:在简单模式和工作模式中,只定义了队列,用空字符串 "" 代替交换机默认使用匿名交换机)。上面也说到了,交换机的类型有:direct、topic、headers和fanout。在发布/订阅模式下,使用到的交换机类型为fanout。

fanout顾名思义为广播模式,即交换机将收到的消息发送给它所知道的所有队列。现在我们可以发送消息到一个具名交换机了:

func (r *RabbitMQ) PublishSubscribe(message string) {
    // 尝试创建交换机
    err = ch.ExchangeDeclare(
      r.Exchange,   // name 交换机名称
      "fanout", // type 交换机类型
      true,     // durable,交换机持久化,与上面的队列持久化、消息持久化相似
      false,    // auto-deleted
      false,    // internal
      false,    // no-wait
      nil,      // arguments
    )
    failOnError(err, "Failed to declare an exchange")
	// 将消息发送到刚创建的交换机内
    err = ch.Publish(
      r.Exchange, // exchange
      "",         // routing key
      false,      // mandatory
      false,      // immediate
      amqp.Publishing{
              ContentType: "text/plain",
              Body:        []byte(message),
      })
}

以上就是我们生产端的代码了,与之前的没什么太大的区别,在发送的时候需要提供routing-key参数,但是由于我们要广播给所有的队列,所有在这里忽略它的值。

3.3.1 临时队列

在简单模式和工作模式下,首先声明的是一个队列的名称。给一个队列命名是很重要的,我们需要把工作者指向正确的队列。在发布模式下,我们打算接收的是所有的消息,而不仅仅是一小部分。所以我们需要做以下两件事情:

  1. 首先当我们连上RabbitMQ的时候,我们需要一个全新的、空的队列,我们可以手动创建一个随机的队列名,或者让服务器为我们选择一个随机的队列名(推荐);
  2. 其次,当与消费者断开连接的时候,这个队列应该被立即删除。在amqp客户端中,当我们将队列名称作为空字符串提供时,我们创建一个具有生成名称的非持久队列
q, err := ch.QueueDeclare(
  "",    // name,申请队列时,传入空字符串,让服务器自动生成
  false, // durable
  false, // delete when usused
  true,  // exclusive
  false, // no-wait
  nil,   // arguments
)

上述代码返回的队列实例包含RabbitMQ生成的随机队列名称。例如,它可能看起来像amq.gen-JzTY20BRgKO-HjmUJj0wLg由于申请队列时,exclusive属性设为true,表明这是一个排他性的队列,(排他队列只能由声明它们的连接访问,并且在连接关闭时将被删除。当试图声明、绑定、使用、清除或删除具有相同名称的队列时,其他连接上的通道将收到一个错误),因此当声明它的连接关闭时,队列将被删除。

3.3.2 绑定(Bindings)

我们已经创建了一个fanout交换机和一个队列。现在我们需要告诉交换机如何发送消息给我们的队列。交换机和队列之间的关系称为绑定(binding)。

err = ch.QueueBind(
  q.Name, // queue name
  "",     // routing key
  r.Exchange, // exchange
  false,
  nil,
)

3.3.3 消费端代码整合

// 2、订阅模式下的消费端
func (r *RabbitMQ) ConsumeSubscribe() {
	// 1.尝试创建交换机
	err := r.channel.ExchangeDeclare(
		// 交换机的名称
		r.Exchange,
		// 在订阅模式下设置为fanout,意为广播模式
		"fanout",
		false,
		false,
		// 为true表示这个exchange不可以被client用来推送消息,仅用来exchange和exchange的绑定
		false,
		false,
		nil,
	)
	if err != nil {
		r.FailOnErr(err, "Failed to declare an exchange.")
	}
	// 2.尝试创建队列,注意不要写名称
	queue, err1 := r.channel.QueueDeclare(
		"",    // name,申请队列时,传入空字符串,让服务器自动生成
        false, // durable
        false, // delete when usused
        true,  // exclusive
        false, // no-wait
        nil,   // arguments
	)
	if err1 != nil {
		r.FailOnErr(err1, "Failed to declare a queue.")
	}
	// 3.绑定刚创建的队列到exchange中
	err = r.channel.QueueBind(
		queue.Name,   // queue name
		"",           // routing key,在这里应该称为binding-key,同样忽略它的值
		r.Exchange,  // exchange
		false,
		nil,
	)
	if err != nil {
		r.FailOnErr(err, "Failed to bind a queue.")
	}
	// 4.消费消息
	message, err2 := r.channel.Consume(
		queue.Name,   // queue
		 "",     // consumer
         true,   // auto-ack
         false,  // exclusive
         false,  // no-local
         false,  // no-wait
         nil,    // args
	)
	if err2 != nil {
		fmt.Println(err2)
	}
	exit := make(chan bool)
	go func() {
		for m := range message {
			log.Printf("Recieved a message:%s", m.Body)
		}
	}()
	log.Printf("[*] Waiting for messages, To exit press CTRL+C")
	<-exit
}

3.4 路由模式

参考模式

在上述发布/订阅模式下,交换机是把所有的消息发送给知道的所有队列。那么在路由模式下,我们需要从不同的队列中获取不同的消息。

3.4.1 绑定(Bindings)

在上述发布/订阅模式中,我们已经创建过绑定,就是指交换机和队列的关系,可以简单理解为:这个队列对这个交换机的消息感兴趣。绑定的时候可以带上一个额外的routing_key参数,为了与channel.Publish中的routing-key混淆,我们可以叫做这个键为binding-key。

err = r.channel.QueueBind(
		queue.Name,
		//
		r.Key,
		r.Exchange,
		false,
		nil,
	)

在上述发布/订阅模式中,我们用的交换机类型为fanout,这个类型的交换机会自动忽略绑定键,所以在路由模式下,我们要使用直连交换机(Direct Exchange)。

3.4.2 直连交换机

上面也说到,我们想要从不同的队列中获取不同的消息,fanout类型的交换机能做的仅仅是广播,所以使用direct类型的交换机来代替。路由的算法很简单:交换机将会对binding-key和routing-key进行精确的匹配,从而确定消息该分发到哪个队列。

如上所示,我们可以看到type类型的交换机X和两个队列进行了绑定,第一个队列使用orange作为binding key,第二个队列有两个绑定,一个使用black作为binding key,另外一个使用green。这样一来,当消息发布到routing key为orange的交换机时,就会被理由到Q1队列;routing key为black或green的消息就会被路由到Q2,其他的消息都会被丢弃。同时,对个队列使用相同的binding-key也是合法的。

如上图所示,direct交换机就和fanout交换机的行为一样了,会将消息广播到所有匹配的队列中。带有routing-key为black的消息会同时发送到Q1和Q2。

3.4.3 代码整合

// 创建RabbitMQ实例
func NewRabbitMQRouting(exchangeName, routeKey string) *RabbitMQ {
	return NewRabbitMQ("", exchangeName, routeKey)
}

// 路由模式下的生产消息
func (r *RabbitMQ) PublishRouting(message string) {
	// 1. 尝试创建交换机
	err := r.channel.ExchangeDeclare(
		r.Exchange,
		// 与订阅模式不同的是类型定义为direct
		"direct",
		true,
		false,
		false,
		false,
		nil,
	)
	if err != nil {
		r.FailOnErr(err, "Failed to declare an exchange.")
	}
	// 2. 发送消息
	err = r.channel.Publish(
		r.Exchange,
		// key必须带上
		r.Key,
		false,
		false,
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(message),
		},
	)
	if err != nil {
		r.FailOnErr(err, "Failed to publish a message.")
	}
}

// 路由模式下消息的接收
func (r *RabbitMQ) ConsumeRouting() {
	// 1.首先声明交换机
	err := r.channel.ExchangeDeclare(
		r.Exchange,
		"direct",
		true,
		false,
		false,
		false,
		nil,
	)
	if err != nil {
		r.FailOnErr(err, "Failed to declare an exchange.")
	}
	// 2.声明队列,队列名称不要写,是随机产生的
	queue, err1 := r.channel.QueueDeclare(
		"",
		true,
		false,
		false,
		false,
		nil,
	)
	if err1 != nil {
		r.FailOnErr(err1, "Failed to declare a queue.")
	}
	// 3.将交换机绑定到队列上面,需要指定bindingKey,通过一系列规则绑定key到交换机上
	err = r.channel.QueueBind(
		queue.Name,
		//
		r.Key,
		r.Exchange,
		false,
		nil,
	)
	if err != nil {
		r.FailOnErr(err, "Failed to bind a queue.")
	}

	msg, err2 := r.channel.Consume(
		queue.Name,
		"",
		true,
		false,
		false,
		false,
		nil,
	)
	if err2 != nil {
		r.FailOnErr(err2, "Failed to consume the message.")
	}
	exit := make(chan bool)

	go func() {
		for m := range msg {
			log.Printf("Recieved a message: %s", m.Body)
		}
	}()

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

使用上述代码来进行生产和消费:

// 生产端
func main() {
    // imoocRouting为交换器的名称,imoocOne、imoocTwo为routeKey
	rabbitOne := RabbitMQ.NewRabbitMQRouting("imoocRouting", "imoocOne")
	rabbitTwo := RabbitMQ.NewRabbitMQRouting("imoocRouting", "imoocTwo")

	for i := 0; i < 10; i++ {
		rabbitOne.PublishRouting("hello imooc one!" + strconv.Itoa(i))
		rabbitTwo.PublishRouting("hello imooc two!" + strconv.Itoa(i))
		time.Sleep(time.Second)
		fmt.Println(i)
	}
}

// 消费端,将只会收到hello imooc one! 的消息
func main() {
	rabbitOne := RabbitMQ.NewRabbitMQRouting("imoocRouting", "imoocOne")
	rabbitOne.ConsumeRouting()
}
// 将只会收到hello imooc two! 的消息
func main() {
	rabbitTwo := RabbitMQ.NewRabbitMQRouting("imoocRouting", "imoocTwo")
	rabbitTwo.ConsumeRouting()
}

3.5 话题模式

参考链接

在上述路由模式中,尽管我们可以指定规则去生产和消费,但是没办法基于多个标准执行路由操作。于是我们就可以使用topic交换机来满足我们的需求。

3.5.1 topic交换机

发送到topic交换机的消息不可以携带随意的routing_key,它的routing_key只能是一个由 . 分隔开的词语列表。这些单词最好是跟携带它们的消息有关系的词汇,词语的个数可以随意,但是不要超过255字节。binding_key也必须拥有同样的格式。topic交换机背后的逻辑与direct交换机很相似,一个携带着特定routing_key的消息会被topic交换机投递给绑定键与之想匹配的队列。但是binding_key和routing_key有两个特殊应用方式:

  1. *(星号)用来表示一个单词;
  2. (井号)用来表示任意数量(零个或多个)单词。

如上图所示:发送的消息所携带的路由键是由三个单词所组成的,这三个单词被两个.分割开。路由键里的第一个单词描述的是动物的手脚的利索程度,第二个单词是动物的颜色,第三个是动物的种类。所以它看起来是这样的: <celerity>.<colour>.<species>。我们创建了三个绑定:Q1的绑定键为 *.orange.*,Q2的绑定键为 *.*.rabbitlazy.# 。可以总结为:Q1对所有橘黄色动物都很感兴趣;Q2则是对所有的兔子和所有懒惰的动物都很感兴趣。

一个携带有 quick.orange.rabbit 的消息将会被分别投递给这两个队列。携带着 lazy.orange.elephant 的消息同样也会给两个队列都投递过去。另一方面携带有 quick.orange.fox 的消息会投递给第一个队列,携带有 lazy.brown.fox 的消息会投递给第二个队列。携带有 lazy.pink.rabbit 的消息只会被投递给第二个队列一次,即使它同时匹配第二个队列的两个绑定。携带着 quick.brown.fox 的消息不会投递给任何一个队列。

如果我们违反约定,发送了一个携带有一个单词或者四个单词("orange" or "quick.orange.male.rabbit")的消息时,发送的消息不会投递给任何一个队列,而且会丢失掉。但是另一方面,即使 "lazy.orange.male.rabbit" 有四个单词,他还是会匹配最后一个绑定,并且被投递到第二个队列中。

3.5.2 代码整合

生产端和消费端代码与上述路由的代码没有太大区别,就是在定义交换机的时候,类型指定为“direct”。

// 生产端,向imoocTopic交换机内发送消息,routing-key分别为imooc.one、imooc.one.two
func main() {
	rabbitOne := RabbitMQ.NewRabbitMQTopic("imoocTopic", "imooc.one")
	rabbitTwo := RabbitMQ.NewRabbitMQTopic("imoocTopic", "imooc.one.two")

	for i := 0; i < 10; i++ {
		rabbitOne.PublishTopic("Hello imooc one" + strconv.Itoa(i))
		rabbitTwo.PublishTopic("Hello imooc two" + strconv.Itoa(i))
		time.Sleep(time.Second)
		fmt.Println(i)
	}
}
// 消费端1,只会收到上述rabbitOne发送的 Hello imooc one 消息
func main() {
	rabbitOne := RabbitMQ.NewRabbitMQTopic("imoocTopic", "imooc.*")
	rabbitOne.ConsumeTopic()
}
// 将会收到所有的消息
func main() {
	rabbitAll := RabbitMQ.NewRabbitMQTopic("imoocTopic", "#")
	rabbitAll.ConsumeTopic()
}

3.6 远程过程调用(RPC)

参考链接

课外阅读:阿里巴巴为什么能抗住90秒100亿?看完这篇你就明白了!

posted @ 2021-06-21 17:34  xiaofeidu  阅读(134)  评论(0编辑  收藏  举报