学习内容
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类似,很少用)。
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的性能,当消息成千上万条要写入磁盘的时候,性能是很低的。
所以使用者要根据自己的情况,选择适合自己的方式。