滴滴面试:Rocketmq消息0丢失,如何实现?
文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
滴滴面试:Rocketmq消息0丢失,如何实现?
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团、蚂蚁、得物的面试资格,遇到很多很重要的面试题:
Rocketmq消息0丢失,如何实现?
Rocketmq如何保证消息可靠?
最近有小伙伴在面试滴滴,都到了相关的面试题,可以说是逢面必问。
小伙伴没有系统的去梳理和总结,所以支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V175版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取
消息的发送流程
Rocketmq和KafKa类似(实质上,最早的Rocketmq 就是KafKa 的Java版本),一条消息从生产到被消费,将会经历三个阶段:
- 生产阶段,Producer 新建消息,而后经过网络将消息投递给 MQ Broker。这个发送可能会发生丢失,比如网络延迟不可达等。
- 存储阶段,消息将会存储在 Broker 端磁盘中,Broker 根据刷盘策略持久化到硬盘中,刚收到Producer的消息在内存中了,但是如果Broker 异常宕机了,导致消息丢失。
- 消费阶段, Consumer 将会从 Broker 拉取消息
以上任一阶段, 都可能会丢失消息,只要这三个阶段0丢失,就能够完全解决消息丢失的问题。
宏观层面的大的阶段和流程,Rocketmq和KafKa类似的。
KafKa 零丢失,具体的文章:
生产阶段如何实现0丢失方式
生产阶段有三种send方法:
- 同步发送
- 异步发送
- 单向发送。
三种send方法的 客户端api,具体如下:
/**
* {@link org.apache.rocketmq.client.producer.DefaultMQProducer}
*/
// 同步发送
public SendResult send(Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {}
// 异步发送,sendCallback作为回调
public void send(Message msg,SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException {}
// 单向发送,不关心发送结果,最不靠谱
public void sendOneway(Message msg) throws MQClientException, RemotingException, InterruptedException {}
produce要想发消息时保证消息不丢失,可以采用同步发送的方式去发消息,send消息方法只要不抛出异常,就代表发送成功。
发送成功会有多个SendResult 状态,以下对每个状态进行说明:
- SEND_OK:消息发送成功,Broker刷盘、主从同步成功
- FLUSH_DISK_TIMEOUT:消息发送成功,但是服务器同步刷盘(默认为异步刷盘)超时(默认超时时间5秒)
- FlUSH_SLAVE_TIMEOUT:消息发送成功,但是服务器同步复制(默认为异步复制)到Slave时超时(默认超时时间5秒)
- SLAVE_NOT_AVAILABLE:Broker从节点不存在
注意:同步发送只要返回以上四种状态,就代表该消息在生产阶段消息正确的投递到了RocketMq,生产阶段没有丢失。
如果业务要求严格,可以只取SEND_OK标识消息发送成功, 其他类型的数据,采用40岁老架构师尼恩给大家设计的,业务维度的 终极0丢失保护措施:本地消息表+定时扫描 (具体参见本文末尾)
是同步发送还是异步发送
根据尼恩的架构设计40个黄金法则 ,AP 和 CP 是天然的矛盾, 到底是 CP 还是 AP的 需要权衡,
-
同步发送的方式 是 CP ,高可靠,但是性能低。
-
异步发送的方式 是 AP ,低可靠,但是性能高。
为了高可靠(CP),可以采取同步发送的方式进行发送消息,发消息的时候会同步阻塞等待broker返回的结果,如果没成功,则不会收到SendResult,这种是最可靠的。
其次是异步发送,再回调方法里可以得知是否发送成功。
最后,单向发送(OneWay)是最不靠谱的一种发送方式,我们无法保证消息真正可达。
当然,具体的如何选择高可用方案,还是要看业务。 也可以选择异步发送 + 业务维度的 终极0丢失保护措施 , 实现消息的0丢失。
生产端的失败重试策略
发送消息如果失败或者超时了,则会自动重试。
同步发送默认是重试三次,可以根据api进行更改,比如改为10次:
producer.setRetryTimesWhenSendFailed(10);
其他模式是重试1次,具体请参见源码
/**
* {@link org.apache.rocketmq.client.producer.DefaultMQProducer#sendDefaultImpl(Message, CommunicationMode, SendCallback, long)}
*/
// 自动重试次数,this.defaultMQProducer.getRetryTimesWhenSendFailed()默认为2,如果是同步发送,默认重试3次,否则重试1次
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
for (; times < timesTotal; times++) {
// 选择发送的消息queue
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
if (mqSelected != null) {
try {
// 真正的发送逻辑,sendKernelImpl。
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
switch (communicationMode) {
case ASYNC:
return null;
case ONEWAY:
return null;
case SYNC:
// 如果发送失败了,则continue,意味着还会再次进入for,继续重试发送
if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
continue;
}
}
// 发送成功的话,将发送结果返回给调用者
return sendResult;
default:
break;
}
} catch (RemotingException e) {
continue;
} catch (...) {
continue;
}
}
}
上面的核心逻辑中,调用sendKernelImpl真正的去发送消息
通过核心的发送逻辑,可以看出如下:
-
同步发送场景的重试次数是1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() =3,其他方式默认1次。
-
this.defaultMQProducer.getRetryTimesWhenSendFailed()默认是2,我们可以手动设置
producer.setRetryTimesWhenSendFailed(10);
-
如果是同步发送sync,且发送失败了,则continue,意味着还会再次进入for,继续重试发送
同步模式下,可以设置严格的消息重试机制,比如设置 RetryTimes为一个较大的值如10。当出现网络的瞬时抖动时,消息发送可能会失败,retries 较大,能够自动重试消息发送,避免消息丢失。
Broker端保证消息不丢失的方法:
首先,尼恩想说正常情况下,只要 Broker 在正常运行,就不会出现丢失消息的问题。
但是如果 Broker 出现了故障,比如进程死掉了或者服务器宕机了,还是可能会丢失消息的。
如果确保万无一失,实现Broker端保证消息不丢失,有两板斧:
- Broker端第一板斧:设置严格的副本同步机制
- Broker端第二板斧:设置严格的消息刷盘机制
Broker端第一板斧:设置严格的副本同步机制
RocketMQ 通过多副本机制来解决的高可用,核心思想也挺简单的:如果数据保存在一台机器上你觉得可靠性不够,那么我就把相同的数据保存到多台机器上,某台机器宕机了可以由其它机器提供相同的服务和数据。
首先,Broker需要集群部署,通过主从模式包括 topic 数据的高可用。
为了消息0丢失,可以配置设置严格的副本同步机制,等Master 把消息同步给 Slave后,才去通知Producer说消息ok。
设置严格的副本同步机制 , RocketMQ 修改broker刷盘配置如下:
所以我们还可以配置不仅是等Master刷完盘就通知Producer,而是等Master和Slave都刷完盘后才去通知Producer说消息ok了。
## 默认为 ASYNC_MASTER
brokerRole=SYNC_MASTER
Broker端第二板斧:设置严格的消息刷盘机制
RocketMQ持久化消息分为两种:同步刷盘和异步刷盘。
RocketMQ和kafka一样的,刷盘的方式有同步刷盘和异步刷盘两种。
-
同步刷盘指的是:生产者消息发过来时,只有持久化到磁盘,RocketMQ、kafka的存储端Broker才返回一个成功的ACK响应,这就是同步刷盘。它保证消息不丢失,但是影响了性能。
-
异步刷盘指的是:消息写入PageCache缓存,就返回一个成功的ACK响应,不管消息有没有落盘,就返回一个成功的ACK响应。这样提高了MQ的性能,但是如果这时候机器断电了,就会丢失消息。
同步刷盘和异步刷盘的区别如下:
- 同步刷盘:当数据写如到内存中之后立刻刷盘(同步),在保证刷盘成功的前提下响应client。
- 异步刷盘:数据写入内存后,直接响应client。异步将内存中的数据持久化到磁盘上。
同步刷盘和异步输盘的优劣:
- 同步刷盘保证了数据的可靠性,保证数据不会丢失。
- 同步刷盘效率较低,因为client获取响应需要等待刷盘时间,为了提升效率,通常采用批量输盘的方式,每次刷盘将会flush内存中的所有数据。(若底层的存储为mmap,则每次刷盘将刷新所有的dirty页)
- 异步刷盘不能保证数据的可靠性.
- 异步刷盘可以提高系统的吞吐量.
- 常见的异步刷盘方式有两种,分别是定时刷盘和触发式刷盘。定时刷盘可设置为如每1s刷新一次内存.触发刷盘为当内存中数据到达一定的值,会触发异步刷盘程序进行刷盘。
Broker端第二板斧:设置严格的消息刷盘机制,设置为Kafka同步刷盘。
RocketMQ默认情况是异步刷盘,Broker收到消息后会先存到cache里,然后通知Producer说消息我收到且存储成功。 Broker起个线程异步的去持久化到磁盘中,但是Broker还没持久化到磁盘就宕机的话,消息就丢失了。
同步刷盘的话是收到消息存到cache后并不会通知Producer说消息已经ok了,而是会等到持久化到磁盘中后才会通知Producer说消息完事了。
-
同步刷盘的方式 是 CP ,高可靠,但是性能低。
-
异步刷盘的方式 是 AP ,低可靠,但是性能高。
根据尼恩的架构设计40个黄金法则 ,AP 和 CP 是天然的矛盾, 到底是 CP 还是 AP的 需要权衡,
为了高可靠(CP),可以采取同步刷盘保障了消息不会丢失,但是性能不如异步高。
如何设置RocketMQ同步刷盘?
RocketMQ 修改broker刷盘配置如下:
## 默认情况为 ASYNC_FLUSH,修改为同步刷盘:SYNC_FLUSH,实际场景看业务,同步刷盘效率肯定不如异步刷盘高。
flushDiskType = SYNC_FLUSH
对应的RocketMQ源码类如下:
package org.apache.rocketmq.store.config;
public enum FlushDiskType {
// 同步刷盘
SYNC_FLUSH,
// 异步刷盘(默认)
ASYNC_FLUSH
}
异步刷盘默认10s执行一次,源码如下:
/*
* {@link org.apache.rocketmq.store.CommitLog#run()}
*/
while (!this.isStopped()) {
try {
// 等待10s
this.waitForRunning(10);
// 刷盘
this.doCommit();
} catch (Exception e) {
CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
}
}
Broker端0丢失的配置总结
Broker端的配置,若想很严格的保证Broker存储消息阶段消息不丢失,则需要如下配置
# master 节点配置
flushDiskType = SYNC_FLUSH
brokerRole=SYNC_MASTER
# slave 节点配置
brokerRole=slave
flushDiskType = SYNC_FLUSH
上面这个配置含义是:
Producer发消息到Broker后,Broker的Master节点先持久化到磁盘中,然后同步数据给Slave节点,Slave节点同步完且落盘完成后才会返回给Producer说消息ok了。
严格的消息刷盘机制 + 严格的消息同步机制,能够确保 Broker端保证消息不丢失
当然,根据尼恩的架构设计40个黄金法则 ,AP 和 CP 是天然的矛盾, 到底是 CP 还是 AP的 需要权衡,
Consumer(消费者)保证消息不丢失的方法:
如果要保证 Consumer(消费者)0 丢失, Consumer 端的策略是啥呢?
普通的情况下,rocketMq拉取消息后,执行业务逻辑。
一旦Consumer执行成功,将会返回一个ACK响应给 Broker,这时MQ就会修改offset,将该消息标记为已消费,不再往其他消费者推送消息。
如果出现消费超时(默认15分钟)、拉取消息后消费者服务宕机等消费失败的情况,此时的Broker由于没有等到消费者返回的ACK,会向同一个消费者组中的其他消费者间隔性的重发消息,直到消息返回成功(默认是重复发送16次,若16次还是没有消费成功,那么该消息会转移到死信队列,人工处理或是单独写服务处理这些死信消息)
但是 消费者,也有两种消费模式:
- 同步消费
- 异步消息
rocketMq 在并发消费模式下,默认有20个消费线程:
关于Rocketmq 的消息消费核心架构,请参见尼恩的硬核文章:
如何保证客户端的高可用,两种场景:
- 同步消费场景手动发送CONSUME_SUCCESS ,保证 消息者的0丢失
- 异步消费场景,需要通过业务维度的 终极0丢失保护措施:本地消息表+定时扫描 ,保证 消息者的0丢失
1、同步消费发送CONSUME_SUCCESS
同步消费指的是拉取消息的线程,先把消息拉取到本地,然后进行业务逻辑,业务逻辑完成后手动进行ack确认,这时候才会真正的代表消费完成。举个例子
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt msg : msgs) {
String str = new String(msg.getBody());
// 消费者 线程 同步进行 业务处理
System.out.println(str);
}
// ack,只有等上面一系列逻辑都处理完后,
// 发 CONSUME_SUCCESS才会通知broker说消息消费完成,
// 如果上面发生异常没有走到这步ack,则消息还是未消费状态。
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
2、异步消费场景,如何保证0丢失
rocketMq 在并发消费模式下,默认有20个消费线程,但是这个还是有限制的。
如果实现高性能呢?
-
可以一方面去进行线程扩容, 比如通过修改配置,扩容到100个rocketMq 同步消费线程,但是这个会在没有 活儿干的场景,浪费宝贵的CPU资源。
-
可以另一方便通过异步的 可动态扩容的业务专用线程池,去完成 消费的业务处理。那么,如果是设置了业务的专用线程池,则需要通过业务维度的 终极0丢失保护措施:本地消息表+定时扫描 ,保证 消息者的0丢失
关于可动态扩容的业务专用线程池,请参考尼恩的文章:
业务维度的 终极0丢失保护措施:本地消息表+定时扫描
40岁老架构师尼恩慎重提示:前面三板斧,并不能保证100%的0丢失。
因为百密一疏,任何环节的异常,都有可能导致数据丢失。
有没有业务维度的 终极保护措施呢? 有:本地消息表+定时扫描
本地消息表+定时扫描 方案,和本地消息表事务机制类似,也是采用 本地消息表+定时扫描 相结合的架构方案。
大概流程如下图
1、设计一个本地消息表,可以存储在DB里,或者其它存储引擎里,用户保存消息的消费状态
2、Producer 发送消息之前,首先保证消息的发生状态,并且初始化为待发送;
3、如果消费者(如库存服务)完成的消费,则通过RPC,调用Producer 去更新一下消息状态;
4、Producer 利用定时任务扫描 过期的消息(比如10分钟到期),再次进行发送。
在这里尼恩想说的是: 本地消息表+定时扫描 的架构方案 ,是业务层通过额外的机制来保证消息数据发送的完整性,是一种很重的方案。 这个方案的两个特点:
- CP 不是 AP,性能低
- 需要 做好幂等性设计
如果降低业务维度的 终极0丢失保护措施带来的性能耗损?
可以减少本地消息表的规模,对于正常投递的消息不做跟踪,只把生产端发送失败的消息、消费端消费失败的消息记录到数据库,并启动一个定时任务,扫描发送失败的消息,重新发送直到超过阈值(如10次),超过之后,发送邮件或短信通知人工介入处理。
CP 不是 AP的 需要权衡,请参见全网最好的架构设计个黄金法则,尼恩的 专门文章具体如下:
全网最好的幂等性 方案,请参见尼恩的 专门文章, 具体如下:
RocketMQ的0丢失的最佳实践
-
Producer端:使用同步发送方式,发送消息。
记住,一定要使用带有回调通知的 send 方法。
-
Producer端:同步模式下,可以设置严格的消息重试机制,比如设置 RetryTimes为一个较大的值如10。当出现网络的瞬时抖动时,消息发送可能会失败,retries 较大,能够自动重试消息发送,避免消息丢失。。
-
Broker 端设置严格的副本同步机制 。
-
Broker 端 设置严格的消息刷盘机制。
-
Consumer 端 确保消息消费完成再提交。可以使用同步消费,并发送CONSUME_SUCCESS。
-
业务维度的的0丢失架构, 采用 本地消息表+定时扫描 架构方案,实现业务维度的 0丢失,100%可靠性。
如上,就是尼恩为大家梳理的,史上最牛掰的 答案, 全网最为爆表的方案。按照尼恩的套取去回到, 面试官一定惊到掉下巴。 offer直接奉上。此答案大家可以收藏一起,有时间看看。
说在最后:有问题找老架构取经
Rocketmq消息0丢失,如何实现?,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。
最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。
遇到职业难题,找老架构取经, 可以省去太多的折腾,省去太多的弯路。
尼恩指导了大量的小伙伴上岸,前段时间,刚指导一个40岁+被裁小伙伴,拿到了一个年薪100W的offer。
狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。
技术自由的实现路径:
实现你的 架构自由:
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
… 更多架构文章,正在添加中
实现你的 响应式 自由:
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
实现你的 spring cloud 自由:
《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
实现你的 linux 自由:
实现你的 网络 自由:
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
实现你的 分布式锁 自由:
实现你的 王者组件 自由:
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》