从现在开始开始练习消息队列RabbitMQ
自己练习消息队列的时候,可以使用真机安装RabbitMQ,也可以使用容器安装RabbitMQ,这里使用docker容器。
环境
docker 容器中的RabbitMQ,端口映射5673 , 默认的端口为5672
一:编程第一步,hello world
该案例是一个简单的只有两端:发送-接收
概览
发送方步骤有:
- 连接队列服务器Dial(“amqp://guest:guest@localhost:5673”)
- 队列声明QueueDeclare
- 消息发送Publish
接收方步骤有:
- 连接队列服务器Dial(“amqp://guest:guest@localhost:5673”)
- 队列声明QueueDeclare
- 消息消费Comsume
本阶段两端的配置只有第三个步骤不一样,一个发送一个接收。
1.1:发送消息
import (
"fmt"
"github.com/streadway/amqp"
"log"
)
func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
}
}
func main() {
//guest是默认的账号,一般只有localhost、127.0.0.1可以使用该账号,其它ip使用会报错
conn,err := amqp.Dial("amqp://guest:guest@localhost:5673/")
failOnError(err,"连接队列服务失败!")
ch,err := conn.Channel()
failOnError(err,"获取channel失败!")
defer ch.Close()
//声明队列
//hello发送方队列的名称:发送方接收方可以不一致,只是用来存储当前发送方的数据的
queue,err := ch.QueueDeclare("hello",
false,false,false,false,nil)
//这里第一个参数是交换类型,这里为空表示使用默认的交换exchange
//第二个参数queue.Name是路由键的名称:发送方接收方要一致,消息将会发送到路由键对应的消息队列,
//接收方需要有与这一样的路由键,或者接收方consume第一个队列参数置为"",否则取不到数据;
//接收方的ch.QueueDeclare声明的队列名称需要跟发送方路由键名称一致,表示为创建该路由键对应的队列。
err = ch.Publish("",queue.Name,
false,false,amqp.Publishing{
ContentType: "application/json", //contentType没有严格要求
Body: []byte("hello世界"),
})
failOnError(err,"消息发布失败")
}
1.2:接收消息
import (
"fmt"
"github.com/streadway/amqp"
"log"
)
func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
}
}
func main() {
conn,err := amqp.Dial("amqp://guest:guest@localhost:5673")
failOnError(err,"连接失败")
ch,err := conn.Channel()
failOnError(err,"打开channel失败")
queue,err := ch.QueueDeclare("hello",//队列的名称:发送方接收方不需要一致,但要跟发送方的路由键保持一致
false,false,false,false,nil)
failOnError(err,"打开队列失败")
//第一个参数queue.Name表示消费哪个队列:要跟发送方的路由键保持一致,或者""消费所有队列。
msgs,err := ch.Consume(queue.Name,
"",true,//该参数为:AutoAck,设置为true之后,表示自动确认。RabbitMQ就认为为消息发送之后就被处理,所以直接将消息从缓冲区删除,消费者异常时,如果消息未被处理,则消息丢失。false时,如果消费者出现异常,重新启动后,会将缓冲区的消息重发给消费者。
false,false,false,nil)
failOnError(err,"注册消费者失败")
forever := make(chan bool) //设置一个channel,用来阻塞主进程,防止程序终止
go func() {
for msg := range msgs {
fmt.Println(string(msg.Body))
}
}()
fmt.Println("连接成功,等待数据..")
<- forever
}
上面的这是一种最简单的使用消息队列的方式,简单,却不安全。消息的发送可能面临着消息还未完全消费但服务已经奔溃造成消息丢失。
二:消息可靠性与channel预取
为了解决这一种情况我们有几种方法来尽量保证消息的可靠性。
- 消费方AutoAck设置为false,在这种情况下,如果一个消息从发出后,消费方没有使用msg.Ack(false)确认消息被消费的话,该条消息则一直存储在内存中。当消费者因为异常中断后重启,队列将会将缓存中的消息再一次发送给消费者,如果调用Ack确认消息被消费后,该条消息则从缓存中清除。
- 设置AutoAck为false,也只能保证消费者挂掉之后重启消息任然存在,但是如果队列服务挂掉之后,任然会出现消息丢失的情况。所以为了解决这种情况,可以设置持久存储;生产者跟消费者声明队列的时候durable设置为true,amqp.Publishing{}构造发送消息的时候增加一个DeliveryMode: amqp.Persistent属性。设置成这种情况后,一般队列服务挂掉后,大概率会将数据存储到磁盘,重新启动队列服务跟消费者后,队列将重新将之前保存的数据发送给消费者,消费者消费后发送一个Ack确认消费,则消息被从缓存跟磁盘清除。
- 一般消息队列只是简单的将入队的消息转发给消费者,它不会管当前消费者是否有未确认的消息。为了解决这种情况,我们可以在消费者端设置一个预取消息数量为1,当本消费者还未确认上条消息时,队列将会把新入队的消息转发给其它空闲消费者。
注意:手动确认ack,nack,reject一般都是与channel预取结合使用,确认一些消息后才继续处理。
接收方Consume方法中的autoAck设置确认模式,true被称为自动模式,false表示手动确认模式,消息队列能将大量的消息发送给某个消费者,所以,一般如果没有使用手动确认消息时的消费者应当是具备高速处理业务的能力。
Ack(false)表示只确认当前这条channel中的当前消息,如果设置为true,则表示把当前channel中所有未确认的消息全部设置为确认。
代码:
发送方:
func main() {
conn,err := amqp.Dial("amqp://guest:guest@localhost:5673/")
failOnError(err,"连接队列服务失败!")
ch,err := conn.Channel()
failOnError(err,"获取channel失败!")
defer ch.Close()
//由于前面队列中已经有一个hello队列了,所以更改配置后需要重新定义一个队列hello_
queue,err := ch.QueueDeclare("hello_",
true,false,false,false,nil)
fmt.Println(queue.Name)
err = ch.Publish("",queue.Name,
false,false,amqp.Publishing{
DeliveryMode: amqp.Persistent,
ContentType: "application/json",
Body: []byte("hello世界"),
})
failOnError(err,"消息发布失败")
}
接收方---可以有多个
func main() {
conn,err := amqp.Dial("amqp://guest:guest@localhost:5673")
failOnError(err,"连接失败")
ch,err := conn.Channel()
failOnError(err,"打开channel失败")
queue,err := ch.QueueDeclare("hello_",
true,false,false,false,nil)
failOnError(err,"打开队列失败")
//设置预取消息数量为100,在上条消息未确认时,将不再下发消息到该消费者
//预取数量要根据场景反复测试可能才会比较准确
err = ch.Qos(
100, // prefetch count
0, // prefetch size
false, // global
)
msgs,err := ch.Consume(queue.Name,"",false,false,false,false,nil)
failOnError(err,"注册消费者失败")
forever := make(chan bool)
go func() {
for msg := range msgs {
fmt.Println(string(msg.Body))
//Ack(false)表示只确认当前这条channel中的当前消息,如果设置为true,
//则表示把当前channel中所有未确认的消息全部设置为确认
msg.Ack(false)
}
}()
fmt.Println("连接成功,等待数据..")
<- forever
}
参考:
https://www.rabbitmq.com/tutorials/tutorial-one-go.html