1、备份交换器Alternate Exchange
生产者发送消息时如果不设置mandatory,消息未被路由则会丢失;如果设置了mandatory需要添加ReturnListener,增加了代码复杂性。如果不想消息丢失,可以使用备份交换器来存储未被路由的消息。可以在exchangeDeclare方法里的args参数列表里添加alternate-exchange参数来给声明的exchange添加Alternate Exchange,再将AE绑定至队列。
AE建议设置为fanout类型防止消息丢失,消息发送到AE时routing key不变。备份交换器和mandatory一起使用时mandatory无效。
2、过期时间TTL
(1)设置消息的TTL
可以设置队列的属性,使队列中所有消息具有相同的过期时间(在队列args参数列表里添加x-message-ttl属性,不设置表示不会过期,设置为0表示除非可以立即投递至消费者,否则丢弃,可以设置TTL为0,配合死信队列来实现immediate的功能)。
针对每条消息设置TTL时,在channel.basicPublish方法的props参数里设置expiration属性。
如果通过队列设置消息的TTL,消息过期就会从队列中抹去(过期的消息在队列头部),如果针对每条消息设置TTL投递到消费者之前才会判断消息是否过期。
(2)设置队列的TTL
可以在channel.queueDeclare的args里添加x-expires参数来控制队列被自动删除前处于未使用状态的时间(即keepAliveTime)。如果队列上没有任何消费者,队列也没有被重新声明,也没有调用过Basic.Get,则keepAliveTime之后删除队列。
3、死信队列
当消息变成死信之后(消息被拒绝、过期,或者队列达到最大长度),可以被重新发送到死信交换器(DLX,Dead-Letter-Exchange),绑定DLX的队列叫死信队列。
通过在channel.queueDeclare方法中设置x-dead-letter-exchange来为队列添加DLX,通过x-dead-letter-routing-key指定路由键,不指定则使用原routing key。DLX可以处理消费者未被正确消费的异常。
4、延迟队列、优先级队列
RabbitMQ不自带DelayQueue,但是可以通过TTL和DLX实现DelayQueue,消费者订阅死信队列,设置TTL,可以实现延迟时间为TTL的DelayQueue。延迟队列常用与订单系统(30 min内支付,智能设备在指定时间时工作等场景)。
可以在queueDeclare中添加x-max-priority来设置队列的优先级,或者在basicPublish中设置消息的优先级。如果消息没有堆积,设置优先级没什么意思,因为消息立刻被消费了。
5、RPC(Remote Procedure Call)
RPC协议有CORBA、Java RMI、WebService的RPC风格、Hessian、Thrift、Restful API等。
如果为每个RPC请求创建一个回调队列非常低效,可以为每个客户端创建一个回调队列,通过correlationId为请求设置Id,回调队列来关联request和response。在basicPublish方法的BasicProperties里,通过replyTo参数设置回调队列,通过correlationId关联request和response。如果回调队列接收到未知correlationId的回复,将其丢弃。
6、持久化
持久化分为交换器、队列和消息的持久化。
交换器不设置持久化,RabbitMQ重启后不能将消息发送到该交换器中。需要同时设置队列和消息的持久化防止消息丢失。如果只设置队列的持久化,重启后消息会丢失;只设置消息的持久化,重启后队列会消失,继而消息也丢失。
即使设置了三者的持久化,消息也有可能丢失。如果设置autoAck为true,消息可能会丢失;持久化的消息存入RabbitMQ之后,还需要一段时间才能存入磁盘(RabbitMQ不会为每条消息调用内核的fsync方法),可能保存在系统缓存里,如果此时Broker宕机,消息会丢失。可以引入镜像队列机制,相当于配置了副本。如果master挂掉切换到slave。
7、生产者确认
确认消息是否到达,RabbitMQ提供了事务机制(低效)和发送方确认机制。
(1)事务机制(与数据库事务概念不同,这里是为了确保消息到达,不是ACID)
channel.txSelect将当前信道设置为事务模式,channel.txCommit提交事务,channel.txRollback回滚事务。如果txCommit提交成功,消息一定到达了,否则回滚。事务机制效率很低。
(2)发送方确认机制(publish confirm)
生产者将channel置为confirm模式,一旦channel进入confirm模式,该channel上的消息会被指派从1开始的ID,消息被投递到队列里时Broker返回Basic.Ack给生产者,如果消息和队列是持久化的,ACK在消息写入磁盘后发出。Broker回传给生产者的确认消息中的deliveryTag包含了确认消息的序号,Broker也可以设置basicAck里的multiple参数表示之前的消息都已处理。
事务机制发送一条消息后会阻塞等待Broker回应,相当于停等协议,发送方确认机制相当于TCP滑动窗口,这点和消费者的推模式、拉模式类似。
事务机制和publish confirm机制只保证消息被正确发送至Exhange,如果没有匹配的队列消息会丢失,发送方需要配合mandatory或者备份交换器保证可靠性。
publish confirm机制可以通过channel.waitForConfirms方法批量comfirm,如果有一条消息NACK需要将这一批消息重发,不适用于消息经常丢失的场景;也可以通过在channel中添加ConfirmListener异步confirm,需要维护未ACK的消息序号的集合(最好使用SortedSet结构)。
8、消费端要点
(1)消息分发
Broker默认将第m条消息发送给第m%n个消费者,但是因为每个消费者的业务复杂度、机器性能不一会造成资源的浪费,可以通过channel.basicQos设置最大未ACK数量(basicQos对于拉模式无效)。
void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;
prefetchSize表示未ACK消息大小的上线,0表示无上限;prefetchCount表示预取数量,即未ACK消息数量,0表示无上限。一个信道可以消费多个队列,设置了prefetchCount时,channel要和多个队列协调保证不超过prefetchCount。global为false是表示channel上新的Consumer需要遵循prefetchCount的限制;为true时表示channel上所有Consumer要遵循prefetchCount的限制。
channel.basicQos(3, false);
channel.basicQos(5, true);
如上代码表示每个消费者最多只能收到3个未ACK消息,channel里所有消费者能收到未ACK的消息数之和为5。像这样设置两种global模式会消耗资源完成限制,最好使用默认的global = false。
(2)消息顺序性
多个生产者同时发送无法保证顺序性。事务回滚、NACK、超时、设置优先级等情况均会导致消息顺序性被打破,如果需要保证顺序性,业务方要进一步处理,比如在消息体内添加全局标识(SequenceId)。
(3)弃用QueueingConsumer
QueueingConsumer内部使用LinkedBlockingQueue缓存消息,如果不设置Basic.Qos容易OOM。QueueingConsumer还会拖累Connection下的所有channel,降低性能;会产生死锁;RabbitMQ的自动连接恢复机制不支持QueueingConsumer;QueueingConsumer不是事件驱动的。通常使用DefaultConsumer。
9、消息传输保障
消息中间件的传输保障分为At most once(最多一次,消息可能丢失,不会重复传输)、At least Once(最少一次,可能重复传输)、Exactly once(恰好一次),RabbitMQ支持最多一次和最少一次。
最少一次的投递需要考虑(1)publish confirm机制(正确传输到Exchange);(2)mandatory参数(从Exchange路由到队列是不被丢弃);(3)队列、消息、Exchange持久化;(4)autoAck为false,手动ACK。