MQ - 如何保证消息不丢失?处理重复消息?消息堆积处理?

消息队列(MQ)是一种在分布式系统中用于解耦服务、实现异步处理和流量控制的技术。常见MQ如Kafka、RabbitMQ等。其工作原理包括队列模型和发布/订阅模型,确保消息不丢失的关键在于生产者、Broker和消费者的正确处理。消息重复可通过幂等性设计来解决,消息堆积则需分析消费端性能并采取扩容、优化策略。面试中,理解并掌握这些知识点至关重要。

什么是消息队列

在百度百科中,消息队列是这么解释的:“消息队列”是在消息的传输过程中保存消息的容器。

消息队列全称为英文 Message Queue 简称(MQ)是一种应用程序对应用程序的通信方法。MQ 是消费-生产者模型的一个典型的代表,一端往消息队列中不断写入消息,而另一端则可以取队列中的消息。消息发布者(生产者)只管把消息发布到 MQ 中而不用管谁来取,消息使用者(消费方)只管从 MQ 中取消息而不用管是谁发布的。

目前市面上的消息队列有很多,例如:Kafka、RabbitMQ、ActiveMQ、RocketMQ、ZeroMQ等。

为什么需要消息队列

从本质上来说是因为互联网的快速发展,业务不断扩张,促使技术架构需要不断的演进。

从以前的单体架构到现在的微服务架构,成百上千的服务之间相互调用和依赖。从互联网初期一个服务器上有 100 个在线用户已经很了不得,到现在坐拥 10 亿日活的微信。我们需要有一个东西来解耦服务之间的关系、控制资源合理合时的使用以及缓冲流量洪峰等等。

消息队列就应运而生了。消息队列的应用场景:异步处理、服务解耦、流量控制。

异步处理

随着公司的业务发展会发现项目的请求链路会越来越长,例如刚开始的电商项目,流程就是扣库存、下单。慢慢地又加上积分服务、短信服务等。这一路同步调用下来需要的时间就很长,客户也会等得不耐烦了,有没有更好的方法调用接口减少响应的时间呢,这时候就是消息队列登场的时刻了。

调用链路长、响应就慢了,并且相对于扣库存和下单,积分和短信没必要这么的及时。因此只需要在下单结束那个流程,扔个消息到消息队列中就可以直接返回响应了。而且积分服务和短信服务可以并行的消费这条消息。

可以看出消息队列可以减少请求的等待,还能让服务异步并发处理,提升系统总体性能。

下面我们看张图就明白了,如下所示:

在这里插入图片描述

服务解耦

模拟秒杀场景,现在用户下单需要经过订单服务、和库存服务,如下图:
在这里插入图片描述
如果库存服务出现问题,会导致订单服务下单失败。而且如果库存服务接口修改了,会导致订单服务也无法工作。

使用消息队列可以实现服务与服务之间的解耦,订单服务不再调用库存服务接口,而是把订单消息写入到消息队列。库存服务从消息队列中拉取消息,然后再减库存,从而实现服务的解耦。

流量控制

想必大家都听过「削峰填谷」,后端服务相对而言都是比较脆弱的,因为业务较重,处理时间较长。像一些例如秒杀活动爆发式流量打过来可能就顶不住了。因此需要引入一个中间件来做缓冲,消息队列再适合不过了。

那么消息队列又是如何完成削峰的呢?好比如你现在有两台机器,每台只能处理 1000 个请求。

在这里插入图片描述

那预估业务会来 3000 个请求,那么这时怎么办?削峰!把请求的流量高峰削掉,每台机器处理了1000个请求,剩下1000 个请求先放到消息队列中,等机器根据自己处理请求的能力去消息队列中拿。

在这里插入图片描述
这里需要注意的是:引入消息队列固然会有很多的好处,但是多引入一个中间件系统的稳定性就下降一层,运维的难度抬高一层。因此要权衡利弊,系统是演进的。

消息队列基本概念

消息队列有两种模型:队列模型和发布/订阅模型。

队列模型

生产者往某个队列里面发送消息,一个队列可以存储多个生产者的消息,一个队列也可以有多个消费者, 但是消费者之间是竞争关系,即每条消息只能被一个消费者消费。
在这里插入图片描述

发布/订阅模型

为了解决一条消息能被多个消费者消费的问题,我们可以使用发布/订阅模型。该模型是将消息发往一个 Topic 主题中,所有订阅了这个 Topic 的订阅者都能消费这条消息。

就好比我们在一个微信群里面,我发送一条消息,只要在这个群里面的人都能收到我发送的消息,而队列模型就是一对一聊天,我发给你的消息,只能在你的聊天窗口弹出,是不可能弹出到别人的聊天窗口中的。

这里又有人会问了,那我一对一聊天对每个人都发同样的消息也可以实现一条消息被多个人消费了啊。

这样也对,通过多队列全量存储相同的消息,即数据的冗余可以实现一条消息被多个消费者消费。RabbitMQ 就是采用队列模型,通过 Exchange 模块来将消息发送至多个队列,解决一条消息需要被多个消费者消费问题。

在这里插入图片描述

如何保证消息不丢失

一条消息从产生到被消费,中间会经历三个阶段:生产者、MQ 内部、消费者,消息在这三个阶段中均有可能出现丢失。

就我们市面上常见的消息队列而言,只要配置得当,我们的消息就不会丢。

我们先画张图看看:

在这里插入图片描述
我们从这三个阶段分别入手来看看如何确保消息不会丢失。

生产消息

当生产者往 Broker 写消息,需要处理 Broker 的响应,不论是同步还是异步发送消息,同步和异步回调都需要做好 try-catch 捕获异常,在异常代码块中重试。如果 Broker 返回写入失败等错误消息,需要重试发送。当多次发送失败需要作报警,日志记录等,这样就能保证在生产消息阶段消息不会丢失。

Broker 存储消息

存储消息阶段需要在消息刷盘之后再给生产者响应,假设消息写入缓存中就返回响应,那么机器突然断电消息就没了,而生产者以为已经发送成功了。

如果 Broker 是集群部署,有多副本机制,即消息不仅仅要写入当前 Broker ,还需要写入副本机中。那配置成至少写入两台机子后再给生产者响应。这样基本上就能保证存储的可靠了。一台挂了另一台还在。

消费消息

这里很多人会犯一个错误,就是当消费者获取到消息以后,返回消费成功的状态给 Broker 在执行业务逻辑,如果这时候消费者宕机了怎么办,那么数据不就丢失了?

所以消费端从 Broker 上拉取消息,只要消费端在收到消息后,不立即发送消费确认给 Broker,而是等到执行完业务逻辑后,再发送消费确认,也能保证消息的不丢失。

如果处理重复消息

消息重复有两种情况:

生产者

假设我们发送消息到 Broker,发送成功需要等待 Broker 响应成功给到生产者,有可能存在 Broker 已经写入了,但是由于网络原因,生产者没有收到 Broker 的响应,然后生产者又重发了一次,此时消息就重复了。

消费者

消费者拿到消息消费了,业务逻辑已经走完了,事务提交了,此时需要更新 Consumer offset 了,然后这个消费者挂了,另一个消费者顶上,此时 Consumer offset 还没更新,于是又拿到刚才那条消息,业务又被执行了一遍。于是消息又重复了。

我们可以知道,正常而言消息重复是不可避免的,因此我们只能通过一种解决方案来解决这个问题,

关键点就是幂等。既然我们不能防止重复消息的产生,那么我们只能在业务上处理重复消息所带来的影响。

幂等处理重复消息

[幂等] 我们可以理解为同样的参数多次调用同一个接口和调用一次产生的结果是一致的。

想要解决“消息丢失”和“消息重复消费”的问题,有一个前提条件就是要实现一个全局唯一 ID 生成的技术方案,这也是面试官喜欢考察的问题。

在分布式系统中,全局唯一 ID 生成的实现方法有数据库自增主键、Redis、UUID、Snowflake 算法。

解决问题的方法就是记录唯一的 ID,比如处理订单这种,记录订单 ID,假如有重复的消息过来,先判断下这个 ID 是否已经被处理过了,如果没处理再进行下一步。当然也可以用全局唯一 ID 等等。

处理消息堆积

除了怎么解决消息被重复消费的问题之外,面试官还喜欢问消息积压。原因在于消息积压反映的是性能问题,解决消息积压问题,可以说明候选者有能力处理高并发场景下的消费能力问题。

消息的堆积往往是因为生产者的生产速度与消费者的消费速度不匹配。有可能是因为消息消费失败反复重试造成的,也有可能就是消费者消费能力弱,渐渐地消息就积压了。

因为消息发送之后才会出现积压的问题,所以和消息生产者没有关系,又因为绝大部分的消息队列单节点都能达到每秒钟几万的处理能力,相对于业务逻辑来说,性能不会出现在中间件的消息存储上面。所以出问题的肯定是消息消费阶段。

那么我们应该怎么入手解决问题呢?主要有如下方案:

  • 如果是线上突发问题,要临时扩容,增加消费端的数量,与此同时,降级一些非核心的业务。 通过扩容和降级承担流量,表明了对应急问题的处理能力。
  • 其次排查解决异常问题,如通过监控,日志等手段分析是否消费端的业务逻辑代码出现了问题,优化消费端的业务处理逻辑。
  • 如果是消费端的处理能力不足,可以通过水平扩容来提供消费端的并发处理能力。 注意在kafka中在扩容消费者的实例数的同时,必须同步扩容主题 Topic 的分区数量,确保消费者的实例数和分区数相等。如果消费者的实例数超过了分区数,由于分区是单线程消费,所以这样的扩容就没有效果。

最后

上面的问题都是面试关于消息队列必问的考点之一,我们在使用消息队列时也经常会遇到这种问题。
通过上面三点,我们知道了如何保证消息不丢失、处理重复消息、处理消息堆积,重要的是如何保证消息不丢失。首先是将消息推送到 Broker 时我们要保证消息的确是到了 Broker 。然后就是存在 Broker 中的消息要保证持久化,这样能解决 Broker 重启导致的内存中的消息不会被丢失。最后就是消费者在消费消息时,我们通过手动 ack 来告诉 Broker 是不是应该将消息移除队列。

<think>好的,我现在需要帮用户解答关于消息队列中数据重复丢失的原因以及如何确保消息有序到达的问题。首先,我得回忆一下消息队列的基本原理,可能涉及生产者、broker、消费者这三个部分。 数据重复的原因可能出现在生产者和消费者两端。生产者发送消息时,如果遇到网络问题或者broker响应延迟,可能会触发重试机制,导致消息重复发送。比如,生产者发送消息后没收到确认,就会重新发送,这时候broker可能已经收到了,但生产者不知道,所以导致重复。另外,消费者在处理消息后,如果提交确认失败,消息可能会被重新投递,导致重复消费。比如消费者处理消息,但还没提交offset就崩溃了,重启后又会拉取同样的消息。 然后是数据丢失的情况。生产者端如果使用异步发送,可能在消息还没成功写入broker时就返回成功,这时候如果broker宕机,消息就丢了。Broker端如果刷盘策略是异步的,可能在写入内存但还没刷盘时宕机,导致数据丢失。消费者方面,如果设置了自动提交offset,可能在处理消息前就提交了,这时候如果消费者崩溃,消息还没处理就被认为已经消费,导致丢失。 接下来是消息有序的问题。有些业务场景需要消息按顺序处理,比如订单状态变更。如果消息被发送到不同的队列,或者消费者多线程处理,顺序就会乱。解决方案可能需要将同一业务ID的消息发送到同一个队列,保证顺序性。例如,RocketMQ中可以通过MessageQueueSelector来选择队列,确保同一ID的消息在同一个队列里,消费者按顺序拉取。 在最佳实践方面,生产者可以使用同步发送并检查确认,比如RocketMQ的SYNC_FLUSH模式确保消息刷盘后再响应。幂等性处理很重要,比如在数据库里记录已处理消息ID,或者在消费端做去重。消费者应该手动提交offset,确保处理完成后再提交。此外,设置死信队列来处理无法消费的消息,避免无限重试。还有,合理设置重试次数和超时时间,避免因为短暂故障导致消息堆积。 现在需要结合用户提供的引用内容。引用[1]提到发送消息并捕获确认响应,说明生产者需要确认消息是否成功接收。引用[2]提到死信队列的作用,可以用来处理无法投递的消息。引用[3]建议在消费入口打印日志,方便排查问题,可能和消费确认机制有关。引用[4]讨论了broker的刷盘策略,SYNC_FLUSH模式确保消息刷盘后才确认,避免丢失。 整理一下结构,先分原因,再讲解决方案,最后最佳实践。要确保每个部分都涵盖生产者和消费者的情况,并且引用相关的参考资料。需要检查是否符合用户的要求,比如使用中文,LaTeX格式正确,生成相关问题等。 可能还需要注意术语的正确性,比如RocketMQ中的MessageQueue和Consumer的配置。确保解决方案具体,例如幂等性设计的具体方法,如唯一索引、状态机等。在有序性方面,可能需要提到全局顺序和分区顺序的区别,以及如何根据业务需求选择。 最后生成相关问题,可能涉及消息队列的其他问题,比如如何保证Exactly-Once语义,如何监控消息队列的健康状态,或者不同消息队列系统的对比等。</think>### 消息队列数据重复丢失的原因及解决方案 #### 一、数据重复的原因及解决方法 1. **重复原因** - **生产者重试机制**:网络波动或响应超时导致生产者重复发送消息[^1] - **消费者重复消费**:消费者处理消息后未及时提交确认(如offset),消息被重新投递[^3] - **消息投递语义**:At-Least-Once模式允许消息多次投递 2. **解决方法** - **幂等性设计**:通过唯一ID、数据库唯一索引或状态机保证多次处理结果一致 - **生产者去重**:启用消息ID去重(如RocketMQ的`UNIQUE_KEY`机制) - **消费者确认机制**:手动提交offset,确保消息处理完成后再确认[^3] --- #### 二、数据丢失的原因及解决方法 1. **丢失原因** - **生产者异步发送失败**:消息未成功写入Broker即返回成功[^4] - **Broker刷盘策略**:异步刷盘(ASYNC_FLUSH)时宕机导致内存数据丢失[^4] - **消费者自动提交offset**:消息处理完成即提交offset,消费者崩溃后消息丢失 2. **解决方法** - **生产者同步发送**:使用SYNC_FLUSH模式确保消息刷盘后再响应 - **Broker高可用部署**:主从复制+持久化存储 - **消费者手动提交offset**:处理完成后手动确认,避免自动提交风险 --- #### 三、确保消息有序到达的解决方案 1. **全局有序性** - **单队列单消费者**:将消息全部发送到同一队列,消费者单线程处理(性能受限) 2. **分区有序性(更常用)** - **相同业务ID哈希到同一队列**:例如订单ID相同的消息分配到固定队列 - **消费者顺序消费**:单线程处理同一队列的消息(RocketMQ的`MessageListenerOrderly`) 3. **实现示例(RocketMQ)** ```java // 发送端:指定业务ID选择队列 producer.send(msg, new MessageQueueSelector() { @Override public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { String orderId = (String) arg; int index = orderId.hashCode() % mqs.size(); return mqs.get(index); } }, orderId); // 消费端:注册顺序监听器 consumer.registerMessageListener(new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { // 处理逻辑 return ConsumeOrderlyStatus.SUCCESS; } }); ``` --- #### 四、最佳实践 1. **生产者** - 启用事务消息(如RocketMQ事务消息- 监控发送失败重试次数(避免无限重试)[^1] 2. **Broker** - 高可用集群部署+SYNC_FLUSH刷盘策略 - 设置死信队列处理无法消费的消息[^2] 3. **消费者** - 记录消息处理日志(如`log.info("RECEIVE_MSG_BEGIN: " + msgs.toString())`) - 限制消费并发度(避免线程竞争导致顺序混乱) ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值