RabbitMQ 基本功能
一、安装 go-amqp 库
go get github.com/streadway/amqp
二、连接 RabbitMQ
一个 Connection 可以用来创建多个 Channel 实例,但是 Channel 实例不能在线程间共享,应用程序应该为每一个线程开辟一个 Channel。
多线程间共享 Channel 实例是非线程安全的。
1. 创建 TCP 连接(Connection)
conn, err := amqp.Dial("amqp://root:shiajun666@192.168.3.104:5672")
if err != nil {
fmt.Println("Connect to RabbitMQ failed: ", err)
return
}
defer conn.Close()
2. 创建信道(Channel)
ch, err := conn.Channel()
if err != nil {
fmt.Println("Open channel failed: ", err)
return
}
defer ch.Close()
三、使用交换器和队列
1. 声明交换器
err := ch.ExchangeDeclare(
"HelloEx", //name:交换器名称。
"direct", //kind:交换器类型。有四种类型:fanout、direct、topic、headers,定义了消息的路由规则。
true, //durable:是否持久化。持久化可以将交换器存盘,在服务器重启的时候不会丢失相关信息。
false, //autoDelete:是否自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,
// 之后所有与这个交换器绑定的队列或者交换器都与此解绑。
false, //internal:是否是内置的。若为内置,客户端程序无法直接发送消息到这个交换器中,只能通过交
// 换器路由到交换器这种方式。
false, //noWait:是否不等待声明结果。若为true,则客户端不会等待RabbitMQ server返回当前交换器声
// 明的结果。
nil, //args:其他一些结构化参数。
)
如果交换器不存在,RabbitMQ server 会创建它,若已存在,则会校验其是否符合参数指定的交换器。符合则什么都不做,返回成功,不符合则报错。
同一个交换器与同一个队列存在多个路由键的绑定关系时,即使满足多个路由键,消息也只会被路由一次。
返回错误时会关闭 Channel。
2. 声明队列
q, err := ch.QueueDeclare(
"helloQ", //name:队列名称。
true, //durable:是否持久化。持久化的队列会存盘,在服务器重启的时候可以保证不丢失相关信息。
false, //autoDelete:是否自动删除。自动删除的前提是:至少有一个消费者连接到这个队列,之后所有与
// 这个队列连接的消费者都断开时,才会自动删除。
false, //exclusive:是否排他。如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在
// 连接断开时自动删除。排他队列是基于连接可见的,同一个TCP连接的不同信道可以同时
// 访问同一连接创建的排他队列;其他连接不允许建立同名的排他队列;即使该队列是持久
// 化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除。
false, //noWait:是否不等待声明结果。若为true,则客户端不会等待RabbitMQ server返回当前队列声
// 明的结果。
nil, //args:其他一些结构化参数。
)
生产者和消费者都可以声明一个交换器或者队列。如果尝试声明一个已经存在的交换器或者队列,只要声明的参数完全匹配现存的交换器或者队列,RabbitMQ 就可以什么都不做,并成功返回。如果声明的参数不匹配则会抛出异常。
每一个声明的队列都会默认与一个名称为 ""(空字符串)、类型为 direct 的交换器绑定,绑定路由键为队列名称。
若声明队列时 noWait 参数为 true,那么如果 RabbitMQ server 还未将队列创建完成,此时客户端紧接着使用这个交换器,必然会发生异常。如果没有特殊的缘由和应用场景,并不建议使用这个方法。
如果消费者在同一个信道上订阅了另一个队列,就无法再声明队列了。必须先取消订阅,然后将信道置为“传输”模式,之后才能声明队列。
3. 绑定交换器和队列
err := ch.QueueBind(
"helloQ", //name:队列名称
"HelloEx2helloQ", //key:绑定路由键
"HelloEx", //exchange:交换器名称
false, //noWait:是否不等待绑定结果
nil, //args:其他一些结构化参数
)
如果该绑定关系已存在,则绑定请求会被忽略。
无论队列或交换器是否自动删除,持久化队列都只能绑定到持久化交换器。
绑定失败会返回错误,同时关闭 channel。
4. 解绑交换器和队列
err := ch.QueueUnbind(
"helloQ", //name:队列名称
"HelloEx2helloQ", //key:绑定路由键
"HelloEx", //exchange:交换器名称
nil, //args:其他一些结构化参数
)
若 exchange 参数为空,表示删除队列与默认交换器之间的绑定关系。
5. 绑定交换器和交换器
err := ch.ExchangeBind(
"helloEx1", //destination:目的交换器名称
"HelloEx2helloEx1", //key:绑定路由键
"HelloEx", //source:源交换器名称
false, //noWait:是否不等待绑定结果
nil, //args:其他一些结构化参数
)
同一个交换器与同一个交换器存在多个路由键的绑定关系时,即使满足多个路由键,消息也只会被路由一次。
绑定失败时 channel 会被关闭。
6. 解绑交换器与交换器
err := ch.ExchangeUnBind(
"helloEx1", //destination:目的交换器名称
"HelloEx2helloEx1", //key:绑定路由键
"HelloEx", //source:源交换器名称
false, //noWait:是否不等待绑定结果
nil, //args:其他一些结构化参数
)
RabbitMQ 的消息存储在队列中,交换器的使用并不真正耗费服务器的性能,而队列会。
若交换器、队列及其绑定关系固定,可在运行业务程序之前将它们创建并绑定完成,而不是在业务逻辑中实现。
四、发送消息
//组装消息
msg := amqp.Publishing{
ContentType: "text/plain", //消息内容类型
DeliveryMode: amqp.Persistent, //消息传输类型:1 amqp.Transient 不管队列是否持久化,消息都不会被持久
// 2 amqp.Persistent 只有队列是持久化的,消息才会持久化,否则消息同样不会持久化
Priority: 0, //消息优先级:0 - 9
ReplyTo: "", //address to to reply to (ex: RPC)
Expiration: "", //消息有效期
MessageId: "", //message identifier
Timestamp: time.Now(), //消息发送时间戳
Type: "", //消息类型
UserId: "", //creating user id - ex: "guest
AppId: "", //creating application id
Body: []byte("Hello World"), //消息内容
}
//发送消息
err = ch.Publish(
"helloEx", //exchange:源交换器名称,如果设置为空字符串,则消息会被发送到RabbitMQ默认的交换器中。
"helloEx2helloQ", //key:路由键
false, //mandatory:
false, //immediate:
msg, //msg:发送消息,数据类型为 amqp.Publishing
)
从客户端发送消息到rabbitmq server中对应的交换器。
流转过程:
五、接收消息
RabbitMQ 的消费模式分两种:推(Push)模式和拉(Pull)模式。
1. 推模式(Push)
//开启消息传输
msgCh, err := ch.Consume(
"helloQ", //queue:队列名称
"helloC", //consumer:消费者,消费者由一个唯一字符串标识。
false, //autoAck:是否自动确认,若为true,消息写入网络之前,rabbitmq server会先默认确认消费者已收到消息。
false, //exclusive:是否排他,若为true,rabbitmq server会确保当前消费者是此队列唯一的消费者;
// 若为false,rabbitmq server会在队列的多个消费者之间公平分配消息。
false, //noLocal:设置为true则表示不能将同一个Connection中生产者发送的消息传送给这个Connection中的消费者。
// RabbitMQ不支持这个参数。
false, //noWait:是否不等待请求结果。
nil, //args:其他一些结构化参数
)
//接收消息
for {
select {
case msg := <-msgCh:
fmt.Println("Receive message: ", string(msg.Body))
}
}
未接收的传递将会阻塞同一连接上的所有方法。
流转过程:
2. 拉模式(Pull)
3. 消费端的确认与拒绝
//开启消息传输
msgCh, err := ch.Consume(
"helloQ",
"helloC",
false,
false,
false,
false,
nil,
)
//接收消息
for msg := range msgCh {
fmt.Println("Receive message: ", string(msg.Body))
//确认
msg.Ack(
false, //multiple:若为true,则当前消息和同一通道上所有先前未确认的消息将被确认,一般用于消息批处理。
)
//拒绝
msg.Reject(
true, //requeue:若为true,rabbitmq server会将被消费端拒绝的消息重新入列,若为false,则会丢弃消息。
)
//批量拒绝
msg.Nack(
true, //multiple:若为true,表示拒绝当前消息以及此前未确认的所有消息,若为false,则与Reject方法功能一样。
true, //requeue:若为true,rabbitmq server会将被消费端拒绝的消息重新入列,若为false,则会丢弃消息。
)
}
推模式的 Consume() 方法第三个参数 autoAck 表示是否自动确认。当 autoAck 等于 false 时,RabbitMQ 会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移去消息(实质上是先打上删除标记,之后再删除)。当 autoAck 等于 true 时,RabbitMQ 会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正地消费到了这些消息。
当 autoAck 等于 false 时,如果 RabbitMQ 一直没有收到消费者的确认信号,并且消费此消息的消费者已经断开连接,则 RabbitMQ 会安排该消息重新进入队列(没有确认接收的消息将会被重新加入当前消息队列的末尾)。
RabbitMQ 不会为未确认的消息设置过期时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开,这么设计的原因是 RabbitMQ 允许消费者消费一条消息的时间可以很久很久。
对于每个未自动确认的消息,必须调用 Delivery.Ack、Delivery.Reject 或 Delivery.Nack 进行拒绝或接受。消费者最好先处理业务逻辑,再调用 Delivery.Ack 确认已收到消息,防止消息丢失。
RabbtiMQ 的 Web 管理平台上可以看到当前队列中的“Ready”状态和“Unacknowledged”状态的消息数,分别对应上文中的等待投递给消费者的消息数和已经投递给消费者但是未收到确认信号的消息数。
部分图片来自《RabbitMQ 实战指南》