三、发布订阅

上一节的练习中我们创建了一个工作队列。队列中的每条消息都会被发送至一个工作进程。这节,我们将做些完全不同的事情——我们将发送单个消息发送至多个消费者。这种模式就是广为人知的“发布订阅”模式。

为了说明这种模式,我们将构建一个简单的日志系统。包括2个应用程序,一个传送日志消息另一个接收并打印这些消息。

我们的日志系统中每一个运作的接收端程序都会收到这些消息。这种方式下,我们就可以运行一个接收端发送日志消息至硬盘,同时可以运行另一个接收端将日志打印到屏幕上。

理论上讲,已发布的日志消息将会被广播到所有的接收者。

交换器(Exchange

之前的几节练习中我们发送接收消息都是在队列中进行,是时候介绍下RabbitMQ完整的消息传递模式了。

先来迅速的回顾下我们之前章节:

  • 一个生产者就是一个用来发送消息的应用程序
  • 一个 队列好比存储消息的缓存buffer
  • 一个消费者就是一个用户应用程序用来接收消息

RabbitMQ消息传递模型的核心思想是生产者从来不会直接发送消息至队列。事实上,生产者经常都不知道消息会被分发至哪个队列。

相反的是,生产者仅仅发送消息至交换器。交换器是非常简单的东西:一边从生产者那边接收消息一边发送这些消息至队列。交换器必须准确的知道这些被接收的消息该如何处理。它应该被添加到某个特定队列?或者添加到多个队列?甚至直接放弃。具体的传输规则就是通过交换器类型来定义的。

交换器类型有四种:direct、topic、headers、fanout。这节我们主要关注最后一种——fanout。让我们来创建一个fanout类型的交换器,命名为logs:

err = ch.ExchangeDeclare(
"logs", // name
"fanout", // type
true, // durable
false, // auto-deleted
false, // internal
false, // no-wait
nil, // arguments
)

正如你从名字中猜测的一样,它仅仅广播所有消息到所有已知的接收队列。实际上这正是我们需要的日志系统。

备注:之前的几节练习中我们并不知道交换器,但我们依然能够将消息发送至队列中,之所以可以实现是因为我们使用了默认的交换器,使用空字符串表示。

回顾下之前我们发送消息是这样子的:

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

这里我们可以使用默认也可以自己命名交换器:如果路由键存在的话,消息会被路由到加上路由键参数的地址,注意fanout类型会直接忽略路由键的存在。

以下是修改后的代码:

err = ch.ExchangeDeclare(
"logs", // name 定义一个名为logs的交换器
"fanout", // type 交换器类型为fanout即广播类型
true, // durable 持久化
false, // auto-deleted 无队列绑定时是否自动删除
false, // internal
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare an exchange") body := bodyFrom(os.Args)
err = ch.Publish(
"logs", // exchange 指定消息发送的交换器名称
"", // routing key 因为fanout类型会自动忽略路由键,所以这里的路由键参数任意,一般不填
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(body),
})

临时队列

你可能记得之前我们声明队列的时候都会指定一个队列名称(记得hello和task_queue?)。队列的命名对我们来说至关重要——我们需要将工作进程指向同一个队列。当你需要在消费者和生产者之间共享队列的话声明队列就显得很重要。

但这对我们的日志系统来说无关重要。我们需要监听的是所有的日志消息,而不是他们中的某一类。我们只关注当前流中的消息而不关注旧的那些。解决这个我们需要做两件事。

首先,每当链接RabbitMQ的时候我们需要创建一个新的、空的队列。为做到这点,我们必须创建一个名称随机的队列,甚至更好的实现方式是——让服务端给我们自动生成一个随机的队列。

其次,一旦消费者链接断开,该队列便会自动删除。

在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参数设置的那样,自动删除。

绑定

我们已经创建了一个fanout类型的交换器和一个队列,现在我们需要告诉交换器将消息发送至我们的队列。这种交换器和队列中的关联关系就叫做绑定。

err = ch.QueueBind(
q.Name, // queue name 绑定的队列名称
"", // routing key 绑定的路由键
"logs", // exchange 绑定的交换器名称
false,
nil
)

从现在起,logs交换器便能发送消息至我们的队列。

糅合在一起

生产者的程序,也就是发送消息端,跟之前几节的发送代码差不多。最重要的是我们现在要发送消息到logs交换器而非默认的交换器。发送的时候我们可以设置一个路由键,但是对于fanout类型的交换器来说它将被忽略。下面就是发送日志方的代码:

// rabbitmq_3_emit_log.go project main.go
package main import (
"fmt"
"log"
"os"
"strings" "github.com/streadway/amqp"
) func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s:%s", err, msg)
panic(fmt.Sprintf("%s:%s", err, msg))
}
} func bodyForm(args []string) string {
var s string
if len(args) < || os.Args[] == "" {
s = "Hello World! This is a test!"
} else {
s = strings.Join(args[:], " ")
}
return s
} func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "failed to dial rabbitmq server")
defer conn.Close() ch, err := conn.Channel()
failOnError(err, "failed to declare the channel")
defer ch.Close() //声明一个交换器,交换器名称logs,类型fanout
err = ch.ExchangeDeclare("logs", "fanout", true, false, false, false, nil)
failOnError(err, "failed to declare the exchange") body := bodyForm(os.Args)
//发送消息到交换器
err = ch.Publish("logs", "", false, false, amqp.Publishing{
Body: []byte(body),
ContentType: "text/plain",
})
failOnError(err, "failed to publish the message")
}

备注:这里发送方并不需要声明队列之类的,不像之前的代码需要声明,这里的发送方唯一关联的是交换器,所以只需声明交换器并发送消息至交换器即可。

正如你想的那样,链接建立后我们声明交换器,这一步是必须的因为发送消息到一个不存在的交换器是完全禁止的。

如果该交换器上面没有队列绑定的话那么发送至该交换器的消息将全部丢失,但这对我们来时ok;如果没有消费者我们会安全地丢弃这些消息。

下面是日志接收方的代码:

// rabbitmq_3_receive_logs.go project main.go
package main import (
"fmt"
"log"
"os"
"strings" "github.com/streadway/amqp"
) func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s:%s", err, msg)
panic(fmt.Sprintf("%s:%s", err, msg))
}
} func bodyForm(args []string) string {
var s string
if len(args) < || os.Args[] == "" {
s = "Hello World! This is a test!"
} else {
s = strings.Join(args[:], " ")
}
return s
} func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "failed to dial rabbitmq server")
defer conn.Close() ch, err := conn.Channel()
failOnError(err, "failed to declare the channel")
defer ch.Close() //声明一个交换器,交换器名称logs,类型fanout
err = ch.ExchangeDeclare("logs", "fanout", true, false, false, false, nil)
failOnError(err, "failed to declare the exchange") //声明一个队列
q, err := ch.QueueDeclare("", false, false, true, false, nil)
failOnError(err, "failed to declare the queue") //设置绑定(第二个参数为路由键,这里为空)
err = ch.QueueBind(q.Name, "", "logs", false, nil)
failOnError(err, "failed to bind the queue") //注册一个消费者
msgs, 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 msgs {
log.Printf(" [x] %s", d.Body)
}
}()
log.Printf(" [*] Waiting for logs. To exit press CTRL+C")
<-forever
}

如果你想将日志保存到文件,执行如下命令:

go run receive_logs.go > logs_from_rabbit.log

如果你仅仅想在屏幕上查看日志,开启一个新的控制台执行如下命令:

go run receive_logs.go

当然了,你最后还要发出日志才行:

go run emit_log.go

使用rabbitmqctl list_bindings命令可以直接查看所有的绑定,如运行2个receive_logs.go程序你就会看到如下输出:

rabbitmqctl list_bindings
Listing bindings ...
logs exchange amq.gen-JzTY20BRgKO-HjmUJj0wLg queue []
logs exchange amq.gen-vso0PVvyiRIL2WoV3i48Yg queue []
...done.

实际效果:

分别开启两个控制台,均监听相同队列,同时收到消息并打印了,说明两个随机的队列均收到了logs交换器发来的消息,发送方略。

rabbitmq消息队列——"发布订阅"的更多相关文章

  1. Spring Data Redis实现消息队列——发布/订阅模式

    一般来说,消息队列有两种场景,一种是发布者订阅者模式,一种是生产者消费者模式.利用redis这两种场景的消息队列都能够实现. 定义:生产者消费者模式:生产者生产消息放到队列里,多个消费者同时监听队列, ...

  2. redis实现消息队列&发布/订阅模式使用

    在项目中用到了redis作为缓存,再学习了ActiveMq之后想着用redis实现简单的消息队列,下面做记录.   Redis的列表类型键可以用来实现队列,并且支持阻塞式读取,可以很容易的实现一个高性 ...

  3. 【转】redis 消息队列发布订阅模式spring boot实现

    最近做项目的时候写到一个事件推送的场景.之前的实现方式是起job一直查询数据库,看看有没有最新的消息.这种方式非常的不优雅,反正我是不能忍,由于羡慕本身就依赖redis,刚好redis 也有消息队列的 ...

  4. 使用EasyNetQ组件操作RabbitMQ消息队列服务

    RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue)的开源实现,是实现消息队列应用的一个中间件,消息队列中间件是分布式系统中重要的组件,主要解决应用耦合, ...

  5. (五)RabbitMQ消息队列-安装amqp扩展并订阅/发布Demo(PHP版)

    原文:(五)RabbitMQ消息队列-安装amqp扩展并订阅/发布Demo(PHP版) 本文将介绍在PHP中如何使用RabbitMQ来实现消息的订阅和发布.我使用的系统依然是Centos7,为了方便, ...

  6. rabbitMQ消息队列1

    rabbitmq 消息队列 解耦 异步 优点:解决排队问题 缺点: 不能保证任务被及时的执行 应用场景:去哪儿, 同步 优点:保证任务被及时的执行 缺点:排队问题 大并发 Web nginx 1000 ...

  7. RabbitMQ 消息队列 应用

    安装参考    详细介绍   学习参考 RabbitMQ 消息队列 RabbitMQ是一个在AMQP基础上完整的,可复用的企业消息系统.他遵循Mozilla Public License开源协议. M ...

  8. (十一)RabbitMQ消息队列-如何实现高可用

    原文:(十一)RabbitMQ消息队列-如何实现高可用 在前面讲到了RabbitMQ高可用集群的搭建,但是我们知道只是集群的高可用并不能保证应用在使用消息队列时完全没有问题,例如如果应用连接的Rabb ...

  9. (十二)RabbitMQ消息队列-性能测试

    原文:(十二)RabbitMQ消息队列-性能测试 硬件配置 宿主机用的联想3850X6的服务器四颗E7-4850v3的处理器,DDR4内存,两块1.25TB的pcie固态.在宿主机上使用的事esxi5 ...

随机推荐

  1. <数据结构与算法>之字符串,散列,布隆过滤器。

    1:字符串 字符串是一组由数字,字符,下划线的一串字符,是特殊的一维数组. 2:字符串的应用 字符串移位包含问题: 例:给定两个字符串s1和s2,要求判断s2是否能被s1做循环移位得到字符串包含.例如 ...

  2. 安装文件制作工具Wix概念快速入门

    前言 Wix==Windows installer XML 顾名思议. 用于制作WINDOWS安装文件的XML格式的描述文件. 因为其实现方式为基于声明的方式,而非命令的方式. 特整理一下其相关的概念 ...

  3. Fragment要点复习

    做系统时间久了,应用层的很多东西都忘了,但是基础还是得巩固,下面对fragment相关的知识点简单整理一下. 一.fragment的引入 fragment是从Android3.0(API level ...

  4. step by step 之餐饮管理系统七(点菜模块实现)

    好长时间没有更新这个系列了,一是因为这段时间比较忙,有很多事情,二来要学习新的东西,AngularJs,devExpress这两上框架,都是比较有名的框架,先上图: 上面就是用来点菜的界面,左边是已点 ...

  5. Java程序,求学员的平均成绩

    第一步,系统提示输入学员的人数. 第二步,逐一获取学员的分数,并累计. 第三步,求平均成绩,并输出. import java.util.Scanner; public class chengji { ...

  6. hdu 1166 敌兵布阵(树状数组)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1394 题意:C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始 ...

  7. 【转】react 状态与属性区别

    prop                  state 能否从父组件获取初始值             是                      否 能否由父组件修改               ...

  8. 简单的导航viewpager

    下载PagerSlidingTabStrip        https://github.com/astuetz/PagerSlidingTabStrip 主界面: public class Main ...

  9. linux Ubuntu(Segmentation fault)段错误出现原因及调试方法

      在linux下编译了一个程序,尝试运行的时候出现: Segmentation fault (core dumped) 初步确认为...完全不知道是什么玩意. 于是找度娘了. ----------- ...

  10. cordova for ios(android一样)添加插件

    1.进入当前工程文件夹 终端:cd ~/Desktop/ cd piao 2.添加插件 :cordova plugin add Basic device information (Device API ...