一、Work Queues 是什么
Work Queues 任务模型:多个消费者绑定同一个队列,共同消费队列中的消息。 场景:消息量较大,单个消费者处理不过来,启动多个消费者分担任务,提升消息处理速度。
核心特点
- 多个消费者绑定到一个队列,可以加快消息处理速度
- 同一条消息只会被一个消费者处理,不会重复消费
- 默认策略:轮询分发。MQ 按顺序轮流把消息发给消费者,不管消费者处理快慢。
- 例:消费者 1 拿到 1、3、5...;消费者 2 拿到 2、4、6...,消息均分
- 优化策略:设置
prefetch = 1(底层basicQos)- 控制消费者预取消息数量:处理完一条、ACK 确认后,才获取下一条消息
- 实现能者多劳:处理快的消费者拿到更多消息,解决轮询模式下慢消费者消息堆积问题
适用场景:消息发送速度超过单个消费者处理上限,出现消息堆积。此时给队列绑定多个消费者并行消费。
二、实验需求
模拟 WorkQueue,一个队列绑定多个消费者:
- RabbitMQ 网页控制台创建队列:
work.queue - 生产者 publisher:1 秒内发送 50 条消息到
work.queue,每发一条休眠 20ms - 消费者 consumer:两个监听者同时监听
work.queue- 消费者 1:处理速度快,每秒处理 50 条
- 消费者 2:处理速度慢,每秒处理 5 条
三、代码实战(Go + streadway/amqp)
1. 生产者代码(发送 50 条消息)
Go
package main
import (
"fmt"
"log"
"time"
"github.com/streadway/amqp"
)
func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
}
}
func main() {
// 连接RabbitMQ
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "连接RabbitMQ失败")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "打开通道失败")
defer ch.Close()
queueName := "work.queue"
// 声明队列(不存在则创建)
_, err = ch.QueueDeclare(
queueName,
false,
false,
false,
false,
nil,
)
failOnError(err, "声明队列失败")
// 循环发送50条消息,每次发送休眠20ms
for i := 1; i <= 50; i++ {
msg := fmt.Sprintf("hello, worker, message_%d", i)
err = ch.Publish(
"", // 默认交换机
queueName, // 队列名称
false,
false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(msg),
},
)
failOnError(err, "发送消息失败")
fmt.Printf("发送消息: %s\n", msg)
time.Sleep(20 * time.Millisecond)
}
fmt.Println("50条消息全部发送完成")
}
2. 消费者代码(双消费者 + prefetch=1 + 手动 ACK)
关键点:使用
ch.Qos(1,0,false)设置 prefetch=1;必须关闭自动 ACKautoAck=false,处理完成手动调用d.Ack(false)。
Go
package main
import (
"fmt"
"log"
"os"
"time"
"github.com/streadway/amqp"
)
func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
}
}
// 消费者1:处理快,每秒50条(几乎无休眠)
func consumeWorkQueue1(ch *amqp.Channel) {
msgs, err := ch.Consume(
"work.queue",
"",
false, // autoAck=false 关闭自动确认,必须手动ACK
false,
false,
false,
nil,
)
failOnError(err, "注册消费者1失败")
go func() {
for d := range msgs {
fmt.Printf("消费者1 收到 work.queue消息:【%s】\n", d.Body)
// 模拟处理:几乎不耗时
time.Sleep(20 * time.Millisecond)
d.Ack(false) // 手动确认消息处理完成
}
}()
fmt.Println("消费者1 已启动")
}
// 消费者2:处理慢,每秒5条(模拟耗时任务),输出到stderr红色
func consumeWorkQueue2(ch *amqp.Channel) {
msgs, err := ch.Consume(
"work.queue",
"",
false, // autoAck=false
false,
false,
false,
nil,
)
failOnError(err, "注册消费者2失败")
go func() {
for d := range msgs {
fmt.Fprintf(os.Stderr, "消费者2 收到 work.queue消息:【%s】\n", d.Body)
// 模拟慢任务,处理耗时久
time.Sleep(200 * time.Millisecond)
d.Ack(false) // 手动确认
}
}()
fmt.Println("消费者2 已启动")
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "连接RabbitMQ失败")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "创建channel失败")
defer ch.Close()
// =========核心配置 prefetch=1 =========
err = ch.Qos(
1, // prefetchCount:预取1条消息
0, // prefetchSize:0代表不限制消息字节大小
false, // global=false:仅当前channel生效
)
failOnError(err, "设置Qos prefetch失败")
// 声明队列
_, err = ch.QueueDeclare(
"work.queue",
false,
false,
false,
false,
nil,
)
failOnError(err, "声明队列失败")
// 启动两个消费者协程
go consumeWorkQueue1(ch)
go consumeWorkQueue2(ch)
// 阻塞常驻消费
select {}
}
四、两种分发模式对比
模式 1:默认轮询(不设置 prefetch)
MQ 预先把消息轮流下发给消费者。
- 消息均分:消费者 1 拿 1、3、5...;消费者 2 拿 2、4、6...
- 缺点:不管消费者处理快慢,消息提前分配。慢消费者堆积大量未处理消息,快消费者空闲。
模式 2:prefetch=1 + 手动 ACK(能者多劳)
MQ 每次只给消费者下发 1 条消息。消费者处理完成、ACK 确认之后,MQ 才会推送下一条消息。
- 处理速度快的消费者可以持续领取消息,拿到更多任务
- 解决慢消费者消息积压问题,充分利用消费者算力
五、总结
- Work 模型:多个消费者绑定同一个队列,并行消费,提升处理速度。
- 消息唯一性:一条消息只会被一个消费者消费一次。
- prefetch 机制:控制预取消息数量,配合手动 ACK 实现能者多劳。
- 使用场景:消息生产速度大于单个消费者处理能力,出现消息堆积。