使用任务队列一个优点是能够轻易地并行处理任务。当处理大量积压的任务,只要增加“Worker”,通过这个方式,能够实现轻易的缩放。
Round-robin dispatching:
默认地,RabbitMQ会逐一地向下一个“Consumer”发放消息,每一个“Consumer”会得到数目相同的消息。
这种发放消息的方式叫Round-ronbin dispaching。
Message acknowledgment:
当“Consumer”接受到一个消息并作长时间处理时,有可能发生意外状况,如运行“Consumer”的机器突然关闭,这时这个消息所要执行的任务可能没有得到正确处理。
我们不希望有任务丢失或者没有正确处理,RabbitMQ支持Message acknowledgment来解决这个问题。
当“Consumer”接收到消息、处理任务完成之后,会发送带有这个消息标示符的ack,来告诉RabbitMQ这个消息接收到并处理完成。
RabbitMQ会一直等到处理某个消息的“Consumer”的链接失去之后,才确定这个消息没有正确处理,从而RabbitMQ重发这个消息。
Message acknowledgment是默认关闭的。
初始化“Consumer”时有个auto参数,如果设置为true,这个Consumer在收到消息之后会马上返回ack。
我们的应用应该是在消息的任务处理完之后再ack,因此初始化“Consumer”时这个参数应该置为false。实例代码:
QueueingConsumer consumer = new QueueingConsumer(channel);
boolean autoAck = false;
channel.basicConsume("hello", autoAck, consumer);
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
//我们的任务处理
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
忘记ack是一个容易犯并且后果很严重的错误。RabbitMQ会侵占越来越多的内存,因为它不会释放没有被ack的消息。
可以使用rabbitmqctl去debug这个错误。实例:
rabbitmqctl list_queues name message_rady message_unacknowleded
Message durability:
Message acknowledgment解决了如果“Consumer”异常之后,任务得到保证。但是这并不能保证服务器异常之后任务能不丢失。Message durability正是用来解决这个问题。
为了实现Message durability,我们应该做两个设置:消息队列和消息本身。
声明消息队列时应该将durable参数置为true,消息的properties做设置,一个实例:
//channel设置
boolean durable = true;
channel.queueDeclare("hello", durable, false, false, null);
//publish的消息properties设置
channel.basicPublish("", "task_queue",
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
这里有一个有意思的提示,RabbitMQ不能重复声明队列名称相同但是属性不一样的队列。属性如durable。
关于message持久性的note:
即使做了上述的几步来确保消息和任务不丢失,但是这样依然有几率丢失消息。
RabbitMQ没有为每一个消息做fsync(2)(不知道这个是什么),消息可能只被保存在cache中,而没有真正写入硬盘,这时服务器突然异常就可能产生丢失。即使这个概率发生很小,但是如果需要更强大的保障,考虑用事件去包装发布消息的过程。
Fair dispatch:
RabbitMQ默认只会用Round-robin的方式盲目地将消息派发给下一个“Consumer”,它不顾及“Consumer”还有多少个消息没有ack。
为解决这个问题,我们可以使用basicQos方法来限制“Consumer”没有ack的消息数目。实例:
int prefetchCount = 1;
channel.basicQos(prefetchCount);
如果这样做也有个问题,如果消息无法派发(“Consumer”的unack消息太多),消息多的可能使消息队列溢出。
exchange:
在实际的情况下,消息发送者不会直接将Message发入queue,而是发入exchange,exchange能通过指定的规则将message放入指定的queue中(public/subscribe模型)。exchange有direct,topic,headers和fanout等几种类型。
声明exchange的方法如下:
channel.exchangeDeclare(String name, String type);
fanout是一种广播模式,将消息派发到所有的绑定到该exchange的queque中。
查看rabbitMQ中的exchange可以使用rabbitmqctl list_exchanges,查看exchange和queue的绑定关系可以使用rabbitmqctl list_bindings
basicPublish()方法中,如果exchange置为"",将使用默认的exchange,这时将消息放入名字为routingKey参数的值的队列。
channel.queueDeclare()不带参数的方法将生产一个non-durable(服务器重启后,这个queue不将存在), exclusive(这个queue只能被这个链接使用), autodelete(这个queue不再使用的时候会被删除)并且名字随机生成的queue,这种queue非常适合做广播接收queue
direct exchange:
这种exchange的routing-key只有一个单词,exchange根据这个key将消息派发到对应的queue中。
topic exchange:
topic exchange的routing-key不能是任意,它必须是一个一些词的数组,用"."隔开,每个词可以是任意的,但通常是代表某些含义的。这个词的数组不能超过255个字节。
"*"可以代表任意一个词
"#"可以代表任意数目的词(0个或更多)。
topic exchange是非常强大的,它几乎能实现其他exchange的routing规则。
自己的note:
一个exchange能用n条routing-key规则去指向一个queue。
如果一个Message符合n条规则去往同一个queue,这个Message只会被投放到这个queue一次。
RPC:
Remote Procedure Call(远程过程调度)。Client向远程的计算机请求一个耗时较长的任务,并等待其返回结果。
RPC尽管是非常常见的一种模式,但是它有许多不足之处。RPC会带来一些不可预知的错误并增加了调试的复杂性。开发人员可能会困惑哪个是远程调用哪个是本地调用,或者说哪个RPC是耗时较长的调用。
开发RPC时,记住以下的建议:
1)将远程调用和本地调用清楚的区别
2)为系统编写文档,将组件的依赖关系清楚的分离。
3)着手准备处理意外事件,要考虑到远程调用的服务器意外关闭之后该如何处理。
用RabbitMQ做RPC是非常容易的,客户端将Message发送到服务器,在Message里应该带上一个回调queue的地址,服务器通过这个地址将结果返回给客户端。
Message一共有14种属性,常见的有这么几个:
1)deliveryMode:标识这个Message是持久性的还是短暂性,如果是持久性并且保持这个Message的queue是durable的,服务器重启之后这个消息如果未被Consumer处理,就会恢复这个Message并加入到queue中。
2)contentType: 标识消息内容的MIME,例如JSON用application/json
3)replayTo: 标识回调的queue的地址
4)correlationId:用于request和response的关联,确保消息的请求和响应的同一性。