参考文档
RMQ官网提供的教程:https://www.rabbitmq.com/getstarted.html
go-amqp库函数手册:https://godoc.org/github.com/streadway/amqp
书籍:RabbitMQ实战指南(书中案例是java描述的)
RMQ的安装和端口
手动安装太麻烦,请自行百度。这里只给出一种基于docker安装的简单形式。
docker run -d --hostname my-rabbit --name rmq -p 15672:15672 -p 5672:5672 -p 25672:25672 -e RABBITMQ_DEFAULT_USER=用户名 -e RABBITMQ_DEFAULT_PASS=密码 rabbitmq:3-management
通过命令可以看出,一共映射了三个端口,简单说下这三个端口是干什么的。
5672:连接生产者、消费者的端口。
15672:WEB管理页面的端口。
25672:分布式集群的端口。
基础概念
amqp:一种消息中间件协议,RMQ是amqp协议的一个具体实现。RMQ使用Erlang语言实现的,具有很好的并发能力,具体历史请百度,这里主要关心怎么用。
注:此图不一定是最标准的,但比较形象,易于理解。
下面是图中出现的单词的详细解释:
Producer:生产者,负责生产消息。
Connect:连接,生产者与RMQ Server之间建立的TCP连接。
Channel:信道,一条连接可包含多条信道,不同信道之间通信互不干扰。考虑下多线程应用场景,每个线程对应一条信道,而不是对应一条连接,这样可以提高性能。
body:消息主体,要传递的数据。
exchange:交换器,负责把消息转发到对应的队列。交换器本身没有缓存消息的功能,消息是在队列中缓存的,如果队列不存在,则交换器会直接丢弃消息。常用的有四种类型的交换器:direct、fanout、topic、headers。不同类型的交换器有不同的交换规则,交换器会根据交换规则把消息转发到对应的队列。
exchangeName:交换器名称,每个交换器对应一个名称,发送消息时会附带交换器名称,根据交换器名称选择对应的交换器。
queue:队列,用于缓存消息。
BandingKey:绑定键,一个队列可以有一个到多个绑定键,通过绑定操作可以绑定交换器和队列,交换器会根据绑定键的名称找到对应的队列。
RotingKey:路由键,发送消息时,需要附带一条路由键,交换器会对路由键和绑定键进行匹配,如果匹配成功,则消息会转发到绑定键对应的队列中。
Consumer:消费者,负责处理消息。
交换器类型
注:这里只关注fanout、direct、topic三种类型,header类型没用过,不关注。
- fanout – 扇出型
用于支持发布、订阅模式(pub/sub)
交换器把消息转发到所有与之绑定的队列中。
扇出类型交换器会屏蔽掉路由键、绑定键的作用。 direct – 直接匹配
用于支持路由模式(Routing)
直接匹配交换器会对比路由键和绑定键,如果路由键和绑定键完全相同,则把消息转发到绑定键所对应的队列中。topic – 模式匹配
与直接匹配相对应,可以用一些模式来代替字符串的完全匹配。
规则:
以 ‘.’ 来分割单词。
‘#’ 表示一个或多个单词。
‘*’ 表示一个单词。
如:
RoutingKey为:
aaa.bbb.ccc
BindingKey可以为:
*.bbb.ccc
aaa.#
默认交换器
RMQ会自带几个交换器,简单看下,这里只介绍AMQP default。
当交换器名称为空时,表示使用默认交换器。空的意思是空字符串。
默认交换器是一个特殊的交换器,他无需进行绑定操作,可以以直接匹配的形式直接把消息发送到任何队列中。
下图两种模式均采用了默认交换器:
两个消费者从一个队列取数据时,会产生竞争条件。此时消息只能给其中的一个消费者。如果两个消费者均没有在收到消息后做应答操作,则消息会平均发送给两个消费者。如果收到消息后做了应答操作,则会采取能者多劳的模式。
创建交换器
func (ch *Channel) ExchangeDeclare(name, kind string, durable, autoDelete, internal, noWait bool, args Table) error
name:交换器的名称,对应图中exchangeName。
kind:也叫作type,表示交换器的类型。有四种常用类型:direct、fanout、topic、headers。
durable:是否持久化,true表示是。持久化表示会把交换器的配置存盘,当RMQ Server重启后,会自动加载交换器。
autoDelete:是否自动删除,true表示是。至少有一条绑定才可以触发自动删除,当所有绑定都与交换器解绑后,会自动删除此交换器。
internal:是否为内部,true表示是。客户端无法直接发送msg到内部交换器,只有交换器可以发送msg到内部交换器。
noWait:是否非阻塞,true表示是。阻塞:表示创建交换器的请求发送后,阻塞等待RMQ Server返回信息。非阻塞:不会阻塞等待RMQ Server的返回信息,而RMQ Server也不会返回信息。(不推荐使用)
args:直接写nil,没研究过,不解释。
创建队列
func (ch *Channel) QueueDeclare(name string, durable, autoDelete, exclusive, noWait bool, args Table) (Queue, error)
name:队列名称
durable:是否持久化,true为是。持久化会把队列存盘,服务器重启后,不会丢失队列以及队列内的信息。(注:1、不丢失是相对的,如果宕机时有消息没来得及存盘,还是会丢失的。2、存盘影响性能。)
autoDelete:是否自动删除,true为是。至少有一个消费者连接到队列时才可以触发。当所有消费者都断开时,队列会自动删除。
exclusive:是否设置排他,true为是。如果设置为排他,则队列仅对首次声明他的连接可见,并在连接断开时自动删除。(注意,这里说的是连接不是信道,相同连接不同信道是可见的)。
nowait:是否非阻塞,true表示是。阻塞:表示创建交换器的请求发送后,阻塞等待RMQ Server返回信息。非阻塞:不会阻塞等待RMQ Server的返回信息,而RMQ Server也不会返回信息。(不推荐使用)
args:直接写nil,没研究过,不解释。
队列绑定
func (ch *Channel) QueueBind(name, key, exchange string, noWait bool, args Table) error
name:队列名称
key:对应图中BandingKey,表示要绑定的键。
exchange:交换器名称
nowait:是否非阻塞,true表示是。阻塞:表示创建交换器的请求发送后,阻塞等待RMQ Server返回信息。非阻塞:不会阻塞等待RMQ Server的返回信息,而RMQ Server也不会返回信息。(不推荐使用)
args:直接写nil,没研究过,不解释。
交换器绑定
func (ch *Channel) ExchangeBind(destination, key, source string, noWait bool, args Table) error
源交换器根据路由键&绑定键把msg转发到目的交换器。
destination:目的交换器,通常是内部交换器。
key:对应图中BandingKey,表示要绑定的键。
source:源交换器。
nowait:是否非阻塞,true表示是。阻塞:表示创建交换器的请求发送后,阻塞等待RMQ Server返回信息。非阻塞:不会阻塞等待RMQ Server的返回信息,而RMQ Server也不会返回信息。(不推荐使用)
args:直接写nil,没研究过,不解释。
发送消息
func (ch *Channel) Publish(exchange, key string, mandatory, immediate bool, msg Publishing) error
exchange:要发送到的交换机名称,对应图中exchangeName。
key:路由键,对应图中RoutingKey。
mandatory:直接false,不建议使用,后面有专门章节讲解。
immediate :直接false,不建议使用,后面有专门章节讲解。
msg:要发送的消息,msg对应一个Publishing结构,Publishing结构里面有很多参数,这里只强调几个参数,其他参数暂时列出,但不解释。
# cat $(find ./amqp) | grep -rin type.*Publishing
type Publishing struct {
Headers Table
// Properties
ContentType string //消息的类型,通常为“text/plain”
ContentEncoding string //消息的编码,一般默认不用写
DeliveryMode uint8 //消息是否持久化,2表示持久化,0或1表示非持久化。
Body []byte //消息主体
Priority uint8 //消息的优先级 0 to 9
CorrelationId string // correlation identifier
ReplyTo string // address to to reply to (ex: RPC)
Expiration string // message expiration spec
MessageId string // message identifier
Timestamp time.Time // message timestamp
Type string // message type name
UserId string // creating user id - ex: "guest"
AppId string // creating application id
}
接受消息 – 推模式
RMQ Server主动把消息推给消费者
func (ch *Channel) Consume(queue, consumer string, autoAck, exclusive, noLocal, noWait bool, args Table) (<-chan Delivery, error)
queue:队列名称。
consumer:消费者标签,用于区分不同的消费者。
autoAck:是否自动回复ACK,true为是,回复ACK表示高速服务器我收到消息了。建议为false,手动回复,这样可控性强。
exclusive:设置是否排他,排他表示当前队列只能给一个消费者使用。
noLocal:如果为true,表示生产者和消费者不能是同一个connect。
nowait:是否非阻塞,true表示是。阻塞:表示创建交换器的请求发送后,阻塞等待RMQ Server返回信息。非阻塞:不会阻塞等待RMQ Server的返回信息,而RMQ Server也不会返回信息。(不推荐使用)
args:直接写nil,没研究过,不解释。
注意下返回值:返回一个<- chan Delivery类型,遍历返回值,有消息则往下走, 没有则阻塞。
接受消息 – 拉模式
消费者主动从RMQ Server拉消息
func (ch *Channel) Get(queue string, autoAck bool) (msg Delivery, ok bool, err error)
queue:队列名称
autoAck:是否开启自动回复。
手动回复消息
func (ch *Channel) Ack(tag uint64, multiple bool) error
func (me Delivery) Ack(multiple bool) error {
if me.Acknowledger == nil {
return errDeliveryNotInitialized
}
return me.Acknowledger.Ack(me.DeliveryTag, multiple)
}
func (d Delivery) Reject(requeue bool) error
简单看一眼,函数2调用了函数1,本质上两个函数没区别。
这里推荐使用第二个,因为方便。
另外说一下multiple参数。true表示回复当前信道所有未回复的ack,用于批量确认。false表示回复当前条目。
函数三:
拒绝本条消息。如果requeue为true,则RMQ会把这条消息重新加入队列,如果requeue为false,则RMQ会丢弃本条消息。
注:推荐手动回复,尽量不要使用autoACK,因autoACK不可控。
关闭连接
func (ch *Channel) Close() error
func (c *Connection) Close() error
简单看下,不解释了,按照流程最好有关闭一下,其实不关闭也没啥事。。
Publish – mandatory参数
false:当消息无法通过交换器匹配到队列时,会丢弃消息。
true:当消息无法通过交换器匹配到队列时,会调用basic.return通知生产者。
注:不建议使用,因会使程序逻辑变得复杂,可以通过备用交换机来实现类似的功能。
Publish – immediate参数
true:当消息到达Queue后,发现队列上无消费者时,通过basic.Return返回给生产者。
false:消息一直缓存在队列中,等待生产者。
注:不建议使用此参数,遇到这种情况,可用TTL和DLX方法代替(后面会介绍)。
备用交换机&TTL+DLX
借助备用交换机、TTL+DLX代替mandatory、immediate方案:
1、P发送msg给Ex,Ex无法把msg路由到Q,则会把路由转发给ErrEx。
2、msg暂存在Q上之后,如果C不能及时消费msg,则msg会转发到DlxEx。
3、TTL为msg在Q上的暂存时间,单位为毫秒。
通过设置参数,可以设置Ex的备用交换器ErrEx
创建Exchange时,指定Ex的Args – “alternate-exchange”:”ErrEx”。
其中ErrEx为备用交换器名称通过设置参数,可以设置Q的DLX交换机DlxEX
创建Queue时,指定Q的Args参数:
“x-message-ttl”:0 //msg超时时间,单位毫秒
“x-dead-letter-exchange”:”dlxExchange” //DlxEx名称
“x-dead-letter-routing-key”:”dlxQueue” //DlxEx路由键
持久化
持久化的作用是防止在重启、关闭、宕机的情况下数据丢失,持久化是相对靠谱的,如果数据在内存中,没来得及存盘就发生了重启,那么数据还是会丢失。
持久化可分为三类:
1、Exchange的持久化(创建时设置参数)
2、Queue的持久化(创建时设置参数)
3、Msg的持久化(发送时设置Args)
Qos
func (ch *Channel) Qos(prefetchCount, prefetchSize int, global bool) error
注意:这个在推送模式下非常重要,通过设置Qos用来防止消息堆积。
prefetchCount:消费者未确认消息的个数。
prefetchSize :消费者未确认消息的大小。
global :是否全局生效,true表示是。全局生效指的是针对当前connect里的所有channel都生效。
封装思路
简单说下,封装是因为这东西参数太多了,不够好用。所以又包了一层,让代码看起来更简洁一些,思路上参考了spring-rmq库,采用json或者xml配置文件来描述rmq中的各个组件。这样看起来会舒服些。
封装的参考代码:
https://gitee.com/vrg0/go-rabbitmq.git
注:喵自己封装的,没上过生产环境,功能上也不算特别全,好不好用也有待考验,姑且可当做一个参考吧。。。