day3

学习内容

RabbitMQ原理及特性,包括不同类型的exchange,queue的参数意义及性能影响等
使用Locust进行压测
了解“领域驱动设计”这种模式及其战略模式

已有知识

在本地上通过“restful风格的CRUD接口设计”项目在使用 map 存储引擎时使用rabbitmq进行主从同步,但只用到了简单的simple模式
学习了ab 与wrk进行压测,但ab在测试时在windows只能get 在linux上只能get put post 但不能delete,wrk在双系统上均可get post put delete

day3

参考及学习文章:https://www.cnblogs.com/pomelo-lemon/p/11440368.html
(java实现的)

1. simple:如何在go中整合rabbit 抽取api
package models

import (
	"fmt"
	"log"
	"strings"

	"github.com/streadway/amqp"
)

const MQURL = "amqp://guest:guest@127.0.0.1:5672/"

//创建rabbitmq结构体实例
type RabbitMQ struct {
	conn      *amqp.Connection
	channel   *amqp.Channel
	QueueName string
	Exchange  string
	Key       string
	Mqurl     string
}

func NewRabbitMQ(queueName string, Exchange string, key string) *RabbitMQ {
	rabbitmq := &RabbitMQ{QueueName: queueName, Exchange: Exchange, Key: key, Mqurl: MQURL}
	var err error
	rabbitmq.conn, err = amqp.Dial(rabbitmq.Mqurl)
	rabbitmq.failOnErr(err, "创建连接错误")
	rabbitmq.channel, err = rabbitmq.conn.Channel()
	rabbitmq.failOnErr(err, "获取channel失败")
	return rabbitmq
}

//断开channel和connection
func (r *RabbitMQ) Destroy() {
	r.channel.Close()
	r.conn.Close()
}

//错误处理函数
func (r *RabbitMQ) failOnErr(err error, message string) {
	if err != nil {
		log.Fatalf("%s:%s", message, err)
		panic(fmt.Sprintf("%s%s", message, err))
	}
}

//simple模式step1: rabbitmq的实例
func NewRabbitMQSimple(queueName string) *RabbitMQ {
	return NewRabbitMQ(queueName, "", "")
}

//简单模式step简单模式下生产代码
func (r *RabbitMQ) PublishSimple(message string) {
	//申请队列,如果队列不存在会自动创建,如果存在则跳过创建
	//保证队列存在,消息能发送到队列中
	_, err := r.channel.QueueDeclare(
		r.QueueName,
		//是否持久化
		false,
		//是否为自动删除
		false,
		//是否具有排他性
		false,
		//是否阻塞
		false,
		//额外属性
		nil,
	)
	if err != nil {
		fmt.Println(err)
	}
	//发送消息到队列中
	err = r.channel.Publish(
		r.Exchange,
		r.QueueName,
		//如果为true,根据exchange类型和routkey规则,如果无法找到符合条件的队列那么会把发送的消息返回给发送者
		false,
		false,
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(message),
		})
	if err != nil {
		fmt.Println(err)
	}
}

func (r *RabbitMQ) ConsumeSimple() {
	_, err := r.channel.QueueDeclare(
		r.QueueName,
		//是否持久化
		false,
		//是否为自动删除
		false,
		//是否具有排他性
		false,
		//是否阻塞
		false,
		//额外属性
		nil,
	)
	if err != nil {
		fmt.Println(err)
	}
	msgs, err := r.channel.Consume(
		r.QueueName,
		//用来区分多个消费者
		"",
		//是否自动应答
		true,
		//是否具有排他性
		false,
		//如果设置为true,表示不能将同一个connection中发送的消息传递给这个connection中的消费者
		false,
		//队列消费是否阻塞
		false,
		//其他属性
		nil,
	)
	if err != nil {
		fmt.Println(err)
	}
	forever := make(chan bool)
	//启用协程处理消息
	go func() {
		for d := range msgs {
			//实现我们要处理的逻辑函数
			log.Printf("Received a message:%s")
			fmt.Println(string(d.Body))
			stringbody := string(d.Body)
			b := strings.Split(stringbody, "///")
			if b[0] == "add" { //如果表示新增,就往从库中加
				DATA2[b[1]] = b[2]
			} else if b[0] == "edit" {
				DATA2[b[1]] = b[2]
			} else if b[0] == "delete" {
				delete(DATA2, b[1]) // 删除不存在的key,原m不影响
			}
			fmt.Println(DATA2)
		}
	}()

	<-forever
}

在这里插入图片描述
simple简单模式:
在这里插入图片描述
消息产生着§将消息放入队列消息的消费者(consumer) 监听(while) 消息队列,如果队列中有消息,就消费掉,消息被拿走后,自动从队列中删除(隐患 消息可能没有被消费者正确处理,已经从队列中消失了,造成消息的丢失)应用场景:聊天(中间有一个过度的服务器;p端,c端)

2.work queue工作模式

工作队列和单发单收模式比起来,接收端可以有多个,接收端多了以后就会出现数据分配问题,发过来的数据到底该被哪个接收端接收,所以有两种模式:

公平分发:每个接收端接收消息的概率是相等的,发送端会循环依次给每个接收端发送消息,图一是公平分发。

公平派遣:保证接收端在处理完某个任务,并发送确认信息后,RabbitMQ才会向它推送新的消息,在此之间若是有新的消息话,将会被推送到其它接收端,若所有的接收端都在处理任务,那么就会等待,图二为公平派遣
在这里插入图片描述
在这里插入图片描述

  • 公平分发模式下
    send:
package main
 
import (
    "RabbitMQ"
    "strconv"
    "strings"
    "time"
)
 
func main(){
    //第一个参数指定rabbitmq服务器的链接,第二个参数指定创建队列的名字
    send_mq := rabbitMQ.New("amqp://user:password@ip:port/","hello")
    i := 0
    for{
        time.Sleep(1)
        greetings :=  []string{"Helloworld!",strconv.Itoa(i)}
        send_mq.Send(strings.Join( greetings, " "))
        i = i 1
    }
 
}

receive1

package main
 
import (
    rabbitMQ "RabbitMQ"
    "log"
)
 
func  main(){
    //第一个参数指定rabbitmq服务器的链接,第二个参数指定创建队列的名字
 
    receive_mq := rabbitMQ.New("amqp://user:password@ip:port/","hello")
    for{
        //接收消息时,指定
        msgs := receive_mq .Consume()
        go func() {
            for d := range msgs {
                log.Printf("recevie 1 Received a message: %s", d.Body)
            }
        }()
    }
 
 
}

receive2

package main
 
import (
    rabbitMQ "RabbitMQ"
    "log"
)
 
func  main(){
    //第一个参数指定rabbitmq服务器的链接,第二个参数指定创建队列的名字
 
    receive_mq := rabbitMQ.New("amqp://user:password@ip:port/","hello")
    for{
        //接收消息时,指定
        msgs := receive_mq .Consume()
        go func() {
            for d := range msgs {
                log.Printf("recevie 1 Received a message: %s", d.Body)
            }
        }()
    }
 
 
}
  • 公平派遣模式下
    公平派遣模式下发送端与公平分发相同,接收端只需要加一端配置代码

我们可以将预取计数设置为1。这告诉RabbitMQ一次不要给工人一个以上的消息。换句话说,在处理并确认上一条消息之前,不要将新消息发送给工作人员。而是将其分派给不忙的下一个工作程序。

//配置队列参数
func (q *RabbitMQ)Qos(){
    e := q.channel.Qos(1,0,false)
    failOnError(e,"无法设置QoS")
}

####3.发布/订阅模式下(重重重点)
这里会引出前面提及过的重要概念 exchange

exchange的作用就是类似路由器,发送端发送消息需要带有routing key 就是路由键,服务器会根据路由键将消息从交换器路由到队列上去,所以发送端和接收端之间有了中介。

exchange有多个种类:direct,fanout,topic,header(非路由键匹配,功能和direct类似,很少用)。
exchange有多个种类:direct,fanout,topic,header(非路由键匹配,功能和direct类似,很少用

1:n 广播模式:exchange下的fanout exchange,它会将发到这个exchange的消息广播到关注此exchange的所有接收端上

发送端连接到rabbitmq后,创建exchange,需要指定交换机的名字和类型,fanout为广播,然后向此exchange发送消息,其它就不用管了。
接收端的执行流程在程序备注中。注意:广播模式下的exchange是发送端是不需要带路由键的哦。
发送端:(不带路由键)

package main
 
import (
    "RabbitMQ"
    "strconv"
    "strings"
    "time"
)
 
func main(){
    ch := rabbitMQ.Connect("amqp://user:password@ip:port/")
    rabbitMQ.NewExchange("amqp://user:password@ip:port/","exchange1","fanout")
    i := 0
    for{
        time.Sleep(1)
        greetings :=  []string{"Helloworld!",strconv.Itoa(i)}
        ch.Publish("exchange1",strings.Join( greetings, " "),"")
        i = i 1
    }
 
}

接收端1:

package main
 
import (
    rabbitMQ "RabbitMQ"
    "log"
)
 
func main(){
    // 1.接收者,首先创建自己队列
    // 2.创建交换机
    // 3.将自己绑定到交换机上
    // 4.接收交换机上发过来的消息
 
    //第一个参数指定rabbitmq服务器的链接,第二个参数指定创建队列的名字
    //1
    receive_mq := rabbitMQ.New("amqp://user:password@ip:port/","hello1")
    //2
    //第一个参数:rabbitmq服务器的链接,第二个参数:交换机名字,第三个参数:交换机类型
    rabbitMQ.NewExchange("amqp://user:password@ip:port/","exchange1","fanout")
    //3
    // 队列绑定到exchange
    receive_mq.Bind("exchange1","")
    //4
    for{
        //接收消息时,指定
        msgs := receive_mq .Consume()
        go func() {
            for d := range msgs {
                log.Printf("recevie1  Received a message: %s", d.Body)
            }
        }()
    }
}

接收端2:

package main
 
import (
    rabbitMQ "RabbitMQ"
    "log"
)
 
func main(){
    // 1.接收者,首先创建自己队列
    // 2.创建交换机
    // 3.将自己绑定到交换机上
    // 4.接收交换机上发过来的消息
 
    //第一个参数指定rabbitmq服务器的链接,第二个参数指定创建队列的名字
    //1
    receive_mq := rabbitMQ.New("amqp://user:password@ip:port/","hello2")
    //2
    //第一个参数:rabbitmq服务器的链接,第二个参数:交换机名字,第三个参数:交换机类型
    rabbitMQ.NewExchange("amqp://user:password@ip:port/","exchange1","fanout")
    //3
    // 队列绑定到exchange
    receive_mq.Bind("exchange1","")
    //4
    for{
        //接收消息时,指定
        msgs := receive_mq .Consume()
        go func() {
            for d := range msgs {
                log.Printf("recevie2  Received a message: %s", d.Body)
            }
        }()
    }
}
全值匹配模式direct (路由模式)

发送端发送消息需要带有路由键,就是下面发送端程序的routing key1,是一个字符串,发送端发给exchange,路由模式下的exchange会匹配这个路由键,如下面这个图,发送者发送时带有orange此路由键时,这条消息只会被转发给Q1队列,如果路由键没有匹配上的怎么办?,全值匹配,没有匹配到,那么所有接收者都接收不到消息,消息只会发送给匹配的队列,接收端的路由键是绑定exchange的时候用的。
注意:接收队列可以绑定多个路由键到exchange上,比如下面,当发送路由键为black,green,会被Q2接收。
在这里插入图片描述
发送端:(需要绑路由键)

package main
 
import (
    "RabbitMQ"
    "strconv"
    "strings"
    "time"
)
 
func main(){
    ch := rabbitMQ.Connect("amqp://user:password@ip:port/")
    rabbitMQ.NewExchange("amqp://user:password@ip:port/","exchange","direct")
    i := 0
    for{
        time.Sleep(1)
        greetings :=  []string{"Helloworld!",strconv.Itoa(i)}
        if i%2 ==1 {
            //如果是奇数
            ch.Publish("exchange",strings.Join( greetings, " "),"routing key1")
        } else{
            ch.Publish("exchange",strings.Join( greetings, " "),"routing key2")
        }
        i = i 1
    }
 
}

接收端1:

package main
 
import (
    rabbitMQ "RabbitMQ"
    "log"
)
 
func main(){
    // 1.接收者,首先自己队列
    // 2.创建交换机
    // 3.将自己绑定到交换机上
    // 4.接收交换机上发过来的消息
    //第一个参数指定rabbitmq服务器的链接,第二个参数指定创建队列的名字
 
    //1
    receive_mq := rabbitMQ.New("amqp://user:password@ip:port/","hello2")
 
    //2
    //第一个参数:rabbitmq服务器的链接,第二个参数:交换机名字,第三个参数:交换机类型
    rabbitMQ.NewExchange("amqp://user:password@ip:port/","exchange","direct")
 
    //3
    receive_mq.Bind("exchange","routing key1")
 
    //4
    for{
        //接收消息时,指定
        msgs := receive_mq .Consume()
        go func() {
            for d := range msgs {
                log.Printf("recevie1  Received a message: %s", d.Body)
            }
        }()
    }
}

接收端2:

package main
 
import (
    rabbitMQ "RabbitMQ"
    "log"
)
 
func main(){
    // 1.接收者,首先自己队列
    // 2.创建交换机
    // 3.将自己绑定到交换机上
    // 4.接收交换机上发过来的消息
    //第一个参数指定rabbitmq服务器的链接,第二个参数指定创建队列的名字
 
    //1
    receive_mq := rabbitMQ.New("amqp://user:password@ip:port/","hello2")
 
    //2
    //第一个参数:rabbitmq服务器的链接,第二个参数:交换机名字,第三个参数:交换机类型
    rabbitMQ.NewExchange("amqp://user:password@ip:port/","exchange","direct")
 
    //3
    receive_mq.Bind("exchange","routing key2")
 
    //4
    for{
        //接收消息时,指定
        msgs := receive_mq .Consume()
        go func() {
            for d := range msgs {
                log.Printf("recevie2  Received a message: %s", d.Body)
            }
        }()
    }
}
4.topic 类型 (exchange之一 但是用的比较普遍)

前面的direct是全值匹配,那么topic就可以部分匹配,又可以全值匹配,比direct更加灵活。
消息发送到topic类型的exchange上时不能随意指定routing_key(一定是指由一系列由点号连接单词的字符串,单词可以是任意的,但一般都会与消息或多或少的有些关联)。Routing key的长度不能超过255个字节。Binding key也一定要是同样的方式。Topic类型的exchange就像一个直接的交换:一个由生产者指定了确定routing key的消息将会被推送给所有Binding key能与之匹配的消费者。然而这种绑定有两种特殊的情况:

  • *(星号):可以(只能)匹配一个单词
  • #(井号):可以匹配多个单词(或者零个)

在这里插入图片描述
重点:
Topic类型的exchange是很强大的,也可以实现其它类型的exchange。

  • 当一个队列被绑定为binding key为”#”时,它将会接收所有的消息,此时和fanout类型的exchange很像。
  • 当binding key不包含”*”和”#”时,这时候就很像direct类型的exchange

发送端:

package main
 
import (
    "RabbitMQ"
    "time"
)
 
func main(){
    ch := rabbitMQ.Connect("amqp://user:password@ip/")
    rabbitMQ.NewExchange("amqp://user:password@ip/","exchange","topic")
    for{
        time.Sleep(1)
        ch.Publish("exchange","hello world","lazy.brown.fox")
    }
 
}

接收端:

package main
 
import (
    rabbitMQ "RabbitMQ"
    "log"
)
 
func main(){
    // 1.接收者,首先自己队列
    // 2.创建交换机
    // 3.将自己绑定到交换机上
    // 4.接收交换机上发过来的消息
    //第一个参数指定rabbitmq服务器的链接,第二个参数指定创建队列的名字
 
    //1
    receive_mq := rabbitMQ.New("amqp://user:password@ip:port/","hello1")
 
    //2
    //第一个参数:rabbitmq服务器的链接,第二个参数:交换机名字,第三个参数:交换机类型
    rabbitMQ.NewExchange("amqp://user:password@ip:port/","exchange","topic")
 
    //3
    receive_mq.Bind("exchange","*.orange.*")
 
    //4
    for{
        //接收消息时,指定
        msgs := receive_mq .Consume()
        go func() {
            for d := range msgs {
                log.Printf("recevie1  Received a message: %s", d.Body)
            }
        }()
    }
}

分析topic!:
在这里插入图片描述
在这个例子中,我们将会发送一些描述动物的消息。Routing key的第一个单词是描述速度的,第二个单词是描述颜色的,第三个是描述物种的:“..”。

  • 这里我们创建三个Binding:Binding key为”.orange.”的Q1,和binding key为”..rabbit”和”lazy.#”的Q2。这些binding可以总结为:Q1对所有橘色的(orange)的动物感兴趣;Q2希望能拿到所有兔子的(rabbit)信息,还有比较懒惰的(lazy.#)动物信息。
  • 一条以” quick.orange.rabbit”为routing key的消息将会推送到Q1和Q2两个queue上,
  • routing key为“lazy.orange.elephant”的消息同样会被推送到Q1和Q2上。
  • 但如果routing key为”quick.orange.fox”的话,消息只会被推送到Q1上;
  • routing key为”lazy.brown.fox”的消息会被推送到Q2上,
  • routing key为"lazy.pink.rabbit”的消息也会被推送到Q2上,但同一条消息只会被推送到Q2上一次。
  • 如果在发送消息时所指定的exchange和routing key在消费者端没有对应的exchange和binding key与之绑定的话,那么这条消息将会被丢弃掉。

例如:“orange"和"quick.orange.male.rabbit”。但是routing为”lazy.orange.male.rabbit”的消息,将会被推到Q2上。

5.各个参数的含义
  • queue: 队列名称
  • durable: 是否持久化, 队列的声明默认是存放到内存中的,如果rabbitmq重启会丢失,如果想重启之后还存在就要使队列持久化,保存到Erlang自带的Mnesia数据库中,当rabbitmq重启之后会读取该数据库
  • exclusive:是否排外的,有两个作用,
    一:当连接关闭时connection.close()该队列是否会自动删除
    二:该队列是否是私有的private,如果不是排外的,可以使用两个消费者都访问同一个队列,没有任何问题,如果是排外的,会对当前队列加锁,其他通道channel是不能访问的,如果强制访问会报异常
    一般等于true的话用于一个队列只能有一个消费者来消费的场景
  • autoDelete:是否自动删除,当最后一个消费者断开连接之后队列是否自动被删除,可以通过RabbitMQ Management,查看某个队列的消费者数量,当consumers = 0时队列就会自动删除
  • arguments: 队列中的消息什么时候会自动被删除?
  • 自动应答:为了确保消息不会丢失,RabbitMQ支持消息应答。消费者发送一个消息应答,告诉RabbitMQ这个消息已经接收并且处理完毕了。RabbitMQ就可以删除它了。
    如果一个消费者挂掉却没有发送应答,RabbitMQ会理解为这个消息没有处理完全,然后交给另一个消费者去重新处理。这样,你就可以确认即使消费者偶尔挂掉也不会丢失任何消息了。
6.消息持久

消息持久化
Rabbit队列和交换器有一个不可告人的秘密,就是默认情况下重启服务器会导致消息丢失,那么怎么保证Rabbit在重启的时候不丢失呢?答案就是消息持久化。

当你把消息发送到Rabbit服务器的时候,你需要选择你是否要进行持久化,但这并不能保证Rabbit能从崩溃中恢复,想要Rabbit消息能恢复必须满足3个条件:

投递消息的时候durable设置为true,消息持久化,
代码:channel.queueDeclare(x, true, false, false, null),参数2设置为true持久化;
设置投递模式deliveryMode设置为2(持久),
代码:channel.basicPublish(x, x, MessageProperties.PERSISTENT_TEXT_PLAIN,x),
参数3设置为存储纯文本到磁盘;
消息已经到达持久化交换器上;
消息已经到达持久化的队列;

持久化工作原理

Rabbit会将你的持久化消息写入磁盘上的持久化日志文件,等消息被消费之后,Rabbit会把这条消息标识为等待垃圾回收。

持久化的缺点

消息持久化的优点显而易见,但缺点也很明显,那就是性能,因为要写入硬盘要比写入内存性能较低很多,从而降低了服务器的吞吐量,尽管使用SSD硬盘可以使事情得到缓解,但他仍然吸干了Rabbit的性能,当消息成千上万条要写入磁盘的时候,性能是很低的。

所以使用者要根据自己的情况,选择适合自己的方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值