在Go中连接RabbitMQ
RabbitMQ是出色的消息中间件,golang理所当然的也支持了。RabbitMQ是一个很棒的pub-sub系统,并且pub-sub已成为微服务中的主要通信体系结构。在我目前的工作中,我们每天通过Go服务使用RabbitMQ推送数亿个社交媒体帖子。让我们一起来看一下如何使用开源amqp软件包有效地发布和订阅消息 。
1、概述
RabbitMQ需注意的两个主要实体是routing keys 和queues。服务将消息(在本例中为JSON格式)发布到routing keys 。然后RabbitMQ将该消息复制到 订阅该routing key的每个queue中。
订阅服务(使用者)可以一次将消息从queue中拉出。值得一提的是,queue也可以从多个routing key接收消息,但是我们在这里不做讨论。
2、连接
首先,没有理由重新发明轮子。我们将使用streadway提供的amqp包来处理连接细节的细节。在我们的大多数项目中,我们[在项目的内部文件夹中构建一个小Rabbit包。它仅公开了我们项目关心的Rabbit功能。
//internal/rabbit/publish.go // Conn -
type Conn struct {
Channel *amqp.Channel
}
// GetConn -
func GetConn(rabbitURL string) (Conn, error) {
conn, err := amqp.Dial(rabbitURL)
if err != nil {
return Conn{}, err
}
ch, err := conn.Channel()
return Conn{
Channel: ch,
}, err
}
该Conn
结构仅保持与RabbitMQ服务器的连接。我们还将公开一种仅使用连接URI来获得新连接的方法。例如,amqp://username:password@localhost
。
3、发布
发布非常容易,并且是线程安全的现成的。在这里,我们仅公开了使用该连接发布的另一个功能。调用代码仅提供路由密钥和有效负载。
//internal/rabbit/consume.go
// Publish -
func (conn Conn) Publish(routingKey string, data []byte) error {
return conn.Channel.Publish(
// exchange - yours may be different
"events",
routingKey,
// mandatory - we don't care if there I no queue
false,
// immediate - we don't care if there is no consumer on the queue
false,
amqp.Publishing{
ContentType: "application/json",
Body: data,
DeliveryMode: amqp.Persistent,
})
}
这个小型内部程序包的目的是为功能更强大的AMQP程序包设置一些默认值,并控制向我们的应用程序公开哪些功能。例如,如果我们知道我们的应用程序将始终使用events
,并且我们希望设置mandatory
orimmediate
标志,则可以在此处使用。
4、消费
消费比发布要难一些。在这里,我们将使用一个简单的模式,让我们的应用程序提供一个处理程序函数,一个队列,该队列绑定到的routing key,以及如何在单独的goroutine中创建最大数量的处理程序。
//internal/rabbit/consume.go// StartConsumer -
func (conn Conn) StartConsumer(
queueName,
routingKey string,
handler func(d amqp.Delivery) bool,
concurrency int) error {
// create the queue if it doesn't already exist
_, err := conn.Channel.QueueDeclare(queueName, true, false, false, false, nil)
if err != nil {
return err
}
// bind the queue to the routing key
err = conn.Channel.QueueBind(queueName, routingKey, "events", false, nil)
if err != nil {
return err
}
// prefetch 4x as many messages as we can handle at once
prefetchCount := concurrency * 4
err = conn.Channel.Qos(prefetchCount, 0, false)
if err != nil {
return err
}
msgs, err := conn.Channel.Consume(
queueName, // queue
"", // consumer
false, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
if err != nil {
return err
}
// create a goroutine for the number of concurrent threads requested
for i := 0; i < concurrency; i++ {
fmt.Printf("Processing messages on thread %v...\n", i)
go func() {
for msg := range msgs {
// if tha handler returns true then ACK, else NACK
// the message back into the rabbit queue for
// another round of processing
if handler(msg) {
msg.Ack(false)
} else {
msg.Nack(false, true)
}
}
fmt.Println("Rabbit consumer closed - critical Error")
os.Exit(1)
}()
}
return nil
}
如果您担心速度,至少不用担心少100的并发速度,这取决于处理程序的计算和内存密集程度。假设您的处理程序是以线程安全的方式编写的,这是确保您的应用程序使用其所有可用CPU而不受到I / O瓶颈限制。如果应用程序的处理程序非常快(可能不涉及网络或磁盘),则可能需要将prefetch
乘数从4更改为更大的值。prefetchCount告诉Rabbit连接每个请求从服务器检索多少消息。数字越大,等待网络获取每条消息的时间越少。
程序在故障发生时,不论是否重启都应该不受其影响。因此,如果RabbitMQ使用者由于任何原因而失败,这里将使用该os.Exit(1)
命令。日志将被收集,然后重新启动。如果这不适用于您的服务,则可能需要其他更优雅的方法进行处理。
5、测试
//main.go
func main() {
conn, err := rabbit.GetConn("amqp://guest:guest@localhost")
if err != nil {
panic(err)
}
go func() {
for {
time.Sleep(time.Second)
conn.Publish("test-key", []byte(`{"message":"test"}`))
}
}()
err = conn.StartConsumer("test-queue", "test-key", handler, 2)
if err != nil {
panic(err)
}
forever := make(chan bool)
<-forever
}
func handler(d amqp.Delivery) bool {
if d.Body == nil {
fmt.Println("Error, no message body!")
return false
}
fmt.Println(string(d.Body))
return true
}