消费者在开启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"两条消息。

怎么设置rabbitmq的qos rabbitmq prefetchcount设置_RabbitMQ

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

prefetch_count

prefetch_count

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<String, Object> args = new HashMap<String, Object>();
args.put("x-max-length", 10);
channel.queueDeclare("myqueue", false, false, false, args);

2. 队列中的消息投递给消费者时的处理

当rabbitmq要将队列中的一条消息投递给消费者时,会遍历该队列上的消费者列表,选一个合适的消费者,然后将消息投递出去。其中挑选消费者的一个依据就是看消费者对应的channel上未ack的消息数是否达到设置的prefetch_count个数,如果未ack的消息数达到了prefetch_count的个数,则不符合要求。当挑选到合适的消费者后,中断后续的遍历。

rabbit_amqqueue_process.erl
deliver_msgs_to_consumers(_DeliverFun, true, State) ->
    {true, State};
deliver_msgs_to_consumers(DeliverFun, false,
                          State = #q{active_consumers =
    case priority_queue:out_p(ActiveConsumers) of
        {empty, _} ->
            {false, State};
        {{value, QEntry, Priority}, Tail} ->
            {Stop, State1} =
             deliver_msg_to_consumer(DeliverFun, QEntry,
                                     Priority,
                                      State#q{active_consumers =
                                             Tail}),
         %%如果处理结果为false,遍历下一个消费者
          deliver_msgs_to_consumers(DeliverFun, Stop, State1)
   end.
deliver_msg_to_consumer(DeliverFun, E = {ChPid, Consumer},
                        Priority, State) ->
    %%判断是否可以将消息投递给该消费者
    case rabbit_limiter:can_send(C#cr.limiter,
                                 Consumer#consumer.ack_required,
                                 Consumer#consumer.tag) of
        %%可以投递,再将该消费者放到队列的尾部
        {continue, Limiter} ->
            AC1 = priority_queue:in(E, Priority,
                                    State#q.active_consumers),
            %%将消息投递给消费者
            deliver_msg_to_consumer0(DeliverFun, Consumer,
                                     C#cr{limiter = Limiter},
                                  State#q{active_consumers = AC1})
3  ...
rabbit_limiter.erl
handle_call({can_send, QPid, AckRequired}, _From,
        State = #lim{volume = Volume}) ->
  case prefetch_limit_reached(State) of
      %%未ack的消息数达到prefetch_count设置的个数
      true  -> {reply, false, limit_queue(QPid, State)};
   false -> {reply, true,
                  %%消息需要被ACK, volume加1rabbit_limiter.erl
2
 
3
handle_cast({ack, Count}, State = #lim{volume = Volume}) ->
4
    NewVolume = if Volume == 0 -> 0;
5
                   true        -> Volume - Count
6
                end,
7
    {noreply, maybe_notify(State, State#lim{volume = NewVolume})};

                 State#lim{volume = if AckRequired -> Volume + 1;
                                     true        -> Volume
                                 end}}
    end
prefetch_limit_reached(#lim{prefetch_count = Limit,
                            volume = Volume}) ->
    Limit =/= 0 andalso Volume >= Limit.

3. 消费者对消息ack后的处理

当消费者对消息进行ack后,最终会修改该消费者对应channel中未ack的消息数,这样队列又可以将消息投递给该消费者。

rabbit_limiter.erl
handle_cast({ack, Count}, State = #lim{volume = Volume}) ->
    NewVolume = if Volume == 0 -> 0;
                true        -> Volume - Count
    {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