CrabNote螃蟹笔记

消息队列面试题

本文《消息队列面试题》由小宇宙从原站整理搬运,详细讲解相关技术要点。

大家好,我是小宇宙。 消息队列这块,在面试里出现的频率越来越高,尤其是做过有一定规模业务的同学,基本逃不掉。很多人平时用 MQ 就是往里发消息、消费消息,问起来能说出个"解耦、异步、削峰",但一旦被追问"消息丢了怎么办""消息重复了怎么处理""积压了几百万条消息怎么解决",就开始说不清楚了。这块知识其实不算难,但要答得有深度,还是得把原理搞透。 这篇文章整理了消息队列面试中最常被问到的知识点,涵盖 MQ 的使用场景、消息可靠性、幂等处理、消息积压,以及 RocketMQ、Kafka、RabbitMQ 三大主流 MQ 的核心原理和对比,覆盖比较全面。 有几块在面试里特别容易被深挖,建议重点花时间: 消息可靠性 :生产者、Broker、消费者三个环节各自怎么保证不丢消息,这是必考题,而且考官很喜欢追问每个环节的细节。 重复消费和幂等 :为什么会重复、怎么设计幂等方案,这个和业务结合很紧,答起来容易流于表面。 消息积压 :如何快速处理积压,尤其是 RocketMQ 里队列数和消费者数的关系,是很多人的盲区。 Kafka 和 RocketMQ 的区别 :存储模型、协调机制、适用场景,这是高频对比题,面试里问得很多。 事务消息 :怎么保证本地事务和消息发送的一致性,半消息机制是怎么工作的,这块能和分布式事务挂钩,答好了很加分。 如果你是第一次系统准备这块,建议先搞清楚 MQ 的基本原理(为什么用、怎么用),再去看可靠性和幂等这两个核心问题,然后再对比几个主流 MQ 的差异,这个顺序学下来会比较顺。 消息队列场景 什么是消息队列? 你可以把消息队列理解为一个 使用队列来通信 的组件。它的本质,就是个 转发器 ,包含 发消息、存消息、消费消息 的过程。最简单的消息队列模型如下: 我们通常说的消息队列,简称 MQ(Message Queue) ,它其实就指 消息中间件 ,当前业界比较流行的开源消息中间件包括: 。 为什么需要消息队列? 如果用一句话概括为什么我们要大费周章地引入 MQ,核心其实就是为了解决系统架构中的三大痛点: 解耦、异步、削峰 。 我可以结合实际的业务场景,给你连贯地汇报一下这三点: 第一点,最核心的作用是为了「解耦」(把绑在一起的系统拆开)。 在没有 MQ 的时候,假设我们做个电商系统,用户付完款后,系统要干好几件事:扣库存、发优惠券、加积分、还要发短信通知。如果订单系统直接用 RPC 调用去挨个通知这些子系统,那代码就高度耦合在了一起。万一有一天不需要发短信了,或者新加了一个自动发货服务,订单系统的研发就得整天去改代码重新发版;更别提如果哪天「发积分服务」挂了报错,甚至会连累整个订单支付流程跟着失败。 引入 MQ 后,整个逻辑就变了。订单系统付完款,它只需要对着 MQ 丢一句话(发一条消息):「用户A支付成功了」,它的主线任务就彻底结束了。后续不管有几个子系统关心这件事,它们自己去订阅 MQ 就好了,哪怕以后新加再多依赖服务,订单系统的代码也一行都不用改。这就达到了物理和逻辑上的彻底解耦,系统容错率极高。 第二点,顺理成章带来的性能提升叫作「异步」。 紧接上面的例子,如果要串行调用发短信、加积分等等这么多非核心流程,哪怕每个接口只用 50 毫秒,加起来用户在页面等待的时间可能就得大半秒甚至好几秒,疯狂转圈圈,体验极差。 有了 MQ,主流程其实只需要把状态改掉,然后往 MQ 里抛一条消息(耗时大概几毫秒),立刻就可以给前端响应「恭喜你支付成功」了。像发短信、加积分这种「不是非得在这零点几秒内完成」的操作,完全可以让系统在后台顺着 MQ 慢慢执行,这样前台接口的响应时间就大幅缩短了。 第三点,是高并发场景下的保命神器,叫作「削峰填谷」。 平常我们底层的 MySQL 数据库,一秒钟扛个一两千并发可能没事。但是一旦搞促销、秒杀,瞬间如果有两万个请求涌进来,如果直接打到数据库上,数据库绝对当场宕机。 这个时候,MQ 就扮演了一个极其关键的\ \ 「蓄水池」或者「排队区」\ \ 的作用。这两万个瞬时狂暴请求过来,我们统一先扔进 MQ 里暂存着。然后后端的订单处理程序,根据自己数据库真实的抗压能力,平稳地、以一秒钟一两千个的速度,慢慢从 MQ 里往外拉任务来消化。这就像游乐场门口的蛇形排队通道,把瞬间爆发的流量洪峰(削峰),分散到了后续慢慢处理(填谷)。虽然用户体感上可能是排队多了几秒钟,但总比整个大盘系统崩溃要好无数倍。 总结一下: 引入消息队列,确实会让系统整体变复杂,还需要处理消息不丢失、不重复消费这些麻烦事儿。但是为了换取各个服务之间的 解耦 、核心接口通过 异步 大幅减少响应时间、以及面对几倍几十倍突发流量时的 削峰 保命,这在稍微复杂一点的系统里都是极其必要的。这就是我们需要 MQ 的根本原因。 消息队列有什么缺点? 关于消息队列(MQ)的优缺点,在软件架构设计里有一句老话叫「没有银弹」。引入 MQ 绝对是一次典型的 Trade off(技术权衡),它既能拯救系统,又能带来一堆让人头疼的麻烦。 我们可以分两面来看: 首先是它的优点,总结起来就是「系统架构救命的三板斧」: 1. 解耦 :把各个原本互相关联的系统拆解开。A 系统只要把消息丢给 MQ 就完事了,B 系统或者日后新加的 C 系统想要数据,自己去 MQ 拿就行。A 系统的代码不用再为了别的系统频繁修改,大家各干各的。 2. 异步 :提升了用户的响应速度。一些非核心、但又很费时间的操作(比如发短信、送积分),全部扔进 MQ 后台慢慢执行。主流程几毫秒就结束了,用户的体感极佳。 3. 削峰 :这是大促、秒杀时挡在数据库前面的超级盾牌。把洪水猛兽般瞬间涌入的并发请求,先拦截在 MQ 的队列里,变成了细水长流,让数据库能按照自己的节奏平稳处理,保住系统不崩溃。 但是,既然它这么好,为什么不每个地方都加呢?这就不得不提它带来的三大缺点或者说是巨大挑战: 第一,系统整体的「可用性」反而变低了。 在没有 MQ 时,A 系统直接调 B 系统,只要它俩不出问题就行。现在中间横插了一个 MQ 组件,多拉了一个「中间人」。一旦这个机器宕机、网络故障,整个业务链路直接瘫痪。所以引入 MQ,你就不得不花大价钱和精力去搭比如主从集群,去保证 MQ 的绝对高可用,不然这就是给系统埋下了一颗大雷。 第二,系统的「开发复杂性」呈指数级上升。 原本很简单的一个方法调用,换成发消息之后,开发人员就面临无数个让人掉头发的异常场景。比如:网络抖动导致一条消息发了两次,B 系统怎么保证不把老王的钱扣两次?(这就需要做跨业务的 防重和幂等性设计 );再比如,MQ 自己重启了,还没处理完的消息怎么保证不丢失?这些都需要我们在写代码时增加非常多的确认机制、补偿机制。 第三,最头疼的「数据一致性」问题。 A 系统本地业务执行成功了,消息发给 MQ 了,就高高兴兴告诉前端「操作成功」。结果下游的 B 系统拿到这条消息后,由于一条空指针异常没处理好,业务失败了。现在好了,A 以为成功了,B 实际失败了,两边的数据产生彻底的脱节。想要解决这个问题,我们就不得不引入相对复杂的「分布式事务」方案,或者开发额外的对账及人工补偿工具。 总结一下: MQ 就像是一把威力巨大的重型武器,优点极其耀眼。但它的缺点决定了,我们绝不是逢坑就用。只有当业务场景真的对解耦、异步、抗高并发有强烈需求,且这部分收益远远大过引入这些复杂性带来的麻烦时,我们才会去谨慎地使用它。 消息队列怎么选型? Kafka、ActiveMQ、RabbitMQ、RocketMQ来进行不同维度对比。 | 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | | | | | | | | 单机吞吐量 | 万级 | 万级 | 10 万级 | 10 万级 | | 时效性 | 毫秒级 | 微秒级 | 毫秒级 | 毫秒级 | | 可用性 | 高(主从) | 高(主从) | 非常高(分布式) | 非常高(分布式) | | 消息语义 | 至少一次 | 至少一次 | 至少一次 / 最多一次 | 至少一次 / 最多一次 / 精确一次(0.11+,需配置开启) | | 消息顺序性 | 有序 | 有序 | 顺序消息(同一队列内有序) | 分区有序 | | 支持主题数 | 千级 | 万级(队列数可更多,但元数据开销较大) | 万级(CommitLog 结构对海量 Topic 友好) | Topic 上千时单 Broker 因分区文件过多,性能会显著下降 | | 消息回溯 | 不支持 | 不支持 | 支持(按时间回溯) | 支持(按 offset 回溯) | | 管理界面 | 普通 | 普通 | 完善 | 普通 | 选型的时候,我们需要根据业务场景,结合上述特性来进行选型。 比如你要支持天猫双十一类超大型的秒杀活动,这种一锤子买卖,那管理界面、消息回溯啥的不重要。 我们需要看什么?看吞吐量! 所以优先选Kafka和RocketMQ这种更高吞吐的。 比如做一个公司的中台,对外提供能力,那可能会有很多主题接入,这时候主题个数又是很重要的考量,像Kafka这样百级的,就不太符合要求,可以根据情况考虑千级的RocketMQ,甚至百万级的RabbitMQ。 又比如是一个金融类业务,那么重点考虑的就是稳定性、安全性,分布式部署的Kafka和Rocket就更有优势。 特别说一下时效性,RabbitMQ以微秒的时效作为招牌,但实际上毫秒和微秒,在绝大多数情况下,都没有感知的区别,加上网络带来的波动,这一点在生产过程中,反而不会作为重要的考量。 其它的特性,如消息确认、消息回溯,也经常作为考量的场景,管理界面的话试公司而定了,反正我呆过的地方,都不看重这个,毕竟都有自己的运维体系。 消息队列使用场景有哪些? 解耦 :可以在多个系统之间进行解耦,将原本通过网络之间的调用的方式改为使用MQ进行消息的异步通讯,只要该操作不是需要同步的,就可以改为使用MQ进行不同系统之间的联系,这样项目之间不会存在耦合,系统之间不会产生太大的影响,就算一个系统挂了,也只是消息挤压在MQ里面没人进行消费而已,不会对其他的系统产生影响。 异步 :假如一个操作涉及到好几个步骤,这些步骤之间不需要同步完成,比如客户创建了一个订单后,还要去客户轨迹系统添加一条轨迹、去库存系统更新库存、去客户系统修改客户的状态等等。这样如果这些系统都直接进行调用,那么将会耗费大量时间,对于客户来说是无法接受的;并且像添加客户轨迹这种操作是不需要同步完成的,如果使用 MQ,在客户创建订单时把后面的轨迹、库存、状态等信息更新全都放到 MQ 里去异步处理,就可以加快主流程的响应速度,提供更好的客户体验。 削峰 :一个系统访问流量有高峰时期,也有低峰时期,比如说,中午整点有一个抢购活动等等。比如系统平时流量并不高,一秒钟只有 100 多个并发请求,系统处理没有任何压力,一切风平浪静,到了某个抢购活动时间,系统并发访问剧增,比如达到了每秒 5000 个并发请求,而我们的系统每秒只能处理 2000 个请求,那么由于流量太大,我们的系统、数据库可能就会崩溃。这时如果使用 MQ 进行流量削峰,将用户的大量请求对应的消息直接放到 MQ 里面,然后我们的系统按自己的最大消费能力去消费这些消息,就可以保证系统的稳定,只是可能要根据业务逻辑,给用户返回特定页面或者稍后通过其他方式通知其结果。 消息重复消费怎么解决? 生产端为了保证消息发送成功,可能会重复推送(直到收到成功ACK),会产生重复消息。但是一个成熟的MQ Server框架一般会想办法解决,避免存储重复消息(比如:空间换时间,存储已处理过的message\ id),给生产端提供一个幂等性的发送消息接口。 但是消费端却无法根本解决这个问题,在高并发标准要求下,拉取消息+业务处理+提交消费位移需要做事务处理,另外消费端服务可能宕机,很可能会拉取到重复消息。 所以,只能业务端自己做控制, 对于已经消费成功的消息,本地数据库表或Redis缓存业务标识,每次处理前先进行校验,保证幂等。 消息丢失怎么解决的? 使用一个消息队列,其实就分为三大块: 生产者、中间件、消费者 ,所以要保证消息就是保证三个环节都不能丢失数据。 消息生产阶段 :生产者会不会丢消息,取决于生产者对于异常情况的处理是否合理。从消息被生产出来,然后提交给 MQ 的过程中,只要能正常收到 ( MQ 中间件) 的 ack 确认响应,就表示发送成功,所以只要处理好返回值和异常,如果返回异常则进行消息重发,那么这个阶段是不会出现消息丢失的。 消息存储阶段 :Kafka 在使用时是部署一个集群,生产者在发布消息时,队列中间件通常会写「多个节点」,也就是有多个副本,这样一来,即便其中一个节点挂了,也能保证集群的数据不丢失。 消息消费阶段 :消费者接收消息+消息处理之后,才回复 ack 的话,那么消息阶段的消息不会丢失。不能收到消息就回 ack,否则可能消息处理中途挂掉了,消息就丢失了。 使用消息队列还应该注意哪些问题? 需要考虑消息可靠性和顺序性方面的问题。 消息队列的可靠性、顺序性怎么保证? 消息可靠性可以通过下面这些方式来保证 消息持久化 :确保消息队列能够持久化消息是非常关键的。在系统崩溃、重启或者网络故障等情况下,未处理的消息不应丢失。例如,像 RabbitMQ 可以通过配置将消息持久化到磁盘,通过将队列和消息都设置为持久化的方式(设置 ),这样在服务器重启后,消息依然可以被重新读取和处理。 消息确认机制 :消费者在成功处理消息后,应该向消息队列发送确认(acknowledgment)。消息队列只有收到确认后,才会将消息从队列中移除。如果没有收到确认,消息队列可能会在一定时间后重新发送消息给其他消费者或者再次发送给同一个消费者。以 Kafka 为例,消费者通过 或者 方法来提交偏移量(offset),从而确认消息的消费。 消息重试策略 :当消费者处理消息失败时,需要有合理的重试策略。可以设置重试次数和重试间隔时间。例如,在第一次处理失败后,等待一段时间(如 5 秒)后进行第二次重试,如果重试多次(如 3 次)后仍然失败,可以将消息发送到死信队列,以便后续人工排查或者采取其他特殊处理。 消息顺序性保证的方式如下: 有序消息处理场景识别 :首先需要明确业务场景中哪些消息是需要保证顺序的。例如,在金融交易系统中,对于同用户的转账操作顺序是不能打乱的。对于需要顺序处理的消息,要确保消息队列和消费者能够按照特定的顺序进行处理。 消息队列对顺序性的支持 :部分消息队列本身提供了顺序性保证的功能。比如 Kafka 可以通过将消息划分到同一个分区(Partition)来保证消息在分区内是有序的,消费者按照分区顺序读取消息就可以保证消息顺序。但这也可能会限制消息的并行处理程度,需要在顺序性和吞吐量之间进行权衡。 消费者顺序处理策略 :消费者在处理顺序消息时,应该避免并发处理可能导致顺序打乱的情况。例如,可以通过单线程或者使用线程池并对顺序消息进行串行化处理等方式,确保消息按照正确的顺序被消费。 如何保证幂等写? 幂等性是指 同一操作的多次执行对系统状态的影响与一次执行结果一致 。例如,支付接口若因网络重试被多次调用,最终应确保仅扣款一次。实现幂等写的核心方案: 唯一标识(幂等键):客户端为每个请求生成全局唯一ID(如 UUID、业务主键),服务端校验该ID是否已处理,适用场景接口调用、消息消费等。 数据库事务 + 乐观锁:通过版本号或状态字段控制并发更新,确保多次更新等同于单次操作,适用场景数据库记录更新(如余额扣减、订单状态变更)。 数据库唯一约束:利用数据库唯一索引防止重复数据写入,适用场景数据插入场景(如订单创建)。 分布式锁:通过锁机制保证同一时刻仅有一个请求执行关键操作,适用场景高并发下的资源抢夺(如秒杀)。 消息去重:消息队列生产者为每条消息生成唯一的消息 ID,消费者在处理消息前,先检查该消息 ID 是否已经处理过,如果已经处理过则丢弃该消息。 如何处理消息队列的消息积压问题? 消息积压是因为生产者的生产速度,大于消费者的消费速度。遇到消息积压问题时,我们需要先排查,是不是有bug产生了。 如果不是bug,我们可以 优化一下消费的逻辑 ,比如之前是一条一条消息消费处理的话,我们可以确认是不是可以优为 批量处理消息 。如果还是慢,我们可以考虑水平扩容,增加Topic的队列数,和消费组机器的数量,提升整体消费能力。 如果是bug导致几百万消息持续积压几小时。有如何处理呢?需要解决bug, 临时紧急扩容 ,大概思路如下: 1. 先修复consumer消费者的问题,以确保其恢复消费速度,然后将现有consumer 都停掉。 2. 新建一个 topic,partition 是原来的 10 倍,临时建立好原先10倍的queue 数量。 3. 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。 4. 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。 5. 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。 如何保证数据一致性,事务消息如何实现? 一条普通的MQ消息,从产生到被消费,大概流程如下: 1. 生产者产生消息,发送到 MQ 服务器 2. MQ收到消息后,将消息持久化到存储系统。 3. MQ服务器返回ACk到生产者。 4. MQ服务器把消息push给消费者 5. 消费者消费完消息,响应ACK 6. MQ服务器收到ACK,认为消息消费成功,即在存储中删除消息。 我们举个 下订单 的例子吧。订单系统创建完订单后,再发送消息给下游系统。如果订单创建成功,然后消息没有成功发送出去,下游系统就无法感知这个事情,出导致数据不一致。 如何保证数据一致性呢?可以使用 事务消息 。一起来看下事务消息是如何实现的吧。 1. 生产者产生消息,发送一条 半事务消息 到MQ服务器 2. MQ收到消息后,将消息持久化到存储系统,这条消息的状态是 待发送 状态。 3. MQ服务器返回ACK确认到生产者,此时MQ不会触发消息推送事件 4. 生产者执行本地事务 5. 如果本地事务执行成功,即commit执行结果到MQ服务器;如果执行失败,发送rollback。 6. 如果是正常的commit,MQ服务器更新消息状态为 可发送 ;如果是rollback,即删除消息。 7. 如果消息状态更新为可发送,则MQ服务器会push消息给消费者。消费者消费完就回ACK。 8. 如果MQ服务器长时间没有收到生产者的commit或者rollback,它会反查生产者,然后根据查询到的结果执行最终状态。 消息队列是参考哪种设计模式? 是参考了观察者模式和发布订阅模式,两种设计模式思路是一样的,举个生活例子: 观察者模式:某公司给自己员工发月饼发粽子,是由公司的行政部门发送的,这件事不适合交给第三方,原因是「公司」和「员工」是一个整体 发布 订阅模式:某公司要给其他人发各种快递,因为「公司」和「其他人」是独立的,其唯一的桥梁是「快递」,所以这件事适合交给第三方快递公司解决 上述过程中,如果公司自己去管理快递的配送,那公司就会变成一个快递公司,业务繁杂难以管理,影响公司自身的主营业务,因此使用何种模式需要考虑什么情况两者是需要耦合的 观察者模式 观察者模式实际上就是一个一对多的关系,在观察者模式中存在一个主题和多个观察者,主题也是被观察者,当我们主题发布消息时,会通知各个观察者,观察者将会收到最新消息,图解如下:每个观察者首先订阅主题,订阅成功后当主题发送消息时会循环整个观察者列表,逐一发送消息通知。 发布订阅模式 发布订阅模式和观察者模式的区别就是发布者和订阅者完全解耦,通过中间的发布订阅中心进行消息通知,发布者并不知道自己发布的消息会通知给谁,因此发布订阅模式有三个重要角色,发布者 发布订阅中心 订阅者。 图解如下:当发布者发布消息到发布订阅中心后,发布订阅中心会将消息通知给所有订阅该发布者的订阅者 让你写一个消息队列,该如何进行架构设计? 这个问题面试官主要考察三个方面的知识点: 你有没有对消息队列的架构原理比较了解 考察你的个人设计能力 考察编程思想,如什么高可用、可扩展性、幂等等等。 遇到这种设计题,大部分人会很蒙圈,因为平时没有思考过类似的问题。大多数人平时埋头增删改啥,不去思考框架背后的一些原理。有很多类似的问题,比如让你来设计一个 Dubbo 框架,或者让你来设计一个MyBatis 框架,你会怎么思考呢? 回答这类问题,并不要求你研究过那技术的源码,你知道那个技术框架的基本结构、工作原理即可。设计一个消息队列,我们可以从这几个角度去思考: 1. 首先是消息队列的整体流程,producer发送消息给broker,broker存储好,broker再发送给consumer消费,consumer回复消费确认等。 2. producer发送消息给broker,broker发消息给consumer消费,那就需要两次RPC了,RPC如何设计呢?可以参考开源框架Dubbo,你可以说说服务发现、序列化协议等等 3. broker考虑如何持久化呢,是放文件系统还是数据库呢,会不会消息堆积呢,消息堆积如何处理呢。 4. 消费关系如何保存呢?点对点还是广播方式呢?广播关系又是如何维护呢?zk还是config server 5. 消息可靠性如何保证呢?如果消息重复了,如何幂等处理呢? 6. 消息队列的高可用如何设计呢?可以参考Kafka的高可用保障机制。多副本 leader & follower broker 挂了重新选举 leader 即可对外服务。 7. 消息事务特性,与本地业务同个事务,本地消息落库;消息投递到服务端,本地才删除;定时任务扫描本地消息库,补偿发送。 8. MQ得伸缩性和可扩展性,如果消息积压或者资源不够时,如何支持快速扩容,提高吞吐?可以参照一下 Kafka 的设计理念,broker topic partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了。 RocketMQ 消息队列为什么选择RocketMQ的? 项目用的是 RocketMQ 消息队列。选择RocketMQ的原因是: 开发语言优势 。RocketMQ 使用 Java 语言开发,比起使用 Erlang 开发的 RabbitMQ 来说,有着更容易上手的阅读体验和受众。在遇到 RocketMQ 较为底层的问题时,大部分熟悉 Java 的同学都可以深入阅读其源码,分析、排查问题。 社区氛围活跃 。RocketMQ 是阿里巴巴开源且内部在大量使用的消息队列,说明 RocketMQ 的确经得起残酷的生产环境考验,并且能够针对线上环境复杂的需求场景提供相应的解决方案。 特性丰富 。根据 RocketMQ 官方文档的列举,其高级特性达到了 ,例如顺序消息、事务消息、消息过滤、定时消息等。顺序消息、事务消息、消息过滤、定时消息。RocketMQ 丰富的特性,能够为我们在复杂的业务场景下尽可能多地提供思路及解决方案。 RocketMQ和Kafka的区别是什么?如何做技术选型? Kafka的优缺点: 优点:首先,Kafka 的最大优势就在于它的高吞吐量,在普通机器 4CPU 8G 的配置下,一台机器可以抗住十几万的 QPS,这一点还是相当优越的。Kafka 支持集群部署,如果部分机器宕机不可用,则不影响 Kafka 的正常使用。 缺点:在配置不当的情况下存在丢消息风险。Kafka 收到消息后默认是先写入 OS Page Cache、由操作系统决定何时刷盘(这和 MySQL/PostgreSQL/Redis AOF 的做法一致,并非 Kafka 独有),如果配置 或开启 等不安全的参数,在主节点宕机时可能丢消息;要获得强可靠性需要配合 + 多副本 ISR + 禁止 unclean 选举。另外 Kafka 功能相对单一,主要支持收发消息,像延迟消息、事务消息等高级特性较弱,适用场景偏大数据和日志流。 RocketMQ是阿里巴巴开源的消息中间件,优缺点 优点:支持功能比较多,比如延迟队列、消息事务等等,吞吐量也高,单机吞吐量达到 10 万级,支持大规模集群部署,线性扩展方便,Java语言开发,满足了国内绝大部分公司技术栈 缺点:性能相比 Kafka 略弱一些。Kafka 在发送消息给消费者时使用的是 真·零拷贝(数据完全不经过用户态,由 DMA 从 page cache 直接送往网卡),而 RocketMQ 使用 的方式——严格来说它只是 减少了一次 CPU 拷贝 (省去了 read 系统调用中用户态与内核态之间的拷贝),但从 page cache 到 socket buffer 仍然有一次 CPU 拷贝,并不是真正的零拷贝,业界通常把它称作"广义零拷贝"。 该怎么选择呢? 如果我们业务只是收发消息这种单一类型的需求,而且可以允许小部分数据丢失的可能性,但是又要求极高的吞吐量和高性能的话,就直接选Kafka就行了,就好比我们公司想要收集和传输用户行为日志以及其他相关日志的处理,就选用的Kafka中间件。 如果公司的需要通过 mq 来实现一些业务需求,比如延迟队列、消息事务等,公司技术栈主要是Java语言的话,就直接一步到位选择RocketMQ,这样会省很多事情。 RocketMQ延时消息的底层原理 总体的原理示意图,如下所示: broker 在接收到延时消息的时候,会将延时消息存入到延时Topic的队列中,然后ScheduleMessageService中,每个 queue 对应的定时任务会不停地被执行,检查 queue 中哪些消息已到设定时间,然后转发到消息的原始Topic,这些消息就会被各自的 producer 消费了。 RocektMQ怎么处理分布式事务? RocketMQ是一种最终一致性的分布式事务 ,就是说它保证的是消息最终一致性,而不是像2PC、3PC、TCC那样强一致分布式事务 假设 A 给 B 转 100块钱 ,同时它们不是同一个服务上,现在目标是就是 A 减100块钱, B 加100块钱。 实际情况可能有四种: 1)就是A账户减100 (成功),B账户加100 (成功) 2)就是A账户减100(失败),B账户加100 (失败) 3)就是A账户减100(成功),B账户加100 (失败) 4)就是A账户减100 (失败),B账户加100 (成功) 这里 第1和第2 种情况是能够保证事务的一致性的,但是 第3和第4 是无法保证事务的一致性的。 那我们来看下RocketMQ是如何来保证事务的一致性的。 分布式事务的流程如上图: 1、A服务先发送一个 Half Message(半消息,是指暂不能被 Consumer 消费的消息。Producer 已经把消息成功发送到了 Broker 端,但此消息被标记为暂不能投递状态,处于该状态下的消息称为半消息。需要 Producer 对消息进行二次确认后,Consumer 才能去消费它)给 Broker 端,消息中携带 B 服务即将要 +100 元的信息。 2、当 A 服务知道 Half Message 发送成功后,那么开始第 3 步执行本地事务。 3、执行本地事务(会有三种情况:1、执行成功。2、执行失败。3、网络等原因导致没有响应) 4.1)、如果本地事务成功,那么 Producer 向 Broker 服务器发送 Commit,这样 B 服务就可以消费该 message。 4.2)、如果本地事务失败,那么 Producer 向 Broker 服务器发送 Rollback,Broker 会直接删除上面这条半消息。 4.3)、如果因为网络等原因迟迟没有返回失败还是成功,那么会执行RocketMQ的回调接口,来进行事务的回查。 从上面流程可以得知 只有A服务本地事务执行成功 ,B服务才能消费该message。 那么 A账户减100 (成功),B账户加100 (失败),这时候B服务失败怎么办? 如果B最终执行失败,几乎可以断定就是代码有问题所以才引起的异常,因为消费端RocketMQ有重试机制,如果不是代码问题一般重试几次就能成功。 如果是代码的原因引起多次重试失败后,也没有关系,将该异常记录下来,由人工处理,人工兜底处理后,就可以让事务达到最终的一致性。 RocketMQ消息顺序怎么保证? 消息的有序性是指消息的消费顺序能够严格保存与消息的发送顺序一致。例如,一个订单产生了3条消息,分别是订单创建、订单付款和订单完成。在消息消费时,同一条订单要严格按照这个顺序进行消费,否则业务会发生混乱。同时,不同订单之间的消息又是可以并发消费的,比如可以先执行第三个订单的付款,再执行第二个订单的创建。 RocketMQ采用了局部顺序一致性的机制,实现了单个队列中的消息严格有序。也就是说,如果想要保证顺序消费,必须将一组消息发送到同一个队列中,然后再由消费者进行注意消费。 RocketMQ 推荐的顺序消费解决方案是:按照业务划分不同的队列,然后将需要顺序消费的消息发往同一队列中即可,不同业务之间的消息仍采用并发消费。这种方式在满足顺序消费的同时提高了消息的处理速度,在一定程度上避免了消息堆积问题。 RocketMQ 顺序消息的原理是: 在 Producer(生产者) 把一批需要保证顺序的消息发送到同一个 MessageQueue Consumer(消费者) 则通过加锁的机制来保证消息消费的顺序性,Broker 端通过对 MessageQueue 进行加锁,保证同一个 MessageQueue 只能被同一个 Consumer 进行消费。 RocketMQ怎么保证消息不被重复消费? 在业务逻辑中实现 幂等性 ,确保即使消息被重复消费,也不会影响业务状态。 例如,对于支付或转账类操作,可以使用唯一订单号或事务ID作为幂等性的标识符,确保同样的操作只会被执行一次。 RocketMQ消息积压了,怎么办? 处理 RocketMQ 的消息积压,其实有点像「大禹治水」,总体思路可以分为\ \ 「紧急疏通止损」 和 「事后排查根因」\ \ 两步走。 如果在生产上突然告警,发现有几百万条消息堆积了,我们通常会按以下几个层次来应对: 第一招,也是最本能的反应:尝试给消费者(Consumer)扩容。 既然处理慢了,那直接加几台消费者服务器不就行了吗? 但这里有一个非常核心、且经常让人踩坑的 RocketMQ 知识点 :在 RocketMQ 里,一个队列(MessageQueue)同一时刻只能被一个消费者机器全权占有。 比如,你这个 Topic 默认只有 4 个队列,那你当初部署了 4 台消费机器,速度已经达到上限了。这时候就算你紧急再加 10 台机器进去,这 10 台机器也会在那儿干瞪眼,根本分不到队列,也就起不到任何加速排队的作用。 那怎么办呢?这就引出我们的第二招,遇到海量积压时真正的「大招」:临时Topic嫁接术。 如果积压实在太多,为了快速恢复正常业务,我们会立刻写一套非常简单的「搬砖程序」上线。这个程序没有任何复杂的业务逻辑,它唯一的任务,就是全速把积压的老 Topic 里的消息拉出来,原封不动地发到一个临时建立的 新 Topic 里去。 绝妙的地方在于,建新 Topic 的时候,我们可以一口气给它分配 数十或上百个队列 。接着,我们再部署几十台真正干活的业务消费者机器,去对接这个多队列的新 Topic。这样就能突破原来老队列数量的物理限制,以十倍甚至几十倍的速度把积压的账给清掉。等积压处理完了,再切回原来的老 Topic 架构。 第三招,业务兜底方案。 如果跟业务方沟通后,发现系统已经濒临崩溃,而且这些积压的消息(比如一些用户的非关键点击日志、普通的埋点数据)丢了也不影响核心营收。那也可以直接在 RocketMQ 的控制台上,一键把消费组的「消费点位(Offset)」重置到最新的时间点。这就相当于人为地把之前积压的消息全部跳过作废了,先保住当下的系统可用性。 最后,当积压处理完了,一定要去做「事后诸葛亮」排查根因。 消息为什么会积压?十有八九不是因为前面发得太快,而是因为后面消费得太慢了。比如你的消费者代码里是不是在调第三方的接口,恰好那个接口今天网络抖动一直在超时连累了你?又或者是消费者在写 MySQL 的时候,出现了慢查询甚至死锁?只有找到代码里卡脖子的地方,优化掉它(比如引入缓存、优化 SQL),才能保证以后不再发洪水。 总结一下我的回答: 面对 RocketMQ 消息积压,首先要评估积压量,少量积压通过扩容 Consumer(前提是队列数足够干活)来解决;海量积压则使用「转存临时扩容 Topic」的杀手锏进行极致加速;当然业务允许的话跳过消费也是止血捷径;最后再回过头来死磕消费者代码里的性能瓶颈。这是我们排查这类问题的一套比较连贯的组合拳。 kafka 对Kafka有什么了解吗? Kafka特点如下: 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作。 可扩展性:kafka集群支持热扩展 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失 容错性:允许集群中节点失败(若副本数量为n,则允许n 1个节点失败) 高并发:支持数千个客户端同时读写 Kafka 为什么这么快? 顺序写入优化 :Kafka将消息顺序写入磁盘,减少了磁盘的寻道时间。这种方式比随机写入更高效,因为磁盘读写头在顺序写入时只需移动一次。 批量处理技术 :Kafka支持批量发送消息,这意味着生产者在发送消息时可以等待直到有足够的数据积累到一定量,然后再发送。这种方法减少了网络开销和磁盘I/O操作的次数,从而提高了吞吐量。 零拷贝技术 :Kafka使用零拷贝技术,可以直接将数据从磁盘发送到网络套接字,避免了在用户空间和内核空间之间的多次数据拷贝。这大幅降低了CPU和内存的负载,提高了数据传输效率。 压缩技术 :Kafka支持对消息进行压缩,这不仅减少了网络传输的数据量,还提高了整体的吞吐量。 kafka的模型介绍一下,kafka是推送还是拉取? 消费者模型 消息由生产者发送到kafka集群后,会被消费者消费。一般来说我们的消费模型有两种:推送模型(psuh)和拉取模型(pull)。 推送模型(push) 基于推送模型(push)的消息系统,有消息代理记录消费者的消费状态。 消息代理在将消息推送到消费者后,标记这条消息已经消费,但这种方式无法很好地保证消费被处理。 如果要保证消息被处理,消息代理发送完消息后,要设置状态为「已发送」,只要收到消费者的确认请求后才更新为「已消费」,这就需要代理中记录所有的消费状态,但显然这种方式不可取。 缺点: push模式很难适应消费速率不同的消费者 因为消息发送速率是由broker决定的,push模式的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。 拉取模型(pull) kafka采用拉取模型,由消费者自己记录消费状态,每个消费者互相独立地顺序拉取每个分区的消息。 说明: 有两个消费者(不同消费者组)拉取同一个主题的消息,消费者A的消费进度是3,消费者B的消费进度是6。 消费者拉取的最大上限通过最高水位(watermark)控制,生产者最新写入的消息如果还没有达到备份数量,对消费者是不可见的。 这种由消费者控制偏移量的优点是: 消费者可以按照任意的顺序消费消息 。比如,消费者可以重置到旧的偏移量,重新处理之前已经消