第七篇:消息队列(MQ)——就是个带存储的异步通信管道

别再背各种MQ的区别了,你只需要知道"它就是个中间人,A把消息给它,它转交给B"

写在前面

消息队列(Message Queue,简称MQ)是分布式系统里最常用的解耦工具 。面试里问到MQ,核心就考一件事:消息怎么不丢、怎么不重、怎么不积压

MQ的本质是什么?

就是两个系统之间的**"信箱"** 。A系统把消息放进信箱,B系统从信箱里取。A不用管B在不在、能不能处理,B也不用管A什么时候发。

就这么简单。所有复杂的功能(持久化、集群、事务消息)都是围绕"信箱怎么更可靠"做的增强。

面试时MQ要掌握到什么程度? 你不需要懂所有MQ的源码,但你要能说清:选型理由 + 怎么保证消息不丢 + 怎么保证不重复消费 + 消息积压怎么处理。

这篇文章就按这个思路来讲。

一、MQ解决了什么问题?(先搞懂"为什么需要")

面试官如果问"你们项目里为什么用MQ",你要回答三个核心价值,而不是只说"解耦"。

① 解耦(服务之间不直接依赖)

A系统完成订单后,需要通知B(短信)、C(积分)、D(物流)。如果A直接调用B、C、D的接口,B挂了A也跟着报错,C改接口A要跟着改代码。引入MQ后,A只管发消息,B/C/D自己去订阅。A不知道B的存在,B挂了不影响A。

② 异步(非阻塞,提升响应速度)

用户下单后,A系统要做:扣库存 → 生成订单 → 发短信 → 加积分。如果同步执行,用户要等好几秒。引入MQ后,A只做核心操作(扣库存+生成订单),然后发一条消息到MQ就返回"下单成功",短信和积分异步执行。用户体验大幅提升。

③ 削峰填谷(流量洪峰时保护下游)

双11零点,每秒10万下单请求,数据库根本扛不住。MQ就像一个缓冲池 ,把请求先存下来,下游数据库按自己能承受的速度(比如每秒5000条)慢慢消费。高峰流量被"削"平了,数据库不会被打死。

二、核心链路(面试就考这一条)

面试官问MQ的所有问题,本质上都围绕着这条链路:

生产者 → Broker(MQ服务器) → 消费者

每个环节都要保证两件事:消息不丢消息不重

2.1 消息不丢(怎么保证可靠性)

① 生产者 → Broker

  • 生产者发消息时,Broker收到后返回ACK(确认)
  • 如果生产者没收到ACK,就重试(最多重试N次,超过记录日志人工介入)。
  • RocketMQ/Kafka都支持acks参数:acks=0(不等待确认,最快但可能丢)、acks=1(Leader确认,默认)、acks=-1/all(所有副本确认,最安全但最慢)。

② Broker 存储消息

  • 消息到了Broker,先刷盘再返回ACK(或异步刷盘折中)。
  • RocketMQ的刷盘方式:SYNC_FLUSH(同步刷盘,安全但慢)和ASYNC_FLUSH(异步刷盘,快但断电可能丢)。
  • Kafka用分区多副本保证:消息写入Leader后,等ISR(同步副本集)里的follower都同步完了才返回ACK。

③ Broker → 消费者

  • 消费者拉取消息,处理完业务逻辑后,再手动提交offset(位移)或ACK。
  • 绝对不能:拉取到消息就自动提交ACK,然后业务处理失败了------消息就丢了。
  • 正确做法:业务处理成功 → 手动提交offset;业务处理失败 → 不提交,消息会被重新消费或进死信队列。

2.2 消息不重(幂等性)

消息队列保证"至少一次(At Least Once)"投递,即消息可能重复,但不会丢失。

所以消费者必须自己做幂等,即同一条消息处理多次和一次效果一样。

常见幂等方案:

  1. 数据库唯一索引:订单号作为唯一键,重复插入报错(简单有效)。
  2. Redis缓存处理状态 :处理前SETNX,如果key已存在说明已处理过,直接跳过。
  3. 业务本身幂等:比如"将库存减1"不幂等(执行两次变减2),但"将库存设为某个值"幂等。

三、消息积压(消费者扛不住了怎么办?)

场景:生产者发消息太快,消费者太慢,MQ里消息越堆越多。

常见原因:

  • 下游数据库慢(SQL没优化、锁表)
  • 消费者单线程处理,没充分利用资源
  • 消费者逻辑太重(比如每条消息要调多个外部接口)

解决思路:

  1. 临时扩容消费者:增加消费者实例,同时增加Topic的分区数(Kafka中分区是并行度的上限,一个分区只能被一个消费者消费)。这是最快速的应急方案。
  2. 优化消费逻辑:批量处理(一次拉取100条,批量插入数据库)、异步化(消费时只做最核心的逻辑,非核心的异步处理)。
  3. 降级:如果积压太严重,先丢弃非核心消息,保证核心业务能正常消费。
  4. 排查上游:是不是生产者的流量突增?能不能上游限流?

注意:队列满了一般不会影响生产者,因为MQ有反压机制(生产者会阻塞或超时)。但积压持续下去,磁盘会被写满,造成更严重的问题。

四、常见MQ选型对比(面试必问)

面试官一定会问:"Kafka、RocketMQ、RabbitMQ你们怎么选的?"

你不需要把三者所有功能都背下来,记住核心场景就行:

对比维度 Kafka RocketMQ RabbitMQ
吞吐量 极高(百万级/秒) 高(十万级) 中等(万级)
延迟 毫秒级(非极端场景) 毫秒级 微秒级(最低)
消息可靠性 高(副本机制) 极高(同步刷盘+事务)
事务消息 不支持 支持 不支持(但有事务机制)
顺序消息 分区内有序 支持 支持
延迟消息 不支持 支持 支持
语言 Scala/Java Java Erlang
适用场景 日志收集、大数据、流处理 业务消息、交易场景、需要事务 低延迟业务、灵活路由

选型建议(面试里你就这么说)

  • 日志收集、大数据、流处理 → Kafka(吞吐量是王道,丢一点日志也能接受)。
  • 金融交易、订单、库存等核心业务 → RocketMQ(支持事务消息,同步刷盘保证数据绝对不丢,Java技术栈好维护)。
  • 低延迟、复杂路由、中小企业 → RabbitMQ(延迟最低,路由灵活,但吞吐量不如前两者)。

五、高级特性(面试加分项)

5.1 RocketMQ的事务消息(分布式事务解决方案)

这是RocketMQ独有的能力,面试里提到会很加分。

问题:A扣款成功了,发消息给B扣库存。如果消息发失败了,A的扣款怎么回滚?如果消息发成功了但B扣库存失败了,怎么办?

RocketMQ事务消息的流程

  1. A发送一条半消息(Half Message) 到RocketMQ------对消费者不可见,相当于"我要开始事务了,先占个坑"。
  2. A执行本地扣款事务(update account set money = money - 100 where id = 1),绑在同一个本地事务里
  3. 根据本地事务结果执行二次确认
    • 扣款成功 → A向MQ发送Commit指令 → 半消息变为可见,B才能消费到这条消息去扣库存。
    • 扣款失败 → A向MQ发送Rollback指令 → MQ直接删除半消息,B永远收不到。
  4. 兜底反查机制 :如果A在执行本地扣款后、发送Commit/Rollback之前突然宕机了怎么办?RocketMQ会定时回查A系统:"你那条扣款到底成功了没?"A根据本地事务状态告诉MQ是Commit还是Rollback。

面试官追问:"本地事务怎么和发送半消息绑定的?"

RocketMQ的事务消息本质上把"发送半消息"和"执行本地事务"放在了一个事务里。具体实现:生产者定义TransactionListener,重写executeLocalTransactioncheckLocalTransaction方法。第一个方法是执行本地事务并返回事务状态(COMMIT/ROLLBACK/UNKNOW),第二个方法是事务状态未知时的回查逻辑。

核心价值 :这是一种最终一致性方案,用消息的可靠投递 + 本地事务 + 反查机制,保证A扣款和B扣库存最终一致。它牺牲了强一致性(允许短暂不一致),换来了高可用和高性能。

5.2 顺序消息(局部有序)

有些场景要求消息按顺序消费(比如订单状态流转:创建→支付→发货,必须按顺序)。

Kafka/RocketMQ的实现方式

  • 把需要有序的消息发到同一个分区(Partition/Queue)
  • 一个分区只能被一个消费者消费(串行),所以消息在分区内是严格有序的。
  • 注意:这只能保证"局部有序"(同一个订单的消息有序),不能保证"全局有序"(所有订单的所有消息有序)。因为不同订单可以走不同分区,并行消费。

场景 :订单创建、支付、发货三条消息,按orderId做分区键,同一个orderId的消息永远进同一个分区,串行消费。不同orderId的消息可以并行消费,互不影响。

5.3 死信队列(处理消费失败的消息)

场景:消息消费重试了N次(比如16次)还是失败,不能再重试了(否则一直占着队列)。

处理 :进入死信队列(DLQ,Dead Letter Queue) ,人工介入处理(比如修复数据后手动补发消息、或者单独写修复脚本)。

RocketMQ和RabbitMQ自带死信机制,Kafka没有死信概念,需要业务自己实现(失败的消息打到另一个Topic)。

5.4 消息幂等 + 去重(高并发必备)

除了前面说的幂等,在高并发场景下还要考虑去重

  • 消费者拿到消息后,先往Redis里存一个处理标志SETNX key value EX 300),key = 业务ID + 消息ID
  • 如果SETNX返回1,说明这条消息没处理过,执行业务逻辑。
  • 如果SETNX返回0,说明已经处理过了(或者在处理中),直接跳过(返回ACK,不重试)。
  • 注意SETNX和业务逻辑如果拆成两步,可能存在并发问题(A线程查不到标志→去执行业务,B线程也查不到→也去执行业务)。要在Lua脚本或分布式锁里保证原子性,或者直接用数据库唯一索引作为去重手段,把"判断是否存在 + 插入"交给数据库的INSERT IGNORE或唯一约束来处理。

六、面试官真正想听什么

场景1:"你们项目里用的什么MQ?为什么?"

普通回答:"RocketMQ。"

加分回答:

"我们做的是订单交易系统,对数据一致性要求很高。RocketMQ的事务消息能帮我们保证订单和库存的最终一致性,而且它是Java写的,出问题了团队能看懂源码。Kafka虽然吞吐量更大,但不太适合事务场景。RabbitMQ的Erlang栈维护成本高,我们没选。"

场景2:"RocketMQ事务消息你们怎么用的?"

加分回答(真实感):

"下单场景:订单服务先发半消息,然后在本地事务里扣库存、生成订单。本地事务提交后就发Commit,库存服务消费到消息扣减库存。如果订单服务挂了,RocketMQ会回查我们提供的checkLocalTransaction接口,我们根据订单号查数据库状态返回Commit或Rollback。这套方案保证了最终一致性,上线后没出现过数据不一致的问题。"

场景3:"遇到过消息积压吗?怎么处理的?"

加分回答:

"有次大促,下游的积分服务消费太慢(每次都要调外部接口),消息积压了几十万条。我们紧急做了两件事:一是给积分服务扩容了3个实例,同时给Topic加了分区;二是在消费逻辑里把批量调接口改成了异步+缓存。半小时内积压清空了。"

场景4:"消息重复消费过吗?"

加分回答:

"有一次消费者在业务处理完成后、提交offset之前宕机了,重启后消息被重新消费,导致数据重复(比如积分加了两次)。后来我们引入了幂等机制:每条消息带唯一ID,处理前用Redis的SETNX做去重,业务逻辑里也用数据库唯一索引兜底。之后就再没出现过重复问题。"

小结

核心概念 大白话 面试考点
解耦 A不知道B的存在 MQ解决的核心问题之一
异步 核心逻辑先做完,非核心慢慢来 用户体验提升
削峰 把高峰流量存起来慢慢放 保护下游数据库
消息不丢 ACK + 持久化 + 手动提交 各环节怎么保证可靠
消息不重 消费者幂等 唯一ID、Redis去重
消息积压 消费太慢,队列堆满了 扩容消费者 + 优化消费逻辑
Kafka 高吞吐日志场景 选型理由
RocketMQ 事务消息交易场景 半消息 + 本地事务 + 反查
RabbitMQ 低延迟路由灵活 选型理由
死信队列 重试N次失败后的归宿 人工介入处理

记住一句话:MQ就是个带存储的异步通信管道,面试围绕的核心就是------怎么让消息安全地从A到B,不丢、不重、不怕压。

下一篇我们讲Spring Boot------自动配置的原理到底是什么?IoC容器就是个Map?AOP的代理是怎么生成的?把Spring的底裤也扒干净。下期见。

相关推荐
Flittly1 小时前
【AgentScope Java新手村系列】(19)多模态-图像音频视频
java·spring boot·spring
Javatutouhouduan1 小时前
国内大厂Java面试高频题库(2026突击版)
java·架构师·java面试·java面试题·后端开发·java程序员·java八股文
Tim_101 小时前
【C++】017、new/delete与malloc/free的区别
java·数据结构·算法
青山木1 小时前
一把 Redis 分布式锁,踩透四个坑:锁争抢、僵尸锁、锁过期、锁丢失
java·数据库·redis·后端
Java内核笔记2 小时前
万字避坑!Spring Boot 3.x 升 4.0 最全升级指南(附 Migration Checklist,建议收藏⭐)
java·后端
从零开始的代码生活_2 小时前
C++ list 原理与实践:双向链表、迭代器与简化实现
开发语言·c++·后端·学习·算法·链表·list
hy.z_7772 小时前
【C语言】11. 深入理解指针1
c语言·开发语言
charlie1145141912 小时前
现代C++工程实践——WeakPtr 第二关:核心骨架与控制块
开发语言·c++
cfm_29143 小时前
SpringBoot启动原理
java·spring boot·spring