丢失消息则丢了数据,这是我们不能接受的,否则MQ意义何在?
因此主流MQ其实都提供了可靠性投递机制,确保即使网络异常,消息也能可靠传递,而不会丢失。
如果发现还是丢失消息了,多半是开发者问题,很可能没有正确配置MQ。不同MQ在保证消息可靠传递方面的实现原理其实也是一样的。
大公司一般都通过分布式链路追踪系统,很方便追踪每条消息。 如果是中小公司,也有个简单方案验证。即利用MQ的有序性:
大多MQ客户端支持拦截器,可在Pro发消息前的拦截器中注入序号到消息中,在Con收消息的拦截器中检测序号连续性。
分布式系统下实现验证方法,须注意:
如果系统的Producer多实例,由于并不好协调多Producer之间的发送顺序,所以也需要每个Producer分别生成消息序号,且需要附加Producer标识,在Con端按每个Pro分别验证序号连续性。
Consumer实例数量最好和分区数量一一对应,如此便可方便在Con内验证消息序号连续性。
有小伙伴要问了,到底哪些地方会导致丢消息,又该如何避免呢?
在Producer创建消息出来,通过网络传输发送到Broker。
MQ通过最常用的请求确认机制保证消息可靠传递: 调用发消息方法时,MQ客户端把消息发至于Broker,Broker收到后,给客户端返回确认响应,表明已收。客户端收到响应后,完成一次正常消息的发送。
只要Pro收到Broker的确认,即可保证消息在生产阶段不会丢失。
写发消息代码时,注意正确处理返回值或捕获异常,即可保证该阶段消息不会丢失。
以Kafka为例看可靠发送消息:
同步发送时,只要注意捕获异常即可。
try {
RecordMetadata metadata = producer.send(record).get();
System.out.println("消息发送成功。");
} catch (Throwable e) {
System.out.println("消息发送失败!");
System.out.println(e);
}
异步发送,则需在回调方法检查。 很多丢消息的原因即在使用异步发送时,却未在回调里检查发送结果。
producer.send(record, (metadata, exception) -> {
if (metadata != null) {
System.out.println("消息发送成功。");
} else {
System.out.println("消息发送失败!");
System.out.println(exception);
}
});
消息在Broker端存储,如果是集群,消息会在该阶段被复制到其他副本。
正常情况下,只要Broker正常运行,就不会丢失消息。 但若Broker异常,比如进程卡死或服务器宕机,则可能丢失消息。
如果对消息可靠性要求非常高,可通过配置Broker参数避免因为宕机丢消息。
对单节点Broker,需配置Broker参数:在收消息后,将消息写进磁盘后再给Pro发确认响应,这即使宕机,也不会丢消息,恢复后还可继续消费。
若Broker属多节点集群,需配置Broker集群:至少将消息发到2个以上节点,再给客户端发确认响应。如此一来,当某Broker宕机,其它Broker可替代宕机节点,也不会发生消息丢失。
Con从Broker拉消息,经过网络传输发到Con。
该阶段采用和生产阶段类似的确认机制保证可靠传递。 客户端从Broker拉取消息后,执行用户的消费业务逻辑,成功后,才会给Broker发送消费确认响应。如果Broker没有收到消费确认响应,下次拉消息的时候还会返回同一条消息,确保消息不会在网络传输过程中丢失,也不会因为客户端在执行消费逻辑中出错导致丢失。
编写消费代码时注意,不要在收到消息后就立即发消费确认,而在执行完所有消费业务逻辑后,再发送消费确认。 见如下以Python语言消费RabbitMQ消息为例,实现可靠的消费代码:
def callback(ch, method, properties, body):
print(" [x] 收到消息 %r" % body)
# 在这儿处理收到的消息
database.save(body)
print(" [x] 消费完成")
# 完成消费业务逻辑后发送消费确认响应
ch.basic_ack(delivery_tag = method.delivery_tag)
channel.basic_consume(queue='hello', on_message_callback=callback)
在消费的回调方法callback中,正确顺序是
这样如果保存消息到数据库失败,就不会执行消费确认代码,下次拉到的还是该消息,直至消费成功。
一条消息从发送到消费整个流程,MQ如何确保消息可靠投递不丢失。该过程可分三阶段,每阶段都需正确编写代码且正确配置,才能配合MQ可靠机制,确保消息不丢失。
理解这几阶段原理后,若再出现丢消息,可通过在代码中加日志,定位哪个阶段出问题了,再进一步分析。
拉消息时,消费者A pull后,没有发确认给Broker就宕机(或因代码问题超时阻塞),这时消息应该还在Broker,消费者B如果此时pull,是否会拉取到那条给消费者A的消息?即 首先,MQ一般都有协调机制,不会让这情况出现。但由于网络不确定性,这情况极小概率也会出现。 在同一消费组内,A消费者拉走index=10的消息,还没发确认,这时这分区的消费位置还是10,B消费者来拉消息,可能有2种情况:
若消息在网络传输过程发送错误,由于发送方收不到确认,会通过重发保证消息不丢失。但若确认响应在网络传输时丢失,也会导致重发消息。即无论Broker、Consumer都可能收到重复消息,编写消费代码时,就得考虑这情况。 在消费消息的代码中,该如何处理这种重复消息,才不会影响业务逻辑的正确性呢?
产生重复消息原因:
一般消息中都会存在个唯一性东西。不管是MQ本身的msgId
还是业务订单号之类,可在DB中存在一个消费表,对这唯一性东西建立唯一索引。 每次处理消费者逻辑前先insert,让DB帮我们去重。
解决方案:业务端去重
当Pro发消息给Broker时(send方法),此方法会在Broker收到消息并正常储存后才返回,期间应该会阻塞,即如果Broker配置同步刷盘,可能会增加调用时间(只能出现对消息敏感场景)。
======================= 消费端支持幂等操作,业务上一般有难度。可考虑消费端增加去冗余机制,例如缓存最新消费成功的N条消息的SN,收到消息后,先确认是否是消费过的消息,如果是,直接ACK并放弃消费。 思路没问题。
======================= 幂等性是一种办法,如果做不到幂等性,那么在消费端需要存储消费的消息ID,关键这个ID什么时候存?
不用,Producer发消息时带着ProducerId并要指定分区发送,Consumer消费时,需按照每个Producer来检查序号连续性。
======================= 如果Producer的某条消息ack因网络故障丢失,那么Producer此时重发消息的唯一标识应该和之前那条消息一样,那只需在Consumer接受消息前判断是否有相同标识的消息,如果有则拦截。 还可在消费端业务逻辑接口中做幂等判断,前面那种可以做到不侵入到业务代码。
非常好!但需要考虑,在分布式环境中“Consumer接受消息前判断是否有相同标识的消息”该如何实现呢?
======================= 检测消息丢失是在还没上线之前做的测试,但是会不会可能在线下没出现消息不一致,但是在线上的时候出现消息丢失了?线上检测消息丢失逻辑会关闭,那线上是会有其他的检测机制么? 这个检测逻辑可以在线上做,不会影响业务。