这是我参与8月更文挑战的第22天,活动详情查看:8月更文挑战
简介
rabbitmq是开源的消息队列之一,使用erlang开发而成
- 消息:又名消息传递,是指程度之间使用消息数据进行通信,而不是程序直接调用程序
- 队列: 排队的意思,消息按照顺序先进先出,依次进行
应用场景
- 异步处理: 非实时返回的结果,通过其他方法返回,比如自助购物柜,先购物,之后再消息通知结算
- 应用解耦: 服务之间相互调用通知,可以通用消息解耦,减轻接口压力
- 流量削峰: 一般在秒杀活动中,选择性扔掉部分请求,按照服务能力接受多少请求
安装
为了快捷方便,这里使用docker快速安装, 往期docker快速安装软件传送门: juejin.cn/post/684490…
安装的时候指定可开启 UI界面的镜像(rabbitmq:3.8.5-management),使用变量设置管理员账密
docker run -itd --rm --hostname rabbitmq-server \
-v /data/docker/rabbitmq:/var/lib/rabbitmq \
-e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin \
-p 15672:15672 -p 5672:5672 rabbitmq:3.8.5-management
客户端
rabbitmq golang的客户端以前是 github.com/streadway/a… 维护。现在交给官方维护了,名字也改了。
官方客户端: github.com/rabbitmq/am…
该客户端非常成熟,是一个十多年的项目,且一直在维护,目前支持到3.9.3的mq版本,上面安装的3.8版本mq肯定也是没问题的
例子
以下示例来自官网,部分方法经过了改造
官网示例地址: github.com/rabbitmq/ra…
全局变量
避免代码冗余和解藕,这里将一些变量和方法通用化或全局命名
var (
conn *amqp.Connection
err error
)
func init() {
conn, err = amqp.Dial("amqp://admin:admin@ip:5672/")
if err != nil {
log.Fatal("连接mq失败: ", err)
}
}
生产消费
一进一出,一对一的工作流程,这是一种简单的消息队列模式
图中“P”是我们的生产者,“C”是我们的消费者。中间的盒子是一个队列——RabbitMQ 代表消费者保留的消息缓冲区。 --图片等来源于rabbitmq官方
为了方便测试,这里把生产和消息,写成两个函数,直接main方法运行
发送消息
发送一条消息,队列名为hello,如果没有指定mq的虚拟机,默认是/
func send() {
ch, _ := conn.Channel()
defer ch.Close()
//队列配置
q, _ := ch.QueueDeclare("hello", false, false, false, false, nil)
body := "Hello World!"
//发送消息
err = ch.Publish("", q.Name, false, false, amqp.Publishing{
ContentType: "test/plain",
Body: []byte(body),
})
if err != nil {
log.Println("发送消息失败: ", err)
}
log.Println("发产达消息成功,消息内容: ", body)
}
接收消息
使用协程来接收消息,因为只有一条,很快就读完了,但是程序不会退出,因为使用了一个无缓冲chan,且没有关闭它。如果想消费完就自动退出,注释掉forever既可
func receive() {
ch, _ := conn.Channel()
defer ch.Close()
//队列配置
q, _ := ch.QueueDeclare("hello", false, false, false, false, nil)
//消费消息
msgs, _ := ch.Consume(q.Name, "", true, false, false, false, nil)
forever := make(chan bool)
//启动一个协程,一个无参函数
go func() {
for d := range msgs {
log.Printf("接收到的消息是: %s",d.Body)
}
}()
log.Printf("等待消息,退出请按 Ctrl+c")
//阻塞等待
<- forever
}
任务队列
请求非常密集时,可以将任务封装成消息发送到队列,然后多个worker同时共享一个队列,处理完后返回
多个消费者共享一个工作队列 ---图片来源于rabbitmq官网
多任务需要打包,启动时传入参数,否则默认 消息就是hello,在bodyFrom函数中定义
go run newTask.go First message.
go run newTask.go Second message..
go run newTask.go Third message...
go run newTask.go Fourth message....
go run newTask.go Fifth message.....
多任务
func newTask() {
ch, _ := conn.Channel()
defer ch.Close()
q, _ := ch.QueueDeclare("task_queue", true, false, false, false, nil)
body := bodyFrom(os.Args)
log.Println(body)
err := ch.Publish("", q.Name, false, false, amqp.Publishing{
DeliveryMode: amqp.Persistent,
ContentType: "test/plain",
Body: []byte(body),
})
if err != nil {
log.Println("发送消息失败: ", err)
}
log.Println("发送消息成功,消息内容: ", body)
}
多消费者需要在idea中启动多个,否则无法体现,在启动2个worker之后,会发现消息是按轮训过来了,消息会按照顺序一条给一台。2个worker总会收到相同数量消息数(偏差1),这是因为消息模式设置的amqp.Persistent
在消费后配置.ack(false) 是手动消息确认机制,确保消费,如果worker挂掉,消息没有处理,将交由另一个worker处理,自动确认机制需要在Consume方法第三方参数传入true
多消费者
func worker() {
ch, _ := conn.Channel()
defer ch.Close()
q, _ := ch.QueueDeclare("task_queue", true,false,false,false,nil,)
_ = ch.Qos(1, 0, false)
msgs, _ := ch.Consume(q.Name, "", false, false, false, false, nil)
forever := make(chan bool)
go func() {
for d := range msgs {
log.Printf("接收消息内容: %s", d.Body)
dotCount := bytes.Count(d.Body,[]byte("."))
t := time.Duration(dotCount)
time.Sleep(t * time.Second)
log.Printf("完成")
d.Ack(false)
}
}()
<- forever
}
发布订阅
rabbitmq完整消息传递模型:
- 生产者从不直接向队列发送任何消息
- 生产者只能将消息发送到交换机
- 交机换接收生产者的消息,并推送到队列
交机类型: direct、topic、headers、fanout
fanout类型生产者
前面都是声明队列,现在是调用声明交换的方法
这种方式类似广播,只要监听该交换机的消费者都可以收到消息
func emitLog() {
ch, _ := conn.Channel()
defer ch.Close()
_ = ch.ExchangeDeclare("logs", "fanout", true, false, false, false, nil)
body := bodyFrom(os.Args)
err = ch.Publish("logs", "", false, false, amqp.Publishing{ContentType: "test/plain", Body: []byte(body)})
if err != nil {
log.Println("发送消息失败: ", err)
}
log.Println("发送消息成功,消息内容: ", body)
}
消费者
这里主要是将队列绑定到交换机上。
消费者需要先启动监听,然后再启动生产者,否则消息因为没有监听而被安全的丢弃
允许多个worker同时启动临听同一个交换机,消费同样的数据
func reveiveLogs() {
ch, _ := conn.Channel()
defer ch.Close()
_ = ch.ExchangeDeclare("logs", "fanout", true, false, false, false, nil)
q, _ := ch.QueueDeclare("", false, false, true, false, nil)
_ = ch.QueueBind(q.Name, "", "logs", false, nil)
msgs, _ := ch.Consume(q.Name, "", true, false, false, false, nil)
forever := make(chan bool)
go func() {
for d := range msgs {
log.Printf("[x] %s", d.Body)
}
}()
<-forever
}
运行之后可以在管理页面看到相关信息
路由
rabbitmq中的路由是将消息从交换机分发到队列的一种过程,根据key来决定路由的分发
Q1和Q2队列绑定交换X, Q1队列绑定key为orage,Q2绑定了两个key,分别为black,green 如果其他key的消息将被丢弃---图来源于官网
还有一种绑定类似于fanout交换,等于在direct交换上实现了fanout交换 ,见下图---图来源于官网
生产者
消息生产可以写成循环,多跑几次
func emitLogDirect() {
ch, _ := conn.Channel()
defer ch.Close()
_ = ch.ExchangeDeclare("logs_direct","direct",true,false,false,false,nil)
body := bodyFrom(os.Args)
err = ch.Publish("logs_direct","green",false,false,amqp.Publishing{ContentType:"text/plan",Body:[]byte(body)})
if err != nil {
log.Println("发送消息失败: ", err)
}
log.Println("发送消息成功,消息内容: ", body)
_ = ch.Publish("logs_direct","black",false,false,amqp.Publishing{ContentType:"text/plan",Body:[]byte(body)})
_ = ch.Publish("logs_direct","orange",false,false,amqp.Publishing{ContentType:"text/plan",Body:[]byte(body)})
}
消费者
消费者需要先于生产者启动,以监控logs_direct交换,否则消息自动丢弃, 另外下列消者的key只有green和black,所有orange会被丢弃
func reveiveLogsDirect() {
ch, _ := conn.Channel()
defer ch.Close()
_ = ch.ExchangeDeclare("logs_direct","direct",true,false,false,false,nil)
q, _ := ch.QueueDeclare("", false, false, true, false, nil)
_ = ch.QueueBind(q.Name, "green", "logs_direct", false, nil)
_ = ch.QueueBind(q.Name, "black", "logs_direct", false, nil)
msgs, _ := ch.Consume(q.Name, "", true, false, false, false, nil)
forever := make(chan bool)
go func() {
for d := range msgs {
log.Printf(" [x] %s", d.Body)
}
}()
<-forever
}
管理界面可以看到 logs_direct 绑定的Routing key
总结
-
队列模式等虽然可以实现,但不是rabbitmq的消息传递核心思想
-
fanout交换模式适合广播,通知所有监控者,收到同样的消息
-
direct交换模式配合路由,可以灵活的定义哪些消费者收哪些消息,或者只收自已需要的消息
-
还有一种文中没有提及,是topic交换模式,它可以更加灵活,除了支持routing key还支持消息内容分隔或特定字符串来识别,从而进行消费