说明:本文内容来源于对amqp-client
和spring-rabbit
包源码的解读及debug
,尽可能保证内容的准确性。
rabbitmq
消费过程示意如下:
图中首字母大写的看上去像类名的,如ConsumerWorkService
,MainLoop
,WorkPoolRunnable
等,没错就是类名,可自行根据类名去查看相关源码。
下面解释上图的含义。
1. 启动流程
- 通过
BeanPostProcessor
扫描所有的bean
中存在的@RabbitListener
注解及相应的Method
; - 由
RabbitListenerContainerFactory
根据配置为每一个@RabbitListener
注解创建一个MessageListenerContainer
,持有@RabbitListener
注解及Method
信息; - 初始化
MessageListenerContainer
,主要是循环依次创建consumer
(AsyncMessageProcessingConsumer
类),启动consumer
; - 创建
consumer
,过程包括:创建AMQConnection
(仅第一次创建),创建AMQChannel
(每个consumer
都会创建),发送消费queue
的请求(basic.consume
),接收并处理消息; AMQConnection
持有连接到rabbitmq server
的Socket
,创建完成后启动MainLoop
循环从Socket
流中读取Frame
,此时流中没有消息,因为channel
还没创建完成;- 创建
AMQChannel
(一个AMQConnection
中持有多个AMQChannel
),并将创建完成的channel
注册到AMQConnection
持有的ConsumerWorkService
,实际就是添加到WorkPool
类的Map
里面去,此时Socket
流中也没有消息,因为channel
还没有与queue
绑定; - 创建完成的
AMQChannel
的代理返回给consumer
,consumer
通过channel
发送消费queue
的请求到rabbitmq server
(绑定成功),此时还没开始处理消息,但Socket
流中已经有消息,并且已经被connection
读取到内存(即BlockingQueue<Runnable>
)中,并且已经开始向BlockingQueue<Delivery>
分发; consumer
启动循环,从BlockingQueue<Delivery>
中取消息,利用MessageListenerContainer
中持有的Method
反射调用@RabbitListener
注解方法处理消息。
2. 消费流程
rabbitmq server
往Socket
流中写入字节。AMQConnection
启动一个main loop thread
来跑MainLoop
,不断从Socket
流中读取字节转换成Frame
对象,这是每个connection
唯一的数据来源。
Frame对象结构如下:
type
:指定当前Frame
的类型,如method(1)
、message header(2)
、message body(3)
、heartbeat(8)
等;
channel
:channel
的编号,从0~n
排列,指定当前Frame
需要交给哪个channel
处理。channel-0
为一类,channel-n
为一类。channel-0
是一个匿名类,用来处理特殊Frame
,如connection.start
。channel-n
都是ChannelN
类,由ChannelManager
类统一管理。
payload
:当前Frame
的具体内容。
consumer
启动后,connection
读取到的Frame
如上图所示(一个consumer
的情况,多个consumer
的Frame
可能会交替)。从basic.deliver
开始是消息的内容,每条消息分成三个Frame
:第一个是method
,basic.deliver
代表这是一个消息,后面一定会再跟着两个Frame
;第二个是message header
;第三个是message body
,body
读取之后将三个Frame
整合到一起转换成一条完整的deliver
命令。
AMQConnection
根据读取到的Frame
中的type
决定要怎么处理这个Frame
:heartbeat(8) do nothing
;其它的根据channel
编号交给相应的AMQChannel
去处理,(编号为0
的是特殊的channel
,消息相关的用的都是编号非0
的channel
),消息都会拿着这个编号到ChannelManager
找对应的ChannelN
处理。ChannelN
经过一系列中间过程由Frame
(消息是三个Frame
)得到了Runnable
,将(ChannelN, Runnable) put
到ConsumerWorkService
持有的WorkPool
里面的一个Map<Channel, BlockingQueue<Runnable>>
里面去。这样这个Runnable
就进入了与ChannelN
对应的BlockingQueue<Runnable>
(写死的size=1000
)里面了。execute
一个WorkPoolRunnable
,执行的任务是:从WorkPool
中找出一个ready
状态的ChannelN
,把这个ChannelN
设为inProgress
状态,从对应的BlockingQueue<Runnable>
中取最多16
(写死的)个Runnable
在WorkPoolRunnable
的线程里依次执行(注意:此处不再另开线程,所以可能会堵塞当前线程,导致这个ChannelN
长时间处于inProgress
状态),执行完后将当前ChannelN
状态改为ready
,并在当前线程execute
另一个WorkPoolRunnable
。BlockingQueue<Runnable>
里面的Runnable
执行的逻辑是:构造一个Delivery put
到与ChannelN
对应的AsyncMessageProcessingConsumer
持有的BlockingQueue<Delivery>
(size=prefetchCount
可配置)里面去(如果消息处理速度太慢,BlockingQueue<Delivery>
已满,此处会堵塞)。- 每个
AsyncMessageProcessingConsumer
都有一个独立的线程在循环从BlockingQueue<Delivery>
一次读取一个Delivery
转换成Message
反射调用@RabbitListener
注解方法来处理。
3. 无ack消费模式与有ack消费模式对比
根据以上对消费过程的分析,将无ack
模式与ack
模式进行对比。
无ack
模式(AcknowledgeMode.NONE
)
server
端行为
rabbitmq server
默认推送的所有消息都已经消费成功,会不断地向消费端推送消息。- 因为
rabbitmq server
认为推送的消息已被成功消费,所以推送出去的消息不会暂存在server
端。
消息丢失的风险
当BlockingQueue<Runnable>
堆满时(BlockingQueue<Delivery>
一定会先满),server
端推送消息会失败,然后断开connection
。消费端从Socket
读取Frame
将会抛出SocketException
,触发异常处理,shutdown
掉connection
和所有的channel
,channel shutdown
后WorkPool
中的channel
信息(包括channel inProgress
,channel ready
以及Map
)全部清空,所以BlockingQueue<Runnable>
中的数据会全部丢失。
此外,服务重启时也需对内存中未处理完的消息做必要的处理,以免丢失。
而在rabbitmq server
,connection
断掉后就没有消费者去消费这个queue
,因此在server
端会看到消息堆积的现象。
有ack
模式(AcknowledgeMode.AUTO
,AcknowledgeMode.MANUAL
)
AcknowledgeMode.MANUAL
模式需要人为地获取到channel
之后调用方法向server
发送ack
(或消费失败时的nack
)信息。
AcknowledgeMode.AUTO
模式下,由spring-rabbit
依据消息处理逻辑是否抛出异常自动发送ack
(无异常)或nack
(异常)到server
端。
server
端行为
rabbitmq server
推送给每个channel
的消息数量有限制,会保证每个channel
没有收到ack
的消息数量不会超过prefetchCount
。server
端会暂存没有收到ack
的消息,等消费端ack
后才会丢掉;如果收到消费端的nack
(消费失败的标识)或connection
断开没收到反馈,会将消息放回到原队列头部。
这种模式不会丢消息,但效率较低,因为server
端需要等收到消费端的答复之后才会继续推送消息,当然,推送消息和等待答复是异步的,可适当增大prefetchCount
提高效率。
注意,有ack
的模式下,需要考虑setDefaultRequeueRejected(false)
,否则当消费消息抛出异常没有catch
住时,这条消息会被rabbitmq
放回到queue
头部,再被推送过来,然后再抛异常再放回…死循环了。设置false
的作用是抛异常时不放回,而是直接丢弃,所以可能需要对这条消息做处理,以免丢失。更详细的配置参考这里。
对比
- 无
ack
模式:效率高,存在丢失大量消息的风险。 - 有
ack
模式:效率低,不会丢消息。