rabbitmq消息队列——"工作队列"

时间:2022-09-12 12:12:38

二、工作队列

rabbitmq消息队列——"工作队列"

在第一节中我们发送接收消息直接从队列中进行。这节中我们会创建一个工作队列来分发处理多个工作者中的耗时性任务。

工作队列主要是为了避免进行一些必须同步等待的资源密集型的任务。实际上我们将这些任务时序话稍后分发完成。我们将某个任务封装成消息然后发送至队列,后台运行的工作进程将这些消息取出然后执行这些任务。当你运行多个工作进程的时候,这些任务也会在它们之间共享。

前期准备

上一节的练习中我们发送的是简单包含“Hello World!”的消息,这节我们还发送字符串不过用此代表更复杂的任务,实际我们这里并没有真正的任务,像图片缩放或pdf文件渲染之类的,这里我们假装我们很忙(即处理的消息任务很耗时),使用time.Sleep函数实现。我们用字符串中的”.”符号的数量代表任务的复杂性,每一个”.”需要耗时1s来执行处理。比如:”Hello…”代表该消息处理耗时3s。

我们稍微修改下上节中send.go代码,为了可以在命令行直接发送任意数量的消息。该程序将任务发送到我们的队列,暂且命名为new_task.go:

body := bodyFrom(os.Args)
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false,
amqp.Publishing {
DeliveryMode: amqp.Persistent,
ContentType: "text/plain",
Body: []byte(body),
})
failOnError(err, "Failed to publish a message")
log.Printf(" [x] Sent %s", body)

我们旧的receiver.go为程序也要坐下修改:对每个消息体中的”.”符号它需要伪造一个每秒执行的工作队列。它将消息从队列中取出并执行,所以这里暂且命名为work.go:

msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer") forever := make(chan bool) go func() {
for d := range msgs {
log.Printf("Received a message: %s", d.Body)
dot_count := bytes.Count(d.Body, []byte("."))
t := time.Duration(dot_count)
time.Sleep(t * time.Second)
log.Printf("Done")
}
}() log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever

请注意,我们这里的假任务模拟的是执行时间。如上一节中方式,运行:

shell1$ go run worker.go
shell2$ go run new_task.go

运行work.go:

rabbitmq消息队列——"工作队列"

运行new_task.go:

rabbitmq消息队列——"工作队列"

rabbitmq消息队列——"工作队列"

可以看到,work.go循环监听消息并打印,new_task.go中,我们接收控制台参数作为消息内容并发送,消息接收后自动应答。

轮转分发(Round-robin dispatching

使用任务队列的一个优点就是有能力更简单的处理平行任务,如果工作任务堆积之后,我们只需要增加更多的工作进程,可以很简单的实现规模拓展。

首先,我们同时运行2个工作队列,都从消息队列中获取消息,实际会怎么样呢?来看看。

你现在需要打开2个窗口,都运行work.go,即work1和work2,这就是我们的2个消费者:C1、C2。

rabbitmq消息队列——"工作队列"

第3个窗口我们用来发送消息到队列,一旦消费者运行起来后便可以发送消息:

shell3$ go run new_task.go First message.
shell3$ go run new_task.go Second message..
shell3$ go run new_task.go Third message...
shell3$ go run new_task.go Fourth message....
shell3$ go run new_task.go Fifth message.....

rabbitmq消息队列——"工作队列"

然后看下work.go中接收的数据:

rabbitmq消息队列——"工作队列"

rabbitmq消息队列——"工作队列"

默认情况下,RabbitMQ会将队列中的每条消息有序的分发给每一个消费者,比如这里的work1和work2,平均每个消费者都会获得相同数量的消息(一个队列中的同一条消息不会同时发送给超过2个消费者),这种分发消息的方式就是“轮转分发”,可以开启3个work试试。

至此完整代码如下:

new_task.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", msg, err)
panic(fmt.Sprintf("%s: %s", msg, err))
}
} func main() {
//连接服务器
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close() //声明channel
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close() //声明队列
q, err := ch.QueueDeclare(
"hello", // name 队列名称
false, // durable 是否持久化,这里false
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue") //创建请求体
body := bodyFrom(os.Args)
//发送消息
err = ch.Publish(
"", // exchange 交换器名称,使用默认
q.Name, // routing key 路由键,这里为队列名称
false, // mandatory
false,
amqp.Publishing{
ContentType: "text/plain", //消息类型,文本消息
Body: []byte(body),
})
failOnError(err, "Failed to publish a message")
log.Printf(" [x] Sent %s", body)
} func bodyFrom(args []string) string {
var s string
if (len(args) < ) || os.Args[] == "" {
s = "hello golang"
} else {
s = strings.Join(args[:], " ")
}
return s
}

work.go:

package main

import (
"bytes"
"fmt"
"log"
"time" "github.com/streadway/amqp"
) func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
panic(fmt.Sprintf("%s: %s", msg, err))
}
} func main() {
//链接服务器
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close() //声明channel
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close() //声明队列
q, err := ch.QueueDeclare(
"hello", // name 队列名称
false, // durable 持久化标识
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue") //声明消费者
msgs, err := ch.Consume(
q.Name, // queue 消费的队列名称
"", // consumer
true, // auto-ack 自动应答
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer") forever := make(chan bool) //主要用来防止主进程窗口退出 go func() {
for d := range msgs {
log.Printf("Received a message: %s", d.Body)
dot_count := bytes.Count(d.Body, []byte("."))
t := time.Duration(dot_count)
time.Sleep(t * time.Second) //延时x秒
log.Printf("Done")
}
}() log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever
}

消息应答

完成一个任务处理可能会花费数秒时间,你可能会纳闷如果其中一个消费者任务处理时间过长只部分完成就挂掉会怎样。如果使用以上代码,一旦RabbitMQ发送一个消息给消费者然后便迅速将该消息从队列内存中移除。这种情况下,如果你杀掉其中一个工作进程,那该进程正在处理的消息也将丢失。我们同样,也将丢失所有发送给该进程的未被处理的消息。

但我们并不想丢失这些任务或消息。如果某个进程挂掉,我们期望该消息仍会被发送至其它工作进程。

如果一个进程挂掉,我们希望该消息或任务可以被分发至其它工作进程。

为了确保消息永不丢失,RabbitMQ支持消息应答机制。当消息被接受,处理之后一条应答便会从消费者回传至发送方,然后RabbitMQ将其删除。

如果某个消费者挂掉(信道、链接关闭或者tcp链接丢失)且没有发送ack应答,RabbitMQ会认为该消息没有被处理完全然后会将其重新放置到队列中。通过这种方式你就可以确保消息永不丢失,甚至某个工作进程偶然挂掉的情况。

永远不会有消息超时这一说,RabbitMQ在工作进程处理挂掉后将会重发消息,这很不错甚至处理消息要发送很长很长的时间。

默认情况下消息应答是关闭的。是时候使用false(auto-ack配置项)参数将其开启了:

msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
false, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer") forever := make(chan bool) go func() {
for d := range msgs {
log.Printf("Received a message: %s", d.Body)
dot_count := bytes.Count(d.Body, []byte("."))
t := time.Duration(dot_count)
time.Sleep(t * time.Second)
log.Printf("Done")
d.Ack(false)
}
}() log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever

这里唯一不同的是将auto-ack设置为了false,使用手动应答,然后在代码中需要调用d.Ack(false),进行手动应答。

使用如上代码后,即时消息处理时按了Ctrl+C结束了进程,什么也不会丢失。工作进程挂掉后所有未应答的消息将会被重新分发。

rabbitmq消息队列——"工作队列"

消息持久化

我们已经学了如何确保消费者挂掉后任务不丢失的情况,但是一旦RabbitMQ服务器重启后我们的消息或任务依旧会丢失。

当RabbitMQ服务器停止或崩溃时,它将会丢失多有的队列和消息,除非你告诉它不要这么做。要做到服务宕机消息不丢失需要做到两点:我们需要将消息和队列同时标为持久化。

首先,我们需要确保RabbitMQ不会丢失我们的队列,为做到此,队列声明修改如下:

q, err := ch.QueueDeclare(
"hello", // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue")

即使这里被我们这样修改过,但是在先前的设置中此代码并不会工作。因为我们已经命名了一个叫做hello的队列,并且非持久。RabbitMQ不允许定义2个不同参数的队列,一旦做了将会报错。但是有一个快速的解决办法:我们声明队列换个名字就行了,如下task_queue,new_task.go:

q, err := ch.QueueDeclare(
"task_queue", // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue")

durable配置项的更改需要同时反映到生产者和消费者的代码上。

基于这点我们可以确定RabbitMQ重启后task_queue队列不会丢失了。现在我们还需要将消息标记为持久:使用amqp.Publishing配置项中的amqp.Persistent值实现:

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

完整的new_task.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", msg, err)
panic(fmt.Sprintf("%s: %s", msg, err))
}
} func main() {
//连接服务器
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close() //声明channel
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close() //声明队列
q, err := ch.QueueDeclare(
"task_queue", // name 队列名称
true, // durable 是否持久化,这里true
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue") //创建请求体
body := bodyFrom(os.Args)
//发送消息
err = ch.Publish(
"", // exchange 交换器名称,使用默认
q.Name, // routing key 路由键,这里为队列名称
false, // mandatory
false,
amqp.Publishing{
DeliveryMode: amqp.Persistent,
ContentType: "text/plain", //消息类型,文本消息
Body: []byte(body),
})
failOnError(err, "Failed to publish a message")
log.Printf(" [x] Sent %s", body)
} func bodyFrom(args []string) string {
var s string
if (len(args) < ) || os.Args[] == "" {
s = "hello golang"
} else {
s = strings.Join(args[:], " ")
}
return s
}

work.go如下:

package main

import (
"bytes"
"fmt"
"log"
"time" "github.com/streadway/amqp"
) func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
panic(fmt.Sprintf("%s: %s", msg, err))
}
} func main() {
//链接服务器
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close() //声明channel
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close() //声明队列
q, err := ch.QueueDeclare(
"task_queue", // name 队列名称
true, // durable 持久化标识
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue") //声明消费者
msgs, err := ch.Consume(
q.Name, // queue 消费的队列名称
"", // consumer
false, // auto-ack 自动应答
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer") forever := make(chan bool) //主要用来防止主进程窗口退出 go func() {
for d := range msgs {
log.Printf("Received a message: %s", d.Body)
dot_count := bytes.Count(d.Body, []byte("."))
t := time.Duration(dot_count)
time.Sleep(t * time.Second) //延时x秒
log.Printf("Done")
d.Ack(false)
}
}() log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever
}

这里测试的话,可以使用RabbitMQ自带的ctl命令进行RabbitMQ应用的重启,然后看下消息会不会丢失。

公平调度

你可能已经注意到了这种消息分发机制并非我们实际想要的那种,举例来说有两个消费者或工作进程,所有奇数的消息都很难处理而所有偶数的消息都便于处理,那么一个工作进程就比较忙碌而另一个就比较轻松,好吧,RabbitMQ实际也不清楚实际的消息分发是怎样的。

这种情况的发生是因为RabbitMQ仅仅负责分发队列中的消息。并不查看消费者中的未应答的消息数量。它只是盲目的将消息均发给每个消费者。

rabbitmq消息队列——"工作队列"

为了避免这种情况我们可以将prefetch count项的值配置为1,这将会指示RabbitMQ在同一时间不要发送超过一条消息给每个消费者。换句话说,直到消息被处理和应答之前都不会发送给该消费者任何消息。取而代之的是,它将会发送消息至下一个比较闲的消费者或工作进程。

err = ch.Qos(
, // prefetch count
, // prefetch size
false, // global
)
failOnError(err, "Failed to set QoS")

所有完整的实例代码如下:

首先是new_task.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", msg, err)
panic(fmt.Sprintf("%s: %s", msg, err))
}
} func main() {
//连接服务器
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close() //声明channel
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close() //声明队列
q, err := ch.QueueDeclare(
"task_queue", // name 队列名称
true, // durable 是否持久化,这里true
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue") //创建请求体
body := bodyFrom(os.Args)
//发送消息
err = ch.Publish(
"", // exchange 交换器名称,使用默认
q.Name, // routing key 路由键,这里为队列名称
false, // mandatory
false,
amqp.Publishing{
DeliveryMode: amqp.Persistent,
ContentType: "text/plain", //消息类型,文本消息
Body: []byte(body),
})
failOnError(err, "Failed to publish a message")
log.Printf(" [x] Sent %s", body)
} func bodyFrom(args []string) string {
var s string
if (len(args) < ) || os.Args[] == "" {
s = "hello golang"
} else {
s = strings.Join(args[:], " ")
}
return s
}

然后是work.go:

package main

import (
"bytes"
"fmt"
"log"
"time" "github.com/streadway/amqp"
) func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
panic(fmt.Sprintf("%s: %s", msg, err))
}
} func main() {
//链接服务器
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close() //声明channel
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close() //声明队列
q, err := ch.QueueDeclare(
"task_queue", // name 队列名称
true, // durable 持久化标识
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue") err = ch.Qos(
, // prefetch count
, // prefetch size
false, // global
)
failOnError(err, "Failed to set QoS") //声明消费者
msgs, err := ch.Consume(
q.Name, // queue 消费的队列名称
"", // consumer
false, // auto-ack 自动应答
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer") forever := make(chan bool) //主要用来防止主进程窗口退出 go func() {
for d := range msgs {
d.Ack(false)
log.Printf("Received a message: %s", d.Body)
dot_count := bytes.Count(d.Body, []byte("."))
t := time.Duration(dot_count)
time.Sleep(t * time.Second) //延时x秒
log.Printf("Done")
}
}() log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever
}

rabbitmq消息队列——"工作队列"的更多相关文章

  1. RabbitMQ消息队列(一)&colon; Detailed Introduction 详细介绍

     http://blog.csdn.net/anzhsoft/article/details/19563091 RabbitMQ消息队列(一): Detailed Introduction 详细介绍 ...

  2. RabbitMQ消息队列1&colon; Detailed Introduction 详细介绍

    1. 历史 RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue )的开源实现.AMQP 的出现其实也是应了广大人民群众的需求,虽然在同步消息通讯的世界里有 ...

  3. &lpar;转&rpar;RabbitMQ消息队列(九):Publisher的消息确认机制

    在前面的文章中提到了queue和consumer之间的消息确认机制:通过设置ack.那么Publisher能不到知道他post的Message有没有到达queue,甚至更近一步,是否被某个Consum ...

  4. &lpar;转&rpar;RabbitMQ消息队列(七):适用于云计算集群的远程调用&lpar;RPC&rpar;

    在云计算环境中,很多时候需要用它其他机器的计算资源,我们有可能会在接收到Message进行处理时,会把一部分计算任务分配到其他节点来完成.那么,RabbitMQ如何使用RPC呢?在本篇文章中,我们将会 ...

  5. &lpar;转&rpar;RabbitMQ消息队列(六):使用主题进行消息分发

    在上篇文章RabbitMQ消息队列(五):Routing 消息路由 中,我们实现了一个简单的日志系统.Consumer可以监听不同severity的log.但是,这也是它之所以叫做简单日志系统的原因, ...

  6. &lpar;转&rpar;RabbitMQ消息队列(四):分发到多Consumer(Publish&sol;Subscribe)

    上篇文章中,我们把每个Message都是deliver到某个Consumer.在这篇文章中,我们将会将同一个Message deliver到多个Consumer中.这个模式也被成为 "pub ...

  7. RabbitMQ消息队列应用

    RabbitMQ消息队列应用 消息通信组件Net分布式系统的核心中间件之一,应用与系统高并发,各个组件之间解耦的依赖的场景.本框架采用消息队列中间件主要应用于两方面:一是解决部分高并发的业务处理:二是 ...

  8. RabbitMQ消息队列(四):分发到多Consumer(Publish&sol;Subscribe)

    上篇文章中,我们把每个Message都是deliver到某个Consumer.在这篇文章中,我们将会将同一个Message deliver到多个Consumer中.这个模式也被成为 "pub ...

  9. RabbitMQ消息队列(九):Publisher的消息确认机制

    在前面的文章中提到了queue和consumer之间的消息确认机制:通过设置ack.那么Publisher能不到知道他post的Message有没有到达queue,甚至更近一步,是否被某个Consum ...

随机推荐

  1. Sharepoint学习笔记—习题系列--70-576习题解析 -&lpar;Q6-Q8&rpar;

    Question 6  You are designing a SharePoint 2010 solution that allows users to enter address informat ...

  2. GCT考试如何准备

    备战考试篇 回首连续的3个月的那段复习过程,感受颇多颇深!以下就各科复习,我谈谈自己的感受和经验: 语文复习: 语文主要是考察你的文学功底和素养以及已经具备的工作生活的常识.从03,04两年的考试真题 ...

  3. 在ios中解析json数据

    刚刚下午那会 弄了个 解析 xml  demo的小例子,本想着json也挺复杂 弄还是 不弄,但是简单的看了下 发现挺简单 考虑了很久,还是写上来吧,毕竟json用得太多了,而且算是自己的积累吧,毕竟 ...

  4. &lbrack;SetPropertiesRule&rsqb;&lbrace;Server&sol;Service&sol;Engine&sol;Host&sol;Context&rcub; Setting property &&num;39&semi;source&&num;39&semi; to &&num;39&semi;org&period;eclipse&period;js

    解决办法: 双击server,勾选上[Server Options]里面的[Publish module contexts to separte XML files],如下图即可.

  5. js时间戳转日期

    //时间戳转日期 2017-04-30 13:20 //type=1--> 2017-04-30 13:20 //type=2-->2018年08月 //type=3-->2018- ...

  6. 通过源码分析View的测量

    要理解View的测量,首先要了解MeasureSpec,系统在测量view的宽高时,要先确定MeasureSpec. MeasureSpec(32为int值)由两部分组成: SpecMode(高2位) ...

  7. Less 创建css3动画&commat;keyframes函数

    封装: /** * animation */ .keyframes (@prefix,@name,@content) when (@prefix=def) { @keyframes @name { @ ...

  8. maven添加jetty插件,同时运行多个实例

    <plugins> <!-- jetty插件 --> <plugin> <groupId>org.eclipse.jetty</groupId&g ...

  9. 体会 git 之优越性

    既生瑜,何生亮.已有subversion,何需git?先有firefox叱咤一时,何需chrome来搅局? 原本以为之前的解决方案已经能够满足现时的需求,但这是真正的事实吗?直到新颖的工具降临,才惊叹 ...

  10. 【Scala】Scala-None-null引发的血案

    Scala-None-null引发的血案 Overview - Spark 2.2.0 Documentation Spark Streaming - Spark 2.2.0 Documentatio ...