同步通信
同步通信就是指两个通信的端点在进行数据传输时需要同时进行交互。发送方发送消息后会等待接收方对消息进行处理并做出响应。直到接收到响应或处理完成后才会继续执行后续操作,在这个通信过程中接收方会阻塞发送方,因为发送方和接受方是相互协作的。
实际开发中同步通信用于对数据的的完整性和准确性有要求的场景。例如发送请求等待服务器响应、客户端与数据库之间的交互等。
优点
- 用户认证和授权:
-
- 当用户登录系统时,通常需要与认证服务进行通信以验证用户的身份,并获取用户的权限信息。这个过程中需要确保用户的身份和权限信息是准确的,因此适合使用同步通信模式。
- 订单处理:
-
- 在电子商务系统中,当用户下单时,通常需要与库存服务进行通信以检查商品的库存情况,以及与支付服务进行通信以完成支付流程。这些操作需要确保订单信息的准确性和支付的完成,因此适合使用同步通信模式。
- 数据查询和处理:
-
- 在企业系统中,当用户需要查询或处理大量数据时,通常会与数据库进行通信。这个过程中需要确保数据的准确性和完整性,因此适合使用同步通信模式。
- 在线交易和交易查询:
-
- 在金融系统中,当用户进行在线交易时,通常需要与银行或支付服务进行通信以完成交易,以及与交易查询服务进行通信以查询交易记录。这些操作需要确保交易的安全性和准确性,因此适合使用同步通信模式。
- 实时通知和反馈:
-
- 在在线客服系统中,当用户向客服发送消息时,通常需要客服立即收到消息并进行回复。这个过程需要确保消息的实时性和准确性,因此适合使用同步通信模式。
缺点
- 扩展性差,耦合高。
- 性能下降,因为发送方在接收方没给出相应前都处于阻塞状态。
- 级联失败,当我们去调用其它服务时,如果这个服务失败,整个事务都将回滚。
异步通信
异步调用中包含三个角色:
- 消息发送者:发送消息。
- 消息broker:管理、暂存、转发消息。
- 消息接受者:接受消息。
异步通信是两个通信断点在同行过程中,发送方不需要等待接收方响应,就可以继续执行后续操作。
例如:给好友发送邮件,发送成功后你不需要等着好友给你回复。你可以继续去做别的事情。
优点
- 提高系统的吞吐量,提升系统性能
- 组件解耦
- 避免阻塞等待
- 故障隔离
- 缓存消息,流量削峰填谷
缺点
- 不能立即得到调用结果,时效性差
- 业务依赖broker的可靠性
- 不确定下游业务是否执行成功
技术选型
消息Broker,目前常见的实现方案就是消息队列(MessageQueue),简称为MQ.
目比较常见的MQ实现:
- ActiveMQ
- RabbitMQ
- RocketMQ
- Kafka
几种常见MQ的对比:
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
可用性 | 高 | 一般 | 高 | 高 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
安装
镜像尽量选择 带-management后缀的,因为这个是自带Web监控页面,同3.12版本MQ有两个
docker pull rabbitmq:3.12-management
docker pull rabbitmq:3.12 这个是不带Web管理页面的,是需要自己手动安装插件
docker pull rabbitmq:3.12-management
延迟队列插件
- 地址
Releases · rabbitmq/rabbitmq-delayed-message-exchange · GitHub
查看插件挂载目录
docker volume inspect mq-plugins
将插件上传到 /var/lib/docker/volumes/mq-plugins/_data 目录下
docker exec -it mq rabbitmq-plugins enable rabbitmq_delayed_message_exchange
运行
创建docker网络
docker network create test
启动容器
docker run \
-e RABBITMQ_DEFAULT_USER=test \
-e RABBITMQ_DEFAULT_PASS=123123 \
-v mq-plugins:/plugins \
--name mq \
--hostname mq \
-p 15672:15672 \
-p 5672:5672 \
--network test \
-d \
rabbitmq:3.12-management
RabbitMQ 常用端口以及作用
端口 | 功能 |
5672 | AMQP(Advanced Message Queuing Protocol)协议的默认端口,用于客户端与RabbitMQ服务器之间的通信。 |
15672 | RabbitMQ的管理界面,默认使用HTTP协议,用于监控和管理RabbitMQ服务器。 |
4369 | Erlang分布式节点通信端口,用于RabbitMQ节点之间的通信。 |
25672 | Erlang分布式节点通信端口,用于集群中的内部通信。 |
5671 | 安全的AMQP端口,使用TLS/SSL进行加密通信。 |
架构图
publisher
:生产者,也就是发送消息的一方consumer
:消费者,也就是消费消息的一方queue
:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理exchange
:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。virtual host
:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue
工作模式
1. 简单模式(Simple)
- 特点:
-
- 生产者直接将消息发送到队列中,消费者从队列中获取消息并进行处理。
- 消息一旦被消费,就会从队列中移除。
- 真实应用场景:
-
- 电子邮件通知系统:生产者将待发送的邮件放入队列,消费者负责从队列中获取邮件并发送。
- 优点:
-
- 实现简单,易于理解和部署。
- 适用于单一消费者场景。
- 缺点:
-
- 没有消息持久化,一旦 RabbitMQ 服务器重启,队列中的消息将会丢失。
1.1. 目录结构
simple
-mq
–-rabbitmq.go //这个是RabbitMQ的封装
-SimplePublish
–-mainSimplePublish.go //生产者发送消息
-SimpleRecieve
–-mainSimpleRecieve.go // 消费者接受消息
1.2. 代码实现
1.2.1. rabbitmq.go
package mq
import (
"fmt"
"log"
"github.com/streadway/amqp"
)
// MQURL amqp://user:password@host:port/vhost
// amqp://是固定参数,这个信息是固定不变的。后面两个是用户名密码ip地址端口号Virtual Host
// 如果vhost是“/”就输入/%2F,/%2F代表斜杠
const MQURL = "amqp://test:123123@127.0.0.1:5672/%2F"
// RabbitMQ rabbitMQ结构体
type RabbitMQ struct {
conn *amqp.Connection
channel *amqp.Channel
//队列名称
QueueName string
//交换机名称
Exchange string
//bind Key 名称
Key string
//连接信息
Mqurl string
}
// NewRabbitMQ 创建结构体实例
func NewRabbitMQ(queueName string, exchange string, key string) *RabbitMQ {
return &RabbitMQ{QueueName: queueName, Exchange: exchange, Key: key, Mqurl: MQURL}
}
// Destory 断开channel 和 connection
func (r *RabbitMQ) Destory() {
err := r.channel.Close()
if err != nil {
return
}
err = r.conn.Close()
if err != nil {
return
}
}
// 错误处理函数
func (r *RabbitMQ) failOnErr(err error, message string) {
if err != nil {
log.Fatalf("%s:%s", message, err)
}
}
// NewRabbitMQSimple 创建简单模式下RabbitMQ实例
func NewRabbitMQSimple(queueName string) *RabbitMQ {
//创建RabbitMQ实例
rabbitmq := NewRabbitMQ(queueName, "", "")
var err error
//获取connection
rabbitmq.conn, err = amqp.Dial(rabbitmq.Mqurl)
rabbitmq.failOnErr(err, "failed to connect rabb"+
"itmq!")
//获取channel
rabbitmq.channel, err = rabbitmq.conn.Channel()
rabbitmq.failOnErr(err, "failed to open a channel")
return rabbitmq
}
// PublishSimple simple模式队列生产
func (r *RabbitMQ) PublishSimple(message string) {
//1.申请队列,如果队列不存在会自动创建,存在则跳过创建
_, err := r.channel.QueueDeclare(
// queue:队列名称
r.QueueName,
//durable:是否持久化,当mq重启之后,还在
true,
//exclusive:是否独占即只能有一个消费者监听这个队列
false,
//autoDelete:是否自动删除。当没有Consumer时,自动删除掉
false,
//noWait:是否阻塞处理。true:不阻塞,false:阻塞
false,
//arguments:其他属性
nil,
)
if err != nil {
fmt.Println(err)
}
//调用channel 发送消息到队列中
r.channel.Publish(
r.Exchange,
r.QueueName,
//如果为true,根据自身exchange类型和routekey规则无法找到符合条件的队列会把消息返还给发送者
false,
//如果为true,当exchange发送消息到队列后发现队列上没有消费者,则会把消息返还给发送者
false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(message),
})
}
// ConsumeSimple simple模式下消费者
func (r *RabbitMQ) ConsumeSimple() {
//1.申请队列,如果队列不存在会自动创建,存在则跳过创建
q, err := r.channel.QueueDeclare(
//队列名称
r.QueueName,
//是否持久化
true,
//是否自动删除
false,
//是否具有排他性
false,
//是否阻塞处理
false,
//额外的属性
nil,
)
if err != nil {
fmt.Println(err)
}
//接收消息
msgs, err := r.channel.Consume(
q.Name, // queue
//用来区分多个消费者
"", // consumer
//是否自动应答
true, // auto-ack
//是否独有
false, // exclusive
//设置为true,表示 不能将同一个Conenction中生产者发送的消息传递给这个Connection中 的消费者
false, // no-local
//列是否阻塞
false, // no-wait
nil, // args
)
if err != nil {
fmt.Println(err)
}
forever := make(chan bool)
//启用协程处理消息
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
}
1.2.2. SimplePublish.go
package main
import (
"fmt"
"qiutian.com/study/02.rabbitmq/Simple/mq"
)
func main() {
rabbitmq := mq.NewRabbitMQSimple("hello.queue1")
rabbitmq.PublishSimple("Hello akita!")
fmt.Println("发送成功!")
}
1.2.3. mainSimpleRecieve.go
package main
import (
"qiutian.com/study/02.rabbitmq/Simple/mq"
)
// simple模式消费者
func main() {
// queueName 替换成你自己的队列名
rabbitmq := mq.NewRabbitMQSimple("hello.queue1")
rabbitmq.ConsumeSimple()
}
2. 工作模式(Work)
- 特点:
-
- 多个消费者同时从同一个队列中获取消息并处理,每个消息只能被一个消费者处理。
- 消息在被消费者处理后会从队列中移除。
- 真实应用场景:
-
- 任务分发系统:多个消费者同时从队列中获取任务,每个消费者处理一个任务,以实现任务的负载均衡。
- 优点:
-
- 可以通过增加消费者来提高消息处理的并发性。
- 缺点:
-
- 没有消息持久化,一旦 RabbitMQ 服务器重启,队列中的消息将会丢失。
2.1. 目录结构
Work
-mq
–-rabbitmq.go //这个是RabbitMQ的封装
-WorkPublish
–-mainWorkPublish.go //生产者发送消息
-WorkRecieve1
–-mainWorkRecieve1.go // 消费者接受消息
-WorkRecieve2
–-mainWorkRecieve2.go // 消费者接受消息
2.2. 代码实现
2.2.1. rabbitmq.go
package mq
import (
"fmt"
"log"
"github.com/streadway/amqp"
)
// MQURL amqp://user:password@host:port/vhost
// amqp://是固定参数,这个信息是固定不变的。后面两个是用户名密码ip地址端口号Virtual Host
// 如果vhost是“/”就输入/%2F,/%2F代表斜杠
const MQURL = "amqp://test:123123@127.0.0.1:5672/%2F"
// RabbitMQ rabbitMQ结构体
type RabbitMQ struct {
conn *amqp.Connection
channel *amqp.Channel
//队列名称
QueueName string
//交换机名称
Exchange string
//bind Key 名称
Key string
//连接信息
Mqurl string
}
// NewRabbitMQ 创建结构体实例
func NewRabbitMQ(queueName string, exchange string, key string) *RabbitMQ {
return &RabbitMQ{QueueName: queueName, Exchange: exchange, Key: key, Mqurl: MQURL}
}
// Destory 断开channel 和 connection
func (r *RabbitMQ) Destory() {
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))
}
}
// NewRabbitMQWork 创建work模式下RabbitMQ实例
func NewRabbitMQWork(queueName string) *RabbitMQ {
//创建RabbitMQ实例
rabbitmq := NewRabbitMQ(queueName, "", "")
var err error
//获取connection
rabbitmq.conn, err = amqp.Dial(rabbitmq.Mqurl)
rabbitmq.failOnErr(err, "failed to connect rabb"+
"itmq!")
//获取channel
rabbitmq.channel, err = rabbitmq.conn.Channel()
rabbitmq.failOnErr(err, "failed to open a channel")
return rabbitmq
}
// PublishWork work模式队列生产
func (r *RabbitMQ) PublishWork(message string) {
//1.申请队列,如果队列不存在会自动创建,存在则跳过创建
_, err := r.channel.QueueDeclare(
// queue:队列名称
r.QueueName,
//durable:是否持久化,当mq重启之后,还在
true,
//exclusive:是否独占即只能有一个消费者监听这个队列
false,
//autoDelete:是否自动删除。当没有Consumer时,自动删除掉
false,
//noWait:是否阻塞处理。true:不阻塞,false:阻塞
false,
//arguments:其他属性
nil,
)
if err != nil {
fmt.Println(err)
}
//调用channel 发送消息到队列中
r.channel.Publish(
r.Exchange,
r.QueueName,
//如果为true,根据自身exchange类型和routekey规则无法找到符合条件的队列会把消息返还给发送者
false,
//如果为true,当exchange发送消息到队列后发现队列上没有消费者,则会把消息返还给发送者
false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(message),
})
}
// ConsumeWork work 模式下消费者
func (r *RabbitMQ) ConsumeWork() {
//1.申请队列,如果队列不存在会自动创建,存在则跳过创建
q, err := r.channel.QueueDeclare(
//队列名称
r.QueueName,
//是否持久化
true,
//是否自动删除
false,
//是否具有排他性
false,
//是否阻塞处理
false,
//额外的属性
nil,
)
if err != nil {
fmt.Println(err)
}
//接收消息
msgs, err := r.channel.Consume(
q.Name, // queue
//用来区分多个消费者
"", // consumer
//是否自动应答
true, // auto-ack
//是否独有
false, // exclusive
//设置为true,表示 不能将同一个Conenction中生产者发送的消息传递给这个Connection中 的消费者
false, // no-local
//列是否阻塞
false, // no-wait
nil, // args
)
if err != nil {
fmt.Println(err)
}
forever := make(chan bool)
//启用协程处理消息
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
}
2.2.2. mainWorkPublish.go
package main
import (
"fmt"
"qiutian.com/study/02.rabbitmq/Work/mq"
"strconv"
"time"
)
func main() {
rabbitmq := mq.NewRabbitMQWork("hello.queue1")
for i := 0; i <= 100; i++ {
rabbitmq.PublishWork("Hello akita!" + strconv.Itoa(i))
time.Sleep(1 * time.Second)
fmt.Println(i)
}
}
2.2.3. mainWorkRecieve1.go
package main
import (
"qiutian.com/study/02.rabbitmq/Work/mq"
)
func main() {
// queueName 替换成你自己的队列名
rabbitmq := mq.NewRabbitMQWork("hello.queue1")
rabbitmq.ConsumeWork()
}
2.2.4. mainWorkRecieve2.go
package main
import (
"qiutian.com/study/02.rabbitmq/Work/mq"
)
func main() {
// queueName 替换成你自己的队列名
rabbitmq := mq.NewRabbitMQWork("hello.queue1")
rabbitmq.ConsumeWork()
}
3. 订阅模式(Publish)
TIPS:
在订阅消费者模式下,消费者后监听到的数据取决于它开始监听的时间。如果消费者在生产者生产数据之前开始监听,那么它将不会消费到生产者在开始监听之前发布的数据。只有在消费者开始监听之后,生产者发布的数据才会被消费者接收和处理。因此,如果你想让消费者消费之前发布的数据,你需要确保消费者在生产者发布数据之后开始监听。
- 特点:
-
- 生产者将消息发送到一个交换机中,交换机将消息广播给所有绑定了该交换机的队列。
- 每个消息都会被交换机广播给所有绑定了该交换机的队列。
- 真实应用场景:
-
- 新闻推送系统:生产者将最新的新闻发布到交换机中,每个订阅者(消费者)都会收到相同的新闻。
- 优点:
-
- 实现了消息的广播功能,适用于多个消费者同时接收相同消息的场景。
- 缺点:
-
- 无法根据消费者的兴趣或条件选择性地接收消息。
3.1. 目录结构
3.2. 代码实现
3.2.1.1. rabbitmq.go
package mq
import (
"fmt"
"log"
"github.com/streadway/amqp"
)
// MQURL amqp://user:password@host:port/vhost
// amqp://是固定参数,这个信息是固定不变的。后面两个是用户名密码ip地址端口号Virtual Host
// 如果vhost是“/”就输入/%2F,/%2F代表斜杠
const MQURL = "amqp://test:123123@127.0.0.1:5672/%2F"
// RabbitMQ rabbitMQ结构体
type RabbitMQ struct {
conn *amqp.Connection
channel *amqp.Channel
//队列名称
QueueName string
//交换机名称
Exchange string
//bind Key 名称
Key string
//连接信息
Mqurl string
}
// NewRabbitMQ 创建结构体实例
func NewRabbitMQ(queueName string, exchange string, key string) *RabbitMQ {
return &RabbitMQ{QueueName: queueName, Exchange: exchange, Key: key, Mqurl: MQURL}
}
// Destory 断开channel 和 connection
func (r *RabbitMQ) Destory() {
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))
}
}
// NewRabbitMQPub 创建Pub模式下RabbitMQ实例
func NewRabbitMQPub(exchangeName string) *RabbitMQ {
//创建RabbitMQ实例
rabbitmq := NewRabbitMQ("", exchangeName, "")
var err error
//获取connection
rabbitmq.conn, err = amqp.Dial(rabbitmq.Mqurl)
rabbitmq.failOnErr(err, "failed to connect rabb"+
"itmq!")
//获取channel
rabbitmq.channel, err = rabbitmq.conn.Channel()
rabbitmq.failOnErr(err, "failed to open a channel")
return rabbitmq
}
// PublishPub Pub模式队列生产
func (r *RabbitMQ) PublishPub(message string) {
//1.尝试创建交换机
err := r.channel.ExchangeDeclare(
//交换机名称
r.Exchange,
//交换机类型
"fanout",
//是否持久化
true,
//是否自动删除
false,
//true表示这个exchange不可以被client用来推送消息,仅用来进行exchange和exchange之间的绑定
false,
//是否阻塞处理
false,
//其他属性
nil,
)
r.failOnErr(err, "Failed to declare an exchange")
//2.发送消息
//发送消息
//参数:
//1.交换机
//2.队列,
//3.是否强制性路由到队列,如果为true,根据自身exchange类型和routekey规则无法找到符合条件的队列会把消息返还
//给发送者
//4.是否立即发送
//5.其他属性
//6.发送消息的内容
//调用channel 发送消息到队列中
r.channel.Publish(
r.Exchange,
"",
//如果为true,根据自身exchange类型和routekey规则无法找到符合条件的队列会把消息返还给发送者
false,
//如果为true,当exchange发送消息到队列后发现队列上没有消费者,则会把消息返还给发送者
false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(message),
})
}
// ConsumePub pub 模式下消费者
func (r *RabbitMQ) ConsumePub() {
//1.试探性创建交换机
err := r.channel.ExchangeDeclare(
// 交换机名称
r.Exchange,
//交换机类型
"fanout",
//是否持久化
true,
// 是否自动删除
false,
//YES表示这个exchange不可以被client用来推送消息,仅用来进行exchange和exchange之间的绑定
false,
//
false,
nil,
)
r.failOnErr(err, "Failed to declare an exchange")
//2.试探性创建队列,这里注意队列名称不要写
q, err := r.channel.QueueDeclare(
//随机生产队列名称
"",
//是否持久化
false,
//是否自动删除
false,
//是否具有排他性
true,
//是否阻塞处理
false,
//其他参数
nil,
)
r.failOnErr(err, "Failed to declare a queue")
//绑定队列到 exchange 中
err = r.channel.QueueBind(
q.Name,
//在pub/sub模式下,这里的key要为空
"",
r.Exchange,
false,
nil)
//消费消息
messages, err := r.channel.Consume(
q.Name,
"",
true,
false,
false,
false,
nil,
)
forever := make(chan bool)
//启用协程处理消息
go func() {
for d := range messages {
log.Printf("Received a message: %s", d.Body)
}
}()
fmt.Println("退出请按 CTRL+C\n")
<-forever
}
3.2.1.2. mainPubPublish.go
package main
import (
"fmt"
"qiutian.com/study/02.rabbitmq/Pub/mq"
"strconv"
"time"
)
func main() {
rabbitmq := mq.NewRabbitMQPub("newExchange")
for i := 0; i < 100; i++ {
rabbitmq.PublishPub("订阅模式生产第" +
strconv.Itoa(i) + "条" + "数据")
fmt.Println("订阅模式生产第" +
strconv.Itoa(i) + "条" + "数据")
time.Sleep(1 * time.Second)
}
}
3.2.1.3. mainPubRecieve1.go
package main
import (
"qiutian.com/study/02.rabbitmq/Pub/mq"
)
func main() {
rabbitmq := mq.NewRabbitMQPub("" +
"newExchange")
rabbitmq.ConsumePub()
}
3.2.1.4. mainPubRecieve2.go
package main
import (
"qiutian.com/study/02.rabbitmq/Pub/mq"
)
func main() {
rabbitmq := mq.NewRabbitMQPub("" +
"newExchange")
rabbitmq.ConsumePub()
}
4. 路由模式(routing)
路由模式(Routing Mode)是消息队列中一种常见的消息路由模式,它允许消息的发送者(Producer)将消息发送到指定的交换机(Exchange),而交换机则根据消息的路由键(Routing Key)将消息路由到符合条件的队列(Queue)。路由模式提供了一种灵活的消息路由机制,使得消息的分发可以根据不同的路由规则进行定制。
优点:
- 灵活性高: 路由模式允许根据消息的属性或条件将消息路由到不同的队列或消费者,从而实现灵活的消息处理和路由策略。
- 解耦性强: 路由模式可以将消息的生产者和消费者解耦,使它们之间相互独立。生产者只需将消息发送到交换机,而不需要知道具体的消费者是谁,消费者也只需订阅感兴趣的消息,而不需要知道消息的生产者是谁,从而实现了系统的解耦。
- 灵活的消息过滤和路由: 路由模式允许根据消息的属性、标签或主题将消息路由到不同的队列或消费者,实现精确的消息过滤和路由,从而提高了系统的灵活性和可扩展性。
- 支持多种路由策略: 路由模式支持多种路由策略,如直连路由、主题路由、分发路由等,可以根据不同的业务需求选择合适的路由策略,从而满足不同的应用场景。
缺点:
- 配置复杂: 路由模式的配置相对复杂,需要定义交换机、队列和绑定关系,以及配置路由规则等,如果配置不当可能会导致消息路由错误或丢失。
- 性能损耗: 路由模式需要对消息进行额外的路由和过滤操作,可能会增加系统的消息处理延迟和性能消耗,特别是在消息量较大的情况下。
- 消息堆积风险: 如果路由模式配置不当或消息处理能力不足,可能会导致消息堆积的风险,从而影响系统的稳定性和可靠性。
- 维护成本高: 路由模式需要维护交换机、队列和绑定关系等配置信息,特别是在系统规模较大或消息路由策略较复杂的情况下,维护成本可能会较高。
使用场景:
- 微服务架构下的消息路由:在微服务架构中,不同的服务可能需要处理不同类型的消息。通过路由模式,可以将消息根据服务的需求进行路由,确保每个服务只接收到与其相关的消息,从而实现解耦和灵活性。
- 实时监控与日志分析:在监控系统或日志分析系统中,需要根据不同的指标或日志级别对数据进行分类和分析。路由模式可以根据数据的属性或标签将数据路由到不同的处理节点,以实现实时监控和日志分析。
- 任务调度与负载均衡:在任务调度系统中,任务可能具有不同的优先级或类型,需要根据任务的属性进行分发和调度。通过路由模式,可以将任务路由到不同的工作节点,实现任务的负载均衡和优先级调度。
- 多级消息过滤与订阅:在订阅发布系统中,订阅者可能对不同类型或主题的消息感兴趣。路由模式可以根据消息的属性或主题将消息路由到不同的订阅者,实现精确的消息过滤和订阅。
- 数据同步与复制:在分布式系统中,可能需要将数据复制到不同的节点或数据中心,以实现数据的备份和灾备。通过路由模式,可以将数据根据不同的条件进行路由和复制,确保数据的一致性和可靠性。
4.1. 目录结构
4.2. 代码实现
4.2.1. rabbitmq.go
package mq
import (
"fmt"
"log"
"github.com/streadway/amqp"
)
// MQURL amqp://user:password@host:port/vhost
// amqp://是固定参数,这个信息是固定不变的。后面两个是用户名密码ip地址端口号Virtual Host
// 如果vhost是“/”就输入/%2F,/%2F代表斜杠
const MQURL = "amqp://test:123123@127.0.0.1:5672/%2F"
// RabbitMQ rabbitMQ结构体
type RabbitMQ struct {
conn *amqp.Connection
channel *amqp.Channel
//队列名称
QueueName string
//交换机名称
Exchange string
//bind Key 名称
Key string
//连接信息
Mqurl string
}
// NewRabbitMQ 创建结构体实例
func NewRabbitMQ(queueName string, exchange string, key string) *RabbitMQ {
return &RabbitMQ{QueueName: queueName, Exchange: exchange, Key: key, Mqurl: MQURL}
}
// Destory 断开channel 和 connection
func (r *RabbitMQ) Destory() {
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))
}
}
// NewRabbitMQRouting 路由模式 创建RabbitMQ实例
func NewRabbitMQRouting(exchangeName string, routingKey string) *RabbitMQ {
//创建RabbitMQ实例
rabbitmq := NewRabbitMQ("", exchangeName, routingKey)
var err error
//获取connection
rabbitmq.conn, err = amqp.Dial(rabbitmq.Mqurl)
rabbitmq.failOnErr(err, "failed to connect rabbitmq!")
//获取channel
rabbitmq.channel, err = rabbitmq.conn.Channel()
rabbitmq.failOnErr(err, "failed to open a channel")
return rabbitmq
}
// PublishRouting 路由模式发送消息
func (r *RabbitMQ) PublishRouting(message string) {
//1.尝试创建交换机
err := r.channel.ExchangeDeclare(
//交换机名称
r.Exchange,
//交换机类型
"direct",
//是否持久化
true,
//是否自动删除
false,
//true表示这个exchange不可以被client用来推送消息,仅用来进行exchange和exchange之间的绑定
false,
//是否阻塞处理
false,
//其他属性
nil,
)
r.failOnErr(err, "Failed to declare an exchange")
//2.发送消息
//发送消息
//参数:
//1.交换机
//2.队列,
//3.是否强制性路由到队列,如果为true,根据自身exchange类型和routekey规则无法找到符合条件的队列会把消息返还
//给发送者
//4.是否立即发送
//5.其他属性
//6.发送消息的内容
//调用channel 发送消息到队列中
r.channel.Publish(
r.Exchange,
r.Key,
//如果为true,根据自身exchange类型和routekey规则无法找到符合条件的队列会把消息返还给发送者
false,
//如果为true,当exchange发送消息到队列后发现队列上没有消费者,则会把消息返还给发送者
false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(message),
})
}
// ConsumeRouting routing 模式下消费者
func (r *RabbitMQ) ConsumeRouting() {
//1.试探性创建交换机
err := r.channel.ExchangeDeclare(
// 交换机名称
r.Exchange,
//交换机类型
"direct",
//是否持久化
true,
// 是否自动删除
false,
//YES表示这个exchange不可以被client用来推送消息,仅用来进行exchange和exchange之间的绑定
false,
//
false,
nil,
)
r.failOnErr(err, "Failed to declare an exchange")
//2.试探性创建队列,这里注意队列名称不要写
q, err := r.channel.QueueDeclare(
//随机生产队列名称
"",
//是否持久化
false,
//是否自动删除
false,
//是否具有排他性
true,
//是否阻塞处理
false,
//其他参数
nil,
)
r.failOnErr(err, "Failed to declare a queue")
//绑定队列到 exchange 中
err = r.channel.QueueBind(
q.Name,
// routing模式下要指定key
r.Key,
r.Exchange,
false,
nil)
//消费消息
messages, err := r.channel.Consume(
q.Name,
"",
true,
false,
false,
false,
nil,
)
forever := make(chan bool)
//启用协程处理消息
go func() {
for d := range messages {
log.Printf("Received a message: %s", d.Body)
}
}()
fmt.Println("退出请按 CTRL+C\n")
<-forever
}
4.2.2. mainRoutingPublish.go
package main
import (
"fmt"
"qiutian.com/study/02.rabbitmq/Routing/mq"
"strconv"
"time"
)
func main() {
routingOne := mq.NewRabbitMQRouting("newRoutingExchange", "newRoutingKey_one")
routingTwo := mq.NewRabbitMQRouting("newRoutingExchange", "newRoutingKey_two")
for i := 0; i < 100; i++ {
if i%2 == 0 {
routingOne.PublishRouting("Hello World! " + strconv.Itoa(i))
} else {
routingTwo.PublishRouting("Hello World! " + strconv.Itoa(i))
}
time.Sleep(time.Second)
fmt.Println(i)
}
}
4.2.3. mianRoutingRecieve1.go
package main
import (
"qiutian.com/study/02.rabbitmq/Routing/mq"
)
func main() {
routingOne := mq.NewRabbitMQRouting("newRoutingExchange", "newRoutingKey_one")
routingOne.ConsumeRouting()
}
4.2.4. mianRoutingRecieve2.go
package main
import (
"qiutian.com/study/02.rabbitmq/Routing/mq"
)
func main() {
routingTwo := mq.NewRabbitMQRouting("newRoutingExchange", "newRoutingKey_two")
routingTwo.ConsumeRouting()
}
5. 话题模式(Topic)
红色Queue:绑定的是usa.#,因此凡是以usa.开头的routing key都会被匹配到
黄色Queue:绑定的是#.news,因此凡是以.news结尾的routing key都会被匹配
话题模式(Topic Mode)是消息队列中一种高级的消息路由模式,它基于消息的主题(Topic)进行消息的订阅和分发。在话题模式中,消息的发送者(Producer)将消息发送到特定的主题,而消息的接收者(Consumer)则根据自己感兴趣的主题进行订阅,从而接收相关的消息。
工作原理:
- 主题定义: 在话题模式中,主题由一个或多个单词组成,每个单词之间用点号(.)分隔,例如:stock.usd.nyse。
- 通配符匹配: 话题模式支持两种通配符,分别是*(星号)和#(井号)。
-
- *:表示匹配一个单词,例如:stock.*.nyse可以匹配stock.usd.nyse和stock.eur.nyse等。
- #:表示匹配零个或多个单词,例如:stock.usd.#可以匹配stock.usd.nyse、stock.usd.nasdaq以及stock.usd等。
- 消息路由: 消息的发送者在发送消息时指定一个主题,而消息的接收者则可以使用通配符来订阅感兴趣的主题。消息队列根据主题的匹配规则将消息路由到符合条件的订阅者。
应用场景:
- 多级消息过滤: 话题模式可以根据消息的主题进行多级的消息过滤和匹配,从而实现精确的消息路由和分发。
- 发布/订阅系统: 话题模式常用于发布/订阅系统中,其中消息的发送者作为发布者,向不同的主题发送消息,而消息的接收者作为订阅者,根据自己的需求订阅感兴趣的主题。
- 事件驱动架构: 话题模式可以用于构建事件驱动的架构,其中各个服务之间通过消息队列进行事件的发布和订阅,从而实现服务之间的解耦和灵活的消息传递。
优点:
- 灵活性高: 话题模式支持灵活的主题定义和通配符匹配,可以根据不同的业务需求实现精确的消息路由和分发。
- 解耦性强: 话题模式可以将消息的生产者和消费者解耦,使它们之间相互独立,从而提高系统的可维护性和可扩展性。
缺点:
- 配置复杂: 话题模式的配置相对复杂,需要定义主题和通配符规则,并确保发送者和接收者之间的匹配规则一致,否则可能导致消息路由错误或丢失。
- 性能损耗: 话题模式需要对消息进行额外的匹配和路由操作,可能会增加系统的消息处理延迟和性能消耗,特别是在消息量较大的情况下
5.1. 目录结构
5.2. 代码实现
5.2.1. rabbitmq.go
package mq
import (
"fmt"
"log"
"github.com/streadway/amqp"
)
// MQURL amqp://user:password@host:port/vhost
// amqp://是固定参数,这个信息是固定不变的。后面两个是用户名密码ip地址端口号Virtual Host
// 如果vhost是“/”就输入/%2F,/%2F代表斜杠
const MQURL = "amqp://test:123123@127.0.0.1:5672/%2F"
// RabbitMQ rabbitMQ结构体
type RabbitMQ struct {
conn *amqp.Connection
channel *amqp.Channel
//队列名称
QueueName string
//交换机名称
Exchange string
//bind Key 名称
Key string
//连接信息
Mqurl string
}
// NewRabbitMQ 创建结构体实例
func NewRabbitMQ(queueName string, exchange string, key string) *RabbitMQ {
return &RabbitMQ{QueueName: queueName, Exchange: exchange, Key: key, Mqurl: MQURL}
}
// Destory 断开channel 和 connection
func (r *RabbitMQ) Destory() {
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))
}
}
// NewRabbitMQTopic 话题模式 创建RabbitMQ实例
func NewRabbitMQTopic(exchangeName string, routingKey string) *RabbitMQ {
//创建RabbitMQ实例
rabbitmq := NewRabbitMQ("", exchangeName, routingKey)
var err error
//获取connection
rabbitmq.conn, err = amqp.Dial(rabbitmq.Mqurl)
rabbitmq.failOnErr(err, "failed to connect rabbitmq!")
//获取channel
rabbitmq.channel, err = rabbitmq.conn.Channel()
rabbitmq.failOnErr(err, "failed to open a channel")
return rabbitmq
}
// PublishTopic 话题模式发送消息
func (r *RabbitMQ) PublishTopic(message string) {
//1.尝试创建交换机
err := r.channel.ExchangeDeclare(
//交换机名称
r.Exchange,
//交换机类型
"topic",
//是否持久化
true,
//是否自动删除
false,
//true表示这个exchange不可以被client用来推送消息,仅用来进行exchange和exchange之间的绑定
false,
//是否阻塞处理
false,
//其他属性
nil,
)
r.failOnErr(err, "Failed to declare an exchange")
//2.发送消息
//发送消息
//参数:
//1.交换机
//2.队列,
//3.是否强制性路由到队列,如果为true,根据自身exchange类型和routekey规则无法找到符合条件的队列会把消息返还
//给发送者
//4.是否立即发送
//5.其他属性
//6.发送消息的内容
//调用channel 发送消息到队列中
r.channel.Publish(
r.Exchange,
r.Key,
//如果为true,根据自身exchange类型和routekey规则无法找到符合条件的队列会把消息返还给发送者
false,
//如果为true,当exchange发送消息到队列后发现队列上没有消费者,则会把消息返还给发送者
false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(message),
})
}
// ConsumeTopic topic 模式下消费者
func (r *RabbitMQ) ConsumeTopic() {
//1.试探性创建交换机
err := r.channel.ExchangeDeclare(
// 交换机名称
r.Exchange,
//交换机类型
"topic",
//是否持久化
true,
// 是否自动删除
false,
//YES表示这个exchange不可以被client用来推送消息,仅用来进行exchange和exchange之间的绑定
false,
//
false,
nil,
)
r.failOnErr(err, "Failed to declare an exchange")
//2.试探性创建队列,这里注意队列名称不要写
q, err := r.channel.QueueDeclare(
//随机生产队列名称
"",
//是否持久化
false,
//是否自动删除
false,
//是否具有排他性
true,
//是否阻塞处理
false,
//其他参数
nil,
)
r.failOnErr(err, "Failed to declare a queue")
//绑定队列到 exchange 中
err = r.channel.QueueBind(
q.Name,
// topic模式下要指定key
r.Key,
r.Exchange,
false,
nil)
//消费消息
messages, err := r.channel.Consume(
q.Name,
"",
true,
false,
false,
false,
nil,
)
forever := make(chan bool)
//启用协程处理消息
go func() {
for d := range messages {
log.Printf("Received a message: %s", d.Body)
}
}()
fmt.Println("退出请按 CTRL+C\n")
<-forever
}
5.2.2. mainTopicPublish.go
package main
import (
"fmt"
"qiutian.com/study/02.rabbitmq/Topic/mq"
"strconv"
"time"
)
func main() {
topicOne := mq.NewRabbitMQTopic("newTopicExchange", "newTopicKey.one")
topicTwo := mq.NewRabbitMQTopic("newTopicExchange", "newTopicKey.two")
for i := 0; i < 100; i++ {
// 发布消息
// 参数:交换机,路由键,消息
topicOne.PublishTopic("Hello World! " + strconv.Itoa(i))
topicTwo.PublishTopic("Hello World! " + strconv.Itoa(i))
// 模拟耗时操作
time.Sleep(time.Second)
fmt.Println(i)
}
}
5.2.3. mainTopicRecieve1.go
package main
import (
"qiutian.com/study/02.rabbitmq/Topic/mq"
)
func main() {
topicOne := mq.NewRabbitMQTopic("newTopicExchange", "#")
topicOne.ConsumeTopic()
}
5.2.4. mainTopicRecieve2.go
package main
import (
"qiutian.com/study/02.rabbitmq/Topic/mq"
)
func main() {
topicTwo := mq.NewRabbitMQTopic("newTopicExchange", "*.two")
topicTwo.ConsumeTopic()
}