消息队列之RabbitMQ(二)—RabbitMQ 的应用

669 阅读2分钟

Docker安装部署RabbitMQ

查询RabbitMQ镜像

 docker search rabbitmq:management

注:此处是获取management版本的,这种镜像才会有管理界面,如果是获取last版本的没有管理界面

拉取RabbitMQ镜像

docker pull rabbitmq:management

拉取成功之后接下来就是运行RabbitMQ镜像了

运行镜像

docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management

参数说明:

  • -d:后台运行容器,返回容器id
  • -p:主机端口和容器端口之间的映射,格式为:主机端口:容器端口
  • --name:指定容器运行的名称

镜像运行成功之后就可以在主机上打开管理界面了,管理界面的地址为:http://主机Ip:15762,初次登录的账号和密码都是guest

登录成功的界面如下所示:

ui.jpg

RabbitMQ的应用

RabbitMQ支持五种消息模型,不同的模型就代表者不同的消息规则,不同的应用场景;这篇文章RabbitMQ介绍对于RabbitMQ的模型已经有了一个简单的介绍,下面主要是RabbitMQ的应用

简单模型(Hello World

简单模型就是只有一个生产者,一个队列和一个消费者,下面是使用go实现的代码示例:

send.go

package main
​
import (
    "github.com/streadway/amqp"
    "log"
)
​
func main(){
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    failOnError(err, "Failed to connect to RabbitMQ")
    defer conn.Close()
​
    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")
    
    body := "Hello World!"
    err = ch.Publish(
      "",     // exchange
      q.Name, // routing key
      false,  // mandatory
      false,  // immediate
      amqp.Publishing {
        ContentType: "text/plain",
        Body:        []byte(body),
      })
    failOnError(err, "Failed to publish a message")
    log.Printf(" [x] Sent %s\n", body)
}
​
func failOnError(err error, msg string) {
    if err != nil {
        log.Printf("%s: %s", msg, err)
    }
}

receive.go

package main
​
import (
    "github.com/streadway/amqp"
    "log"
)
​
func main(){
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    failOnError(err, "Failed to connect to RabbitMQ")
    defer conn.Close()
​
    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")
​
    var forever chan struct{}
​
    go func() {
        for d := range msgs {
            log.Printf("Received a message: %s", d.Body)
        }
    }()
​
    log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
    <-forever
}
​
func failOnError(err error, msg string) {
    if err != nil {
        log.Printf("%s: %s", msg, err)
    }
}

注:简单模型当中,发送消息是直接发送到队列当中,所以在send.go中发送消息时的routingkey=queueName

模型图:

hello-world.jpg

Worker模型

Worker模型和简单模型的代码几乎是相同的,因为在worker模型中也是只有一个生产者,但是有多个消费者,所以只需要将消费者的代码产生多个实例就好了

模型图:

worker.jpg

Worker模型与简单模型不同之处在于出现了多个消费者,那么就需要有一个消息分发策略了,默认的消息分发策略就是轮询,这种模型下多个消费者消费的消息个数都是差不多的

但是还有一种分发策略为Fair Dispatch(公平分发),这种分发需要指定一个Prefetch(预获取值),这个值保证了该消费者一次只能消费固定数量的消息,只有消费的这些消息消费完成才会继续获得消息

代码示例如下:

send.go

package main
​
import (
        "log"
        "os"
        "strings"
​
        amqp "github.com/rabbitmq/amqp091-go"
)
​
func failOnError(err error, msg string) {
        if err != nil {
                log.Panicf("%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()
​
        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")
​
        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) < 2) || os.Args[1] == "" {
                s = "hello"
        } else {
                s = strings.Join(args[1:], " ")
        }
        return s
}

receive.go

package main
​
import (
        "bytes"
        "log"
        "time"
​
        amqp "github.com/rabbitmq/amqp091-go"
)
​
func failOnError(err error, msg string) {
        if err != nil {
                log.Panicf("%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()
​
        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")
​
    
       // 通过此处指定 prefetch 的值
        err = ch.Qos(
                1,     // prefetch count
                0,     // 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")
​
        var forever chan struct{}
​
        go func() {
                for d := range msgs {
                        log.Printf("Received a message: %s", d.Body)
                        dotCount := bytes.Count(d.Body, []byte("."))
                        t := time.Duration(dotCount)
                        time.Sleep(t * time.Second)
                        log.Printf("Done")
                        d.Ack(false)
                }
        }()
​
        log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
        <-forever
}

参数说明:

  • prefetch count:指定一次消费的消息个数
  • prefetch size:指定一次消费的消息大小
  • global:如果为true,则该规则应用到当前连接所有channel上的所有Consumer
发布/订阅(Public/Subscribe)

前面的模型都是直接将消息发送到消息队列,而在发布订阅模型中引入了一个新的组件:交换机(Exchange),消息首先会被发送到交换机,然会由交换机再发送到消息队列当中,模型图如下:

02.png

那为什么需要这个交换机呢?交换机是如何知道将消息发送哪个队列呢

首先,如果不使用交换机那么当需要将一个消息发送给多个队列时,你就需要将一条消息发送多次,并且每增加一个队列时都需要再重新改代码,这样对扩展不友好;而使用交换机就可以一次将一条消息同时发送到多个队列当中,同时也可以兼容新增的应用和队列

其次,交换机是根据一个RoutingKey来匹配消息队列的,匹配成功之后就会将消息发送到对应的消息队列当中,如下面图所示:

03.png

RabbitMQ中提供了四种交换机类型,分别是:direct,topic,header,fanout;下面来首先来介绍fanout

fanout exchange

这种类型比较简单,它是广播所有的消息到所有绑定到这个exchange的消息队列中,下面是具体的代码示例:

sender.go

package main
​
import (
    "log"
    "os"
    "strings""github.com/streadway/amqp"
)
​
func failOnError(err error, msg string) {
    if err != nil {
        log.Panicf("%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()
​
    ch, err := conn.Channel()
    failOnError(err, "Failed to open a channel")
    defer ch.Close()
​
    err = ch.ExchangeDeclare(
        "logs",   // name
        "fanout", // type
        true,     // durable
        false,    // auto-deleted
        false,    // internal
        false,    // no-wait
        nil,      // arguments
    )
    failOnError(err, "Failed to declare an exchange")
​
    body := bodyFrom(os.Args)
    err = ch.Publish(
        "logs", // exchange
        "",     // routing key
        false,  // mandatory
        false,  // immediate
        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) < 2) || os.Args[1] == "" {
        s = "hello"
    } else {
        s = strings.Join(args[1:], " ")
    }
    return s
}

receive.go

package main
​
import (
    "log""github.com/streadway/amqp"
)
​
func failOnError(err error, msg string) {
    if err != nil {
        log.Panicf("%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()
​
    ch, err := conn.Channel()
    failOnError(err, "Failed to open a channel")
    defer ch.Close()
​
    err = ch.ExchangeDeclare(
        "logs",   // name
        "fanout", // type
        true,     // durable
        false,    // auto-deleted
        false,    // internal
        false,    // no-wait
        nil,      // arguments
    )
    failOnError(err, "Failed to declare an exchange")
​
    q, err := ch.QueueDeclare(
        "",    // name
        false, // durable
        false, // delete when unused
        true,  // exclusive
        false, // no-wait
        nil,   // arguments
    )
    failOnError(err, "Failed to declare a queue")
​
    err = ch.QueueBind(
        q.Name, // queue name
        "",     // routing key
        "logs", // exchange
        false,
        nil,
    )
    failOnError(err, "Failed to bind 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")
​
    var forever chan struct{}
​
    go func() {
        for d := range msgs {
            log.Printf(" [x] %s", d.Body)
        }
    }()
​
    log.Printf(" [*] Waiting for logs. To exit press CTRL+C")
    <-forever
}
direct exchange

前面的fanout exchange会将消息广播到所有绑定到它的队列上面,但是如果我们不想广播消息,而是只发送消息到指定的消息队列呢

那么此时就可以使用direct exchange了,direct exchange就是指定一个RoutingKey,然会交换机只会将消息发送到指定的消息队列当中

注:为了避免routingkey混淆,我们将Channel.Publish参数中的routingKey称之为BindingKey

下面是使用direct exchangego代码示例:

send.go

package main
​
import (
    "log"
    "os"
    "strings""github.com/streadway/amqp"
)
​
func failOnError(err error, msg string) {
    if err != nil {
        log.Panicf("%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()
​
    ch, err := conn.Channel()
    failOnError(err, "Failed to open a channel")
    defer ch.Close()
​
    err = ch.ExchangeDeclare(
        "logs_direct", // name
        "direct",      // type
        true,          // durable
        false,         // auto-deleted
        false,         // internal
        false,         // no-wait
        nil,           // arguments
    )
    failOnError(err, "Failed to declare an exchange")
​
    body := bodyFrom(os.Args)
    err = ch.Publish(
        "logs_direct",         // exchange
        severityFrom(os.Args), // routing key
        false, // mandatory
        false, // immediate
        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) < 3) || os.Args[2] == "" {
        s = "hello"
    } else {
        s = strings.Join(args[2:], " ")
    }
    return s
}
​
func severityFrom(args []string) string {
    var s string
    if (len(args) < 2) || os.Args[1] == "" {
        s = "info"
    } else {
        s = os.Args[1]
    }
    return s
}

receive.go

package main
​
import (
    "log"
    "os""github.com/streadway/amqp"
)
​
func failOnError(err error, msg string) {
    if err != nil {
        log.Panicf("%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()
​
    ch, err := conn.Channel()
    failOnError(err, "Failed to open a channel")
    defer ch.Close()
​
    err = ch.ExchangeDeclare(
        "logs_direct", // name
        "direct",      // type
        true,          // durable
        false,         // auto-deleted
        false,         // internal
        false,         // no-wait
        nil,           // arguments
    )
    failOnError(err, "Failed to declare an exchange")
​
    q, err := ch.QueueDeclare(
        "",    // name
        false, // durable
        false, // delete when unused
        true,  // exclusive
        false, // no-wait
        nil,   // arguments
    )
    failOnError(err, "Failed to declare a queue")
​
    if len(os.Args) < 2 {
        log.Printf("Usage: %s [info] [warning] [error]", os.Args[0])
        os.Exit(0)
    }
    for _, s := range os.Args[1:] {
        log.Printf("Binding queue %s to exchange %s with routing key %s",
            q.Name, "logs_direct", s)
        err = ch.QueueBind(
            q.Name,        // queue name
            s,             // routing key
            "logs_direct", // exchange
            false,
            nil)
        failOnError(err, "Failed to bind 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")
​
    var forever chan struct{}
​
    go func() {
        for d := range msgs {
            log.Printf(" [x] %s", d.Body)
        }
    }()
​
    log.Printf(" [*] Waiting for logs. To exit press CTRL+C")
    <-forever
}
topic exchange

前面已经介绍过两种交换机模型了,一种是广播,一种是单播,那有没有组播呢?下面介绍的topic exchange就是实现组播的功能了

topic exchange提供了两个特殊的字符:*#,用来匹配多个队列,从而将消息发送到多个队列当中

  • *:星号可以匹配一个单词,比如:log.*可以匹配 log.err,log.info等,但是不能匹配log.err.app
  • #:井号可以匹配零个或多个单词,比如:log.#可以匹配log.err,log.info同时也可以匹配log.err.app

下面是使用go语言实现的代码示例:

sender.go

package main
​
import (
    "log"
    "os"
    "strings""github.com/streadway/amqp"
)
​
func failOnError(err error, msg string) {
    if err != nil {
        log.Panicf("%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()
​
    ch, err := conn.Channel()
    failOnError(err, "Failed to open a channel")
    defer ch.Close()
​
    err = ch.ExchangeDeclare(
        "logs_topic", // name
        "topic",      // type
        true,         // durable
        false,        // auto-deleted
        false,        // internal
        false,        // no-wait
        nil,          // arguments
    )
    failOnError(err, "Failed to declare an exchange")
​
    body := bodyFrom(os.Args)
    err = ch.Publish(
        "logs_topic",          // exchange
        severityFrom(os.Args), // routing key
        false, // mandatory
        false, // immediate
        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) < 3) || os.Args[2] == "" {
        s = "hello"
    } else {
        s = strings.Join(args[2:], " ")
    }
    return s
}
​
func severityFrom(args []string) string {
    var s string
    if (len(args) < 2) || os.Args[1] == "" {
        s = "anonymous.info"
    } else {
        s = os.Args[1]
    }
    return s
}

receive.go

package main
​
import (
    "log"
    "os""github.com/streadway/amqp"
)
​
func failOnError(err error, msg string) {
    if err != nil {
        log.Panicf("%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()
​
    ch, err := conn.Channel()
    failOnError(err, "Failed to open a channel")
    defer ch.Close()
​
    err = ch.ExchangeDeclare(
        "logs_topic", // name
        "topic",      // type
        true,         // durable
        false,        // auto-deleted
        false,        // internal
        false,        // no-wait
        nil,          // arguments
    )
    failOnError(err, "Failed to declare an exchange")
​
    q, err := ch.QueueDeclare(
        "",    // name
        false, // durable
        false, // delete when unused
        true,  // exclusive
        false, // no-wait
        nil,   // arguments
    )
    failOnError(err, "Failed to declare a queue")
​
    if len(os.Args) < 2 {
        log.Printf("Usage: %s [binding_key]...", os.Args[0])
        os.Exit(0)
    }
    for _, s := range os.Args[1:] {
        log.Printf("Binding queue %s to exchange %s with routing key %s",
            q.Name, "logs_topic", s)
        err = ch.QueueBind(
            q.Name,       // queue name
            s,            // routing key
            "logs_topic", // exchange
            false,
            nil)
        failOnError(err, "Failed to bind 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")
​
    var forever chan struct{}
​
    go func() {
        for d := range msgs {
            log.Printf(" [x] %s", d.Body)
        }
    }()
​
    log.Printf(" [*] Waiting for logs. To exit press CTRL+C")
    <-forever
}
header exchange

header exchangetopic exchange非常相似,只不过它是通过header中的参数来路由消息的,而会自动忽略掉routingKey

header exchange中,如果header中的值和绑定的值相互匹配,那么就会被消息发送到这个队列当中去了;在队列和交换机绑定中提供了一个参数为x-match,它有两个值分别是:all,any,这个参数表明是否header中所有的值都匹配还是最少匹配一个

  • all:表示所有header中的key-val都必须匹配
  • any:表示所有header中最少匹配一个

下面是使用go实现的代码示例:

send.go

package main
​
import (
    "github.com/streadway/amqp"
    "time"
)
​
func main() {
    conn, _ := amqp.Dial("amqp://user:password@localhost:ip/vhost")
    defer conn.Close()
    
    ch, _ := conn.Channel()
    defer ch.Close()
    
    body := "Hello World! " + time.Now().Format("2006-01-02 15:04:05")
    ch.ExchangeDeclare("logs", amqp.ExchangeHeaders, true, false, false, false, nil)
​
    ch.Publish(
        "logs", // exchange 
        "log.err",   // routing key
        false,         // mandatory
        false,         // immediate
        amqp.Publishing{
            ContentType: "text/plain",
            Body:        []byte(body),
            Headers:     amqp.Table{"x-match": "any", "mail": "test@qq.com", "author": "sendi"}, // 头部信息 any:匹配一个即可 all:全部匹配
            //Expiration:  "3000",
        })
}

receive.go

package main
​
import (
    "github.com/streadway/amqp"
    "log"
)
​
func main() {
    conn, _ := amqp.Dial("amqp://user:password@localhost:ip/vhost")
    defer conn.Close()
​
    ch, _ := conn.Channel()
    defer ch.Close()
​
    ch.QueueDeclare("log.app", true, false, true, false, nil)
    ch.ExchangeDeclare("logs", amqp.ExchangeHeaders, true, false, false, false, nil)
​
​
    ch.QueueBind(
        "log.app",    // queue name
        "",   // 此处即使写了 routingkey 也会被自动忽略
        "logs", // exchange
        false,
        amqp.Table{"mail": "test@qq.com"}, //  头部信息 any:匹配一个即可 all:全部匹配
    )
​
    //监听
    msgs, _ := ch.Consume(
        "log.app", // queue name
        "",         // consumer
        true,       // auto-ack
        false,      // exclusive
        false,      // no-local
        false,      // no-wait
        nil,        // args
    )
​
    forever := make(chan bool)
​
    go func() {
        for d := range msgs {
            //println("tset")
            log.Printf(" [x] %s", d.Body)
        }
    }()
​
    <-forever
}

参考:www.rabbitmq.com/getstarted.…