原文地址:http://my.oschina.net/hncscwc/blog/195560
消费者在开启acknowledge的情况下,对接收到的消息可以根据业务的需要异步对消息进行确认。
然而在实际使用过程中,由于消费者自身处理能力有限,从rabbitmq获取一定数量的消息后,希望rabbitmq不再将队列中的消息推送过来,当对消息处理完后(即对消息进行了ack,并且有能力处理更多的消息)再接收来自队列的消息。在这种场景下,我们可以通过设置basic.qos信令中的prefetch_count来达到这种效果。
先直观的看看设置了prefetch_count的效果,:
1) 对比测试:两个消费者都订阅同一队列,no_ack均设置为false即开启acknowledge机制,且均未设置prefetch_count,向队列发布5条消息
结果:不管消息是否被ack,rabbitmq会轮流向两个消费者投递消息,第一个消费者收到"1","3","5"三条消息, 第二个消费者收到"2","4"两条消息。
2)prefetch_count设置测试:两个消费者都订阅同一队列,开启acknowledge机制,第一个消费者prefetch_count设置为1,另一个消费者未设置prefetch_count,同样向队列发布5条消息
结果:rabbitmq向第一个消费者投递了一条消息后,消费者未对该消息进行ack,rabbitmq不会再向该消费者投递消息,剩下的四条消息均投递给了第二个消费者
看完效果后,再来看看rabbitmq里的一些实现。
1. rabbitmq对basic.qos信令的处理
首先,basic.qos是针对channel进行设置的,也就是说只有在channel建立之后才能发送basic.qos信令。
在rabbitmq的实现中,每个channel都对应会有一个rabbit_limiter进程,当收到basic.qos信令后,在rabbit_limiter进程中记录信令中prefetch_count的值,同时记录的还有该channel未ack的消息个数。
注:其实basic.qos里还有另外两个参数可进行设置,prefetch_size和global,但是RabbitMQ没有实现prefetch_size,并在3.3.0版本中对global这个参数的含义进行了重新定义,即glotal=true时表示在当前channel上所有的consumer都生效,否则只对设置了之后新建的consumer生效
global |
Meaning of prefetch_count in AMQP 0-9-1 |
Meaning of prefetch_count in RabbitMQ |
false |
shared across all consumers on the channel |
applied separately to each new consumer on the channel |
true |
shared across all consumers on the connection |
shared across all consumers on the channel |
一个 queue 中消息最大保存量可以在声明 queue 的时候通过设置 x-max-length 参数为非负整数进行指定。Queue 长度的选取需要考量 就绪消息量、被忽略的未确认消息量,以及消息大小。当 queue 中的消息量达到了设定的上限时,为了给新消息腾出空间,将会从该 queue 用于保存消息的队列的前端将“老”消息丢弃或者 dead-lettered 。
下面的 Java 示例展示了如何声明一个最多保存 10 条消息的 queue :
Map args = new HashMap();
args.put("x-max-length", 10);
channel.queueDeclare("myqueue", false, false, false, args);
2. 队列中的消息投递给消费者时的处理
当rabbitmq要将队列中的一条消息投递给消费者时,会遍历该队列上的消费者列表,选一个合适的消费者,然后将消息投递出去。其中挑选消费者的一个依据就是看消费者对应的channel上未ack的消息数是否达到设置的prefetch_count个数,如果未ack的消息数达到了prefetch_count的个数,则不符合要求。当挑选到合适的消费者后,中断后续的遍历。
01 |
rabbit_amqqueue_process.erl |
03 |
deliver_msgs_to_consumers(_DeliverFun, true , State) -> |
05 |
deliver_msgs_to_consumers(DeliverFun, false , |
06 |
State = #q{active_consumers = |
08 |
case priority_queue:out_p(ActiveConsumers) of |
11 |
{{value, QEntry, Priority}, Tail} -> |
13 |
deliver_msg_to_consumer(DeliverFun, QEntry, |
15 |
State#q{active_consumers = |
17 |
%%如果处理结果为 false ,遍历下一个消费者 |
18 |
deliver_msgs_to_consumers(DeliverFun, Stop, State1) |
21 |
deliver_msg_to_consumer(DeliverFun, E = {ChPid, Consumer}, |
25 |
case rabbit_limiter:can_send(C#cr.limiter, |
26 |
Consumer#consumer.ack_required, |
27 |
Consumer#consumer.tag) of |
29 |
{ continue , Limiter} -> |
30 |
AC1 = priority_queue:in(E, Priority, |
31 |
State#q.active_consumers), |
33 |
deliver_msg_to_consumer0(DeliverFun, Consumer, |
34 |
C#cr{limiter = Limiter}, |
35 |
State#q{active_consumers = AC1}) |
40 |
handle_call({can_send, QPid, AckRequired}, _From, |
41 |
State = #lim{volume = Volume}) -> |
42 |
case prefetch_limit_reached(State) of |
43 |
%%未ack的消息数达到prefetch_count设置的个数 |
44 |
true -> {reply, false , limit_queue(QPid, State)}; |
45 |
false -> {reply, true , |
47 |
State#lim{volume = if AckRequired -> Volume + 1 ; |
52 |
prefetch_limit_reached(#lim{prefetch_count = Limit, |
54 |
Limit =/= 0 andalso Volume >= Limit. |
3. 消费者对消息ack后的处理
当消费者对消息进行ack后,最终会修改该消费者对应channel中未ack的消息数,这样队列又可以将消息投递给该消费者。
3 |
handle_cast({ack, Count}, State = #lim{volume = Volume}) -> |
4 |
NewVolume = if Volume == 0 -> 0 ; |
7 |
{noreply, maybe_notify(State, State#lim{volume = NewVolume})}; |
4. 注意
prefetch_count在no_ask=false的情况下生效,即在自动应答的情况下这两个值是不生效的
C客户端里,用amqp_basic_qos函数来设置
参考:
http://www.rabbitmq.com/consumer-prefetch.html