MQ有点对点(P2P,Point-to-Point)和发布/订阅(Pub/Sub)两种模式。RabbitMQ属于P2P模式,但也可以通过将消息写入多个队列来实现发布订阅,如图所示。
MQ的作用概括如下:解耦(两边处理过程独立)、冗余(存储)、扩展性、削峰、可恢复性、顺序保证(一定程度上)、缓冲(控制数据流速度)、异步通信。
RabbitMQ是Erlang实现的AMQP(高级消息队列协议)的消息中间件,两者的模型架构一样。AMPQ协议可以看作结构化命令的集合,有Module Layer(定义命令)、Session Layer(为通信提供可靠性机制)、Transport Layer(物理层、链路层)三层。RabbitMQ有可靠性、路由灵活、扩展性、高可用性(镜像)、支持多种协议、多语言客户端、管理界面、插件机制等特点。运行时可以通过rabbitmq-server -detached以守护线程的方式在后台运行,默认端口号5672。
从生产者到消费者,分为Producer,Exchange,Queue和Consumer,Exchange和Queue合称Broker,可以看作一个RabbitMQ服务节点,即一台服务器。Queue实际存储消息,而Exchange相对而言并不消耗性能。
消息分为消息体(payload)和标签(label),标签表述这条消息,比如exchange名称或者routing key,消息路由时标签会丢弃,进入队列的只有消息体。
RabbitMQ不支持队列层面的广播消费,多个消费者订阅同一队列时会轮询,一条消息只能被一个Consumer消费,如果需要广播消费需要二次开发或者使用冗余的队列。
Producer发送给Exchange时需要routing key,Exchange绑定Queue时需要binding key,两者是不一样的,需要联合使用才能生效,但有时会统称为路由键,同一组Exchange和Queue绑定时可以使用多个binding key。
Exchange有fanout(把消息路由到所有与该Exchange绑定的Queue中)、direct(routing key=binding key)、topic(模糊匹配,如图)、headers(绑定Exchange和Queue时指定一组键值对,只有与消息体中的headers完全一致时才会路由到该队列,性能差,不实用)四种类型。
RabbitMQ的API中通过channel(信道)来执行操作(创建Exchange和Queue等),channel是建立在Connection(TCP连接)之上的虚拟连接,复用了TCP连接,适用于channel流量不大时,流量很大时多开辟Connection。每个channel有唯一的ID,通常线程和channel一一对应。
如下列出了一些常用API。
// 设置最大未确认ACK数量
channel.basicQos(64);
// 拉模式
channel.basicGet();
// 请求Broker重新发送未被确认的消息
channel.basicRecover();
// 发送消息
channel.basicPublish();
// 确认
channel.basicAck();
// 声明交换器
channel.exchangeDeclare();
// 声明队列
channel.queueDeclare();
// 队列与交换器绑定
channel.queueBind();
(1)Connection,channel
ConnectionFactory factory = new Connection Factory();
factory.setUri("amqp://userName:password@ipAddress:portNumber/virtualHost");
Connection conn = factory.newConnection();
Channel channel = conn.createChannel();
也可以单独设置factory的这些参数。应用程序应该为每一个线程开辟一个channel,channel有isOpen()方法,if(channel.isOpen()){...},但要在if最外层加同步,不推荐使用,如果channel使用时处于关闭状态会抛出ShutdownSignalException。
(2)exchangeDeclare
Exchange.DeclareOk exchangeDeclare(String exchange, String type, boolean durable, boolean autoDelete, boolean internal, Map arguments) throws IOEXception;
exchangeDeclare有多个重载方法,exchange是交换器名称,type是交换器类型,durable表示是否持久化,autoDelete是否自动删除,自动删除的前提是至少有一个Queue或者Exchange与该Exchange绑定之后,都与该Exchange解绑,即无Queue/Exchange与该Exchange绑定时。internal表示是否是内置交换器,内置交换器无法直接接收消息,只能通过Exchange路由到内置交换器。argument表示其他参数。
此外还有void exchangeDeclareNoWait方法,不等待Broker返回(exchangeDeclare等待Broker返回Exchange.DeclareOk), 不推荐使用,因为客户端可能会在exchange没创建好时就使用它。
(3)queueDeclare
Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map argments) throws IOException;
不带任何参数的queueDeclare默认创建排他的、自动删除的、非持久化的队列。排他队列基于Connection可见,同一个Connection里的不同channel可共用,如果一个Connection声明了排他队列,其他Connection不允许声明同名队列。如果Connection关闭或者客户端退出,该排他队列会被自动删除(无论是否持久化),适用于一个客户端同时发送和读取消息的场景。
autoDelete的前提是有消费者连接到这个队列,之后所有与这个队列连接的消费者都断开。
如果订阅了一个队列,就无法声明队列了,必须取消订阅,将信道置为传输模式才能声明队列。即同一个channel不能同时生产和消费。
同样的,也有queueDeclareNoWait方法,还有Queue.PurgeOk queuePurge(String queue) throws IOException方法清空队列(不删除队列)。
(4)queueBind,绑定队列和交换器
Queue.BindOk queueBind(String queue, String exchange, String routingKey, Map arguments) throws IOException;
(5)exchangeBind,交换器与交换器绑定
Exchange.BindOk exchangeBind(String destination, String source, String routingKey, Map arguments) throws IOException;
(6)basicPublish,发送消息
void basicPublish(String exchange, String routingKey, boolean mandatory, boolean imediate, BasicProperties props, byte[] body) throws IOException;
props是消息的基本属性集,有contentType, contentEncoding, headers, deliveryMode, priority, correlationId, replyTo, expiration, messageId, timestamp, type, userId, appId, clusterId等14个属性。
当mandatory为true时,Exchange找不到合适队列时会将消息返回给生产者(Basic.return),为false时丢弃消息,生产者通过channel.addReturnListener添加ReturnListener监听器来获取丢弃的消息。
当immediate为true时,如果队列上没有消费者,则将消息返回给生产者,不用将消息存入队列等待消费者。即mandatory找不到队列时返回,immediate找不到消费者时返回。RabbitMQ 3.0之后去掉了对immediate的支持,官方的解释是会影响镜像队列的功能,增加复杂性,建议采用TTL和DLX(死信交换器)替代(设置很短的TTL,没有消费者则进入DLX)。
(7)消费分为推模式(Push,持续接收,Basic.Consume)和拉模式(Pull,接收单条,Basic.Get)
推模式:
String basicConsume(String queue, boolean autoAck, String consumerTag, boolean noLocal, boolean exclusive, Map arguments, Consumer callback) throws IOException;
Basic.Consume将channel置为投递模式。当autoAck为false,会等待消费者显示ACK(Basic.Ack)之后才从内存或磁盘中移除消息;为true时,发送完消息会自动删除,会导致消息丢失。如果RabbitMQ一直没有收到确认信号,并且消费者断开了连接,会将消息重新发送给下一个消费者。RabbitMQ不会为未确认的消息设置过期时间,重新投递的唯一依据是消费者连接是否已经断开(RabbitMQ允许消费者消费消息很久,所以如果消费者很久未收到消息,可以重新连接)。
consumerTag用来区分多个消费者;noLocal为true表示不能将同一个Connection中生产者的消息发送给该Connection中的消费者;exclusive设置是否排他;Consumer的实现通常选用DefaultConsumer,重写handleDelivery方法。通常的做法是一个channel对应一个消费者,也可以对应多个,但是同一时刻只有一个消费者在消费。
拉模式
GetResponse basicGet(String queue, boolean autoAck) throws IOException;
单条地获取消息,不能循环get来代替Basic.Consume,性能很差,Basic.Get类似于TCP里的停等协议,而Basic.Consume类似于滑动窗口,可以通过Basic.Qos限制最大未ACK的消息数量。
(8)确认与拒绝
void basicReject(long deliveryTag, boolean requeue) throws IOException;
basicReject只能拒绝单条消息,deliveryTag可以看做消息编号,requeue表示是否重新入队和发送。
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
multiple为false时和basicReject方法一样,为true表示拒绝deliveryTag编号之前所有未ACK的消息。
Basic.RecoverOk basicRecover(boolean requeue) throws IOException;
重发消息,requeue为true可能会分配给不同的消费者(默认),为false发送给原消费者。
(9)关闭连接
channel.close()不是必须的,connection.close()的时候channel会自动关闭。channel和connection有Open、Closing、Closed三种状态,转变为Closed时会调用ShutDownListener,可以获取到关闭原因。如果将ShutDownListener注册到Closed的对象上会立即执行。