RabbitMQ系列-如何保证消息的可靠传输
消息的可靠投递除了需要硬件,网络,消息中间件等的可靠保证外,还需要生产者,消费者来共同保证来完成。一条消息从生产者产生,到发送到交换机,并被投递到队列,并最终被消费者消费,这整个路径上,途径的每一个地方都要保证消息的可靠性。
其实,官方文档Reliability Guide已经总结了消息系统安全的方方面面。
- 网络方面可以使用心跳检测TCP连接:Detecting Dead TCP Connections with Heartbeats
- 分布式的RabbitMQ可以用Federation and Shovel来实现
- 可以通过建立监控来监视服务器状况:Monitoring and Health Checks
除了上面这几点外,还需要下面一系列的方法来共同保证消息的可靠传输。
一、持久化-(Data Safety on the Broker Side)
官方文档:Data Safety on the Broker Side
将交换机、队列和消息设为durable
①交换机和队列持久化
spring代码中,交换机和队列默认都是持久化的
②消息持久化
需要将消息的投递模式(delivery_mode)设置为2(也就是持久化)。
当我们使用RabbitTemplate调用了convertAndSend(String exchange, String routingKey, final Object object) 方法。默认就是持久化模式。
注意:
- 持久化的消息在到达队列时就被写入到磁盘,并且如果可以,持久化的消息也会在内存中保存一份备份,这样可以提高一定的性能,只有在内存吃紧的时候才会从内存中清除。
- 非持久化的消息一般只保存在内存中,在内存吃紧的时候会被换入到磁盘中,以节省内存空间。
但要注意的是,将所有的消息都设置为持久化,会严重影响RabbitMQ的性能,写入硬盘的速度比写入内存的速度慢的不只一点点。对于可靠性不是那么高的消息可以不采用持久化处理以提高整体的吞吐率,在选择是否要将消息持久化时,需要在可靠性和吞吐量之间做一个权衡。
在某种应用场景,如大流量的订单交易系统,为了不影响性能,我们可以不设置持久化,但是我们会定时扫描数据库中的未发送成功的消息,进行重试发送,实际应用场景,我们其实有很多解决方案。
二、生产者消息确认机制-(Acknowledgements and Confirms)
当消息发送出去之后,我们如何知道消息有没有正确到达exchange呢?如果在这个过程中,消息丢失了,我们根本不知道发生了什么,也不知道是什么原因导致消息发送失败了,为解决这个问题,主要有如下两种方案:
-
通过事务机制实现
-
通过生产者消息确认机制(publisher confirm)实现
但是使用事务机制实现会严重降低RabbitMQ的消息吞吐量,我们采用一种轻量级的方案——生产者消息确认机制。
①生产者消息确认机制
消息确认机制就是生产者发送的消息一旦被投递到匹配的队列之后,交换机就会发送一个确认消息给生产者,生产者就知晓消息已经正确到达了目的地。 如果消息和队列是持久化存储的,那么确认消息会在消息写入磁盘之后发出。
具体实现:通过实现ConfirmCallBack接口,消息发送到Exchange后触发回调。
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { //未收到确认 if (!ack) { log.debug("消息投递到exchange失败,原因: {}", cause); } } });
这里仅仅是记录了日志文件,后续还要对消息进行重试发送,可以根据业务的需要使用立即重试或者在未来的某个时间点重试。
重试机制
当然,使用生产者消息确认机制需要考虑的另外一个问题是由于网络断开等原因导致生产者收不到ack确认,那么对于生产者来说可能会有两种结果:
- 消息没有发送成功,自然消费者也不会消费,此时生产者需要重发消息
- 消息已发送成功但没收到确认,消费者很大程度已经消费了消息,此时无需重发消息
对于后者,一种可能的处理方式是在生产者确认回调方法中去验证(查询)一下消费者对应的业务是否对该消息进行了处理(根据消息对应的业务的id),但很显然这样是非常不合理的。因为消费者可能有多个,需要一一去验证,同时这也与引入消息队列实现生产者和消费者的解耦相悖。所以最合理的处理方式就是生产者进行重试,但消费者要进行幂等处理了。可以参考RocketMQ消息幂等处理。
对于立即重试,因为可能是网络故障,所以依然于事无补,所以可以指定一定的重试次数,或者可以同时调整重试时间间隔(1,2,4,8……)。对于后者,可以将未成功发送的消息记录到数据库日志表,然后使用一个定时器去定时扫描日志表,然后调用生产者重新发送消息。
具体实现如下:
RabbitTemplate有一个确认回调接口,该接口中有个confirm方法。只需要实现该回调接口,并在confirm方法中判断是否收到确认,如果未收到确认,则记录日志(消息id,失败原因),然后将消息id和消息内容保存到redis,然后启动一个定时任务来重发消息。
public class PublishConfirmCallback implements RabbitTemplate.ConfirmCallback { private static final Logger LOGGER = LoggerFactory.getLogger(PublishConfirmCallback.class); @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { //未收到确认消息,重试 if (!ack) { LOGGER.warn("mq send message error: id [{}] cause [{}]", correlationData.getId(), cause); //todo : add id and info to redis; schedule a job to resend message; //todo : 将id和信息加入redis,启动定时任务来重发消息 } } }
对于生产者重试的实现可以参考博客:rabbitmq可靠发送的自动重试机制
为了防止消息丢失,以及对消息收发进行有效跟踪,需要在发送消息的时候记录,消息接收处理后删除。可以在发消息的时候,同时往redis或数据库里面同时写一份。
在消息发送和接收时记录DB日志,定时轮询DB日志,查明哪些发送消息没有成功消费,启动重新发送消息机制。
②mandatory参数
补充一个Mandatory参数。当Mandatory参数设为true时,如果目的不可达,会将消息返还给生产者,生产者通过一个回调函数可以获取该信息。
三、消费者消息手动确认机制-(Acknowledgements and Confirms)
为了保证消息从队列可靠地到达消费者,RabbitMQ提供了消费者消息确认机制(message acknowledgement)。采用消息确认机制之后,消费者就有足够的时间来处理消息,不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会一直等待并持有消息,直到消费者确认了该消息。
但默认情况下消费者是自动 ack (确认)消息的,也就是消费者接收到消息后会立即回复一个ack确认消息,而无论消费者是否对消息成功进行了处理。如果消费者在处理消息时发生了异常,此时就无法保证业务的一致性了,这样显然是不合理的。所以,可以设置为消费者手动确认,一旦消息者成功消费就会手动发送确认消息,mq收到确认后就会将消息从队列中删除。但需要注意的是,消费者一定要做幂等处理,从而避免进行重复处理。
参考博客:spring-boot + rabbitmq消息手动确认模式的几点说明(重试机制)
四、死信队列-(Unprocessable Deliveries)
官方文档:dead letter
什么是死信交换机
DLX,Dead Letter Exchange 的缩写,又死信邮箱、死信交换机。DLX就是一个普通的交换机,和一般的交换机没有任何区别。 当消息在一个队列中变成死信(dead message)时,通过这个交换机将死信发送到死信队列中(指定好相关参数,rabbitmq会自动发送)。什么是死信呢?什么样的消息会变成死信呢?
-
消息被拒绝(basic.reject或basic.nack)并且requeue=false
-
消息TTL过期
- 队列达到最大长度(队列满了,无法再添加数据到mq中)
如何使用死信交换机
在定义业务队列的时可以考虑指定一个死信交换机,并绑定一个死信队列,当消息变成死信时该消息就会被发送到该死信队列上,这样就方便我们查看消息失败的原因了。
定义业务(普通)队列的时候指定参数:
-
x-dead-letter-exchange: 用来设置死信后发送的交换机
-
x-dead-letter-routing-key:用来设置死信的routingKey
@Bean public Queue helloQueue() { //将普通队列绑定到私信交换机上 Map<String, Object> args = new HashMap<>(2); args.put(DEAD_LETTER_QUEUE_KEY, deadExchangeName); args.put(DEAD_LETTER_ROUTING_KEY, deadRoutingKey); Queue queue = new Queue(queueName, true, false, false, args); return queue; }
五、集群模式-(Clustering and Message Replication)
参考博客:RabbitMQ的集群模式
除了上面讲的基本可靠性保证外,其实还有很多性能优化方案、可靠性保证方案:集群监控、流控、镜像队列、HAProxy+Keeplived高可靠负载均衡