消息队列

什么是消息队列(MQ)

MQ(Message Queue)消息队列,是基础数据结构中“先进先出”的一种数据结构。指把要传输的数据(消息)放在队列中,用队列机制来实现消息传递——生产者产生消息并把消息放入队列,然后由消费者去处理。消费者可以到指定队列拉取消息,或者订阅相应的队列,由MQ服务端给其推送消息。

常见的消息队列(如何选型)

Kafka 吞吐量极高(十万级 TPS)、大数据生态友好 运维复杂、事务消息需额外开发 日志采集、大数据流处理、高吞吐业务 吞吐 > 可靠性 > 灵活性
RocketMQ 金融级可靠性、事务消息、顺序性强 多语言支持弱(主要依赖 Java) 金融交易、订单核心流程、事务场景 可靠性 > 吞吐 > 生态(Java 优先)
RabbitMQ 路由灵活、延迟低(微秒级)、多语言友好 吞吐量有限(千级 TPS) 通知推送、复杂路由、中小业务 灵活性 > 延迟 > 吞吐
Pulsar 云原生、多租户、冷热数据分离 社区成熟度略低、运维中等复杂度 云环境、多团队共享、混合消息 / 流场景 云原生 > 多租户 > 存储成本

消息队列的作用

应用解耦

  • 传统方式:系统 A(订单服务)调用系统 B(库存服务)的接口,二者紧耦合。

  • 问题:B 挂了,A 就受影响。改动 B 的接口,A 也要改。

  • MQ 方案:A 只需要把消息(订单数据)丢进 MQ,至于谁来消费(库存服务、物流服务等),A 不关心。 ➡️ 好处:减少系统依赖,模块间更加独立。

异步处理

  • 场景:用户下单后,需要做很多操作:扣库存、生成订单、发送短信、推送消息。

  • 问题:如果全在主流程里处理,响应很慢。

  • MQ 方案:订单服务只负责写订单,其他操作通过 MQ 异步处理(短信服务、推送服务分别消费消息)。 ➡️ 好处:提升响应速度,用户体验更好。

流量削峰

  • 场景:秒杀、双十一大促,瞬间有上百万请求涌入。

  • 问题:数据库、服务直接被压垮。

  • MQ 方案:请求先进入 MQ 队列,消费者服务按照自己的能力慢慢消费处理。 ➡️ 好处:保护下游服务,避免雪崩。

消息重复消费怎么解决?

消息重复消费是分布式系统中常见的问题,本质是由于网络抖动、消费者崩溃、消息确认机制异常等原因,导致同一条消息被消费者多次接收。解决的核心思路就是要保证消费逻辑的“幂等性”。

为什么会出现重复消费

生产者重试:生产者发送消息后未收到确认,触发重试机制,导致消息重复发送。

消费者确认失败:消费者处理完消息后,向MQ发送ACK时失败,MQ会认为消息未处理,重新投递。

MQ自身机制:部分MQ默认采用至少一次的投递策略(优先保证不丢失消息,允许重复)

消费端幂等设计

基于唯一标识去重(通用的)

为每条消息或业务数据分配全局唯一 ID,消费时通过判断 ID 是否已处理过,避免重复执行。

  • 适用场景:订单、支付、通知等有明确业务标识的场景。

  • 实现方式

    • 消息唯一 ID:利用 MQ 自带的消息 ID(如 RocketMQ 的msgId、Kafka 的offset+partition),但需注意:部分场景下(如生产者重试),同一业务消息可能有不同的消息 ID,此时需结合业务 ID。

    • 业务唯一 ID:用业务自身的唯一标识(如订单号、支付流水号),更可靠。

    • 存储校验:将已处理的 ID 存入数据库 / Redis(如set key value NX),消费前先查询:若已存在则跳过,否则处理并记录。

基于数据库的唯一约束

在表中为业务唯一 ID(如订单号)设置唯一键,重复消费时,数据库会抛出Duplicate key异常,捕获异常后直接忽略即可。

基于状态机幂等

业务数据存在明确的状态流转(如订单:待支付→支付中→已支付),通过状态机判断当前操作是否合法,避免重复执行。

  • 适用场景:有状态流转的业务(如订单支付、任务状态更新)。
  • 实现方式:消费前检查当前状态是否允许执行操作。例如,“支付” 操作仅在订单处于 “待支付” 状态时执行,若已处于 “已支付”,则直接跳过。

基于分布式锁

若业务逻辑无唯一 ID 或状态机,可通过分布式锁保证同一消息同一时间仅被处理一次(避免并发重复)。

  • 适用场景:无明确唯一标识,但需避免并发重复的场景(如库存扣减)。
  • 实现方式:消费时先获取锁(以消息 ID 或业务 ID 为锁键),获取成功则处理,失败则等待或跳过。

消息丢失怎么解决的

消息丢失的 3 个核心环节

  1. 生产者发送阶段:消息未成功投递到 MQ(如网络中断、MQ 接收失败)。
  2. MQ 存储阶段:消息已到达 MQ,但未被持久化或因 MQ 故障(如宕机)丢失。
  3. 消费者消费阶段:消息已被 MQ 投递到消费者,但消费者未处理完成或未确认,导致 MQ 认为消息未消费而 “丢失”(实际是未正确记录消费状态)。

具体分析

1. 生产者发送阶段:确保消息成功投递到 MQ

问题根源

  • 生产者发送消息后,因网络超时、MQ 临时不可用等,未收到 MQ 的 “接收确认”,但消息可能已被 MQ 接收(也可能未接收),若生产者直接放弃,可能导致丢失。
  • 生产者未开启重试机制,单次发送失败后直接丢弃消息。

解决方案

  • 开启发送确认机制(关键):让 MQ 在成功接收并持久化消息后,向生产者返回 “发送成功” 确认(ACK)。生产者只有收到 ACK,才认为消息发送成功;否则触发重试。
    • Kafka:生产者配置acks=all(需所有 ISR 副本确认接收),并开启retries(重试次数,如retries=3)。
    • RocketMQ:使用同步发送(send())而非异步 / 单向发送,通过返回的SendResult判断是否成功(SEND_OK),失败则重试。
    • RabbitMQ:开启publisher-confirms(发布确认机制),通过回调确认消息是否被交换机接收;若失败,触发重试。
  • 避免消息发送阻塞导致丢失:生产者需设置合理的超时时间(如sendTimeout=3000ms),避免因 MQ 繁忙导致发送线程长期阻塞;同时配置消息缓冲区(如 Kafka 的buffer.memory),防止瞬间高并发导致消息被丢弃。
  • 本地持久化兜底(极端场景):若对消息可靠性要求极高(如金融交易),可在生产者本地暂存消息(如写入数据库 / 本地文件),待收到 MQ 的 ACK 后再删除;若重试多次失败,触发人工介入(避免彻底丢失)。

2. MQ 存储阶段:确保消息在 MQ 中可靠存储

问题根源

  • MQ 默认使用内存存储消息,未开启持久化,宕机后内存数据丢失。
  • 虽开启持久化,但采用 “异步刷盘”(先存内存,定期刷到磁盘),宕机时内存中未刷盘的消息丢失。
  • MQ 单节点部署,节点故障后无副本恢复数据。

解决方案

  • 强制开启消息持久化:确保消息到达 MQ 后被写入磁盘,而非仅存于内存。
    • Kafka:创建 Topic 时设置replication-factor ≥ 2(多副本),且消息默认持久化到磁盘(不可关闭)。
    • RocketMQ:消息默认持久化(存储在 CommitLog 文件),可通过persistent=true显式指定(默认即为 true)。
    • RabbitMQ:队列需设置durable=true(队列持久化),消息发送时设置delivery_mode=2(消息持久化),确保队列和消息均写入磁盘。
  • 选择同步刷盘策略(核心场景):牺牲部分性能,确保消息写入磁盘后才返回 “存储成功”,避免异步刷盘的丢失风险。
    • RocketMQ: broker 配置flushDiskType=SYNC_FLUSH(同步刷盘,默认是 ASYNC_FLUSH),适合金融级场景。
    • Kafka:通过acks=all确保消息被所有 ISR 副本持久化后才确认,等价于 “同步到副本磁盘”。
  • 部署多副本 / 集群(高可用):单节点故障时,通过副本恢复数据,避免单点丢失。
    • Kafka:Topic 分区设置多副本(如replication-factor=3),并确保min.insync.replicas ≥ 2(至少 2 个副本同步成功)。
    • RocketMQ:部署主从架构(Master+Slave),消息同步到 Slave 后才确认(syncMaster= true),Master 宕机后 Slave 可切换为 Master。
    • RabbitMQ:开启镜像队列(Mirror Queue),将队列数据同步到多个节点,避免单节点故障丢失。

3. 消费者消费阶段:确保消息被正确处理并确认

问题根源

  • 消费者开启 “自动确认”(Auto ACK),消息一到达消费者就被 MQ 标记为 “已消费”,但消费者实际处理失败(如崩溃),导致消息丢失。
  • 消费者处理消息超时(超过 MQ 的重试阈值),MQ 将消息移入死信队列或丢弃(未正确配置死信策略)。

解决方案

  • 关闭自动确认,使用手动 ACK(关键):消费者处理完业务逻辑(如订单入库、支付完成)后,再手动向 MQ 发送 “消费成功” 确认(ACK),确保消息处理完成后才被 MQ 标记为 “已消费”。
    • Kafka:消费者关闭enable.auto.commit(禁止自动提交 offset),在业务处理成功后调用commitSync()手动提交 offset。
    • RocketMQ:消费监听中,处理成功后返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS(默认手动确认),失败返回RECONSUME_LATER(触发重试)。
    • RabbitMQ:关闭autoAck=false,处理成功后调用channel.basicAck(deliveryTag, false)手动确认;失败则调用basicNack让 MQ 重新投递。
  • 合理设置消费超时与重试机制:避免因处理超时导致 MQ 误判消息未消费而丢弃。
    • RocketMQ:调整consumeTimeout(默认 15 分钟),若业务处理耗时较长,适当调大;同时配置重试次数(maxReconsumeTimes),重试失败后移入死信队列(DLQ),避免无限重试导致丢失。
    • RabbitMQ:设置队列的x-message-ttl(消息超时时间)和x-dead-letter-exchange(死信交换机),超时未处理的消息进入死信队列,后续人工处理。
  • 消费逻辑幂等 + 事务:即使消息被重复投递(因重试),也需保证消费逻辑幂等(避免重复处理);若消费涉及数据库操作,需用事务确保 “业务处理” 与 “消息确认” 的原子性(如处理失败则回滚业务,不发送 ACK)。

额外保障:监控与兜底机制

  1. 全链路监控:

    监控 MQ 的消息堆积量、发送成功率、消费成功率(如通过 Prometheus+Grafana),一旦出现发送失败率突增、消费延迟过高,立即告警。

  2. 死信队列(DLQ):

    配置死信队列,将处理失败(超过重试次数)的消息存入 DLQ,而非直接丢弃,后续通过人工或定时任务重试,避免彻底丢失。

  3. 定期数据校验:

    核心业务(如支付)需定期对比生产者发送量、MQ 存储量、消费者处理量,发现数据不一致时及时排查(如通过消息轨迹追踪工具)。

消息保证顺序性

顺序性指 “消息的消费顺序严格遵循发送顺序”(如订单的 “创建→支付→发货” 需按序处理)。顺序混乱的根源是 “并行处理”(多分区、多线程消费),需通过 “串行化” 或 “分区对齐” 解决。

1. 为什么会出现顺序混乱?

  • 多分区 / 队列并行:Kafka/RocketMQ 通过 “分区 / 队列” 并行提升吞吐,同一 Topic 的不同分区 / 队列内消息有序,但跨分区 / 队列无法保证顺序(如消息 1 发往分区 A,消息 2 发往分区 B,消费时可能 2 先被处理)。
  • 多线程消费:即使单分区,若消费者用多线程并行处理,也可能导致顺序混乱(如线程 1 处理消息 1 未完成,线程 2 已处理消息 2)。

2. 保证顺序的核心方案

方案 1:单分区 / 队列 + 单线程消费(最彻底)
  • 原理:将需要保证顺序的消息全部发送到同一个分区 / 队列,且消费者用单线程处理该分区 / 队列的消息,从存储和消费两端强制串行化。
  • 实现
    • Kafka:发送消息时指定固定的partition(如partition=0),或通过自定义分区器(Partitioner)将同一业务标识(如订单号)的消息路由到同一分区;消费者单线程消费该分区。
    • RocketMQ:发送时指定MessageQueueSelector,将同一业务 ID 的消息路由到同一 Queue;消费者使用单线程监听器(MessageListenerOrderly)。
    • RabbitMQ:将消息发送到同一个队列(避免多队列),且消费者使用单线程消费(关闭多消费者实例,或同一队列仅绑定一个消费者)。
  • 适用场景:严格顺序需求(如订单状态变更),但会牺牲吞吐(单分区 / 队列的吞吐有限)。
方案 2:业务标识分区 + 分区内单线程(平衡吞吐与顺序)
  • 原理:按 “业务标识”(如用户 ID、订单号)分区,同一标识的消息进入同一分区(保证该标识内的消息有序),不同标识的消息可并行(提升吞吐)。

  • 示例:

    电商订单场景中,按 “用户 ID” 哈希路由到分区(partition = userID % 分区数),同一用户的订单消息在同一分区内有序,不同用户的消息可并行处理。

  • 注意:需确保分区数固定(避免扩容导致哈希路由变化),且消费者对每个分区使用单线程处理(如 Kafka 的max.poll.records=1,每次拉取 1 条消息处理)。

方案 3:消费端排序(松耦合场景)
  • 原理:允许消息乱序到达消费端,但通过业务层记录消息序号,消费后按序号排序再处理(适合非实时、可缓冲的场景)。

  • 实现:

    生产者发送消息时携带 “全局序号”(如seq=1,2,3…),消费者将消息暂存到本地缓存(如 Redis 有序集合),当检测到序号连续时(如 1、2、3 都到达),按序处理;缺失序号则等待重试。

  • 适用场景:顺序要求不严格、允许短暂乱序(如日志聚合),可兼容高吞吐。

如何保证幂等写?

幂等性是指 同一操作的多次执行对系统状态的影响与一次执行结果一致。例如,支付接口若因网络重试被多次调用,最终应确保仅扣款一次。实现幂等写的核心方案:

  • 唯一标识(幂等键):客户端为每个请求生成全局唯一ID(如UUID、业务主键),服务端校验该ID是否已处理,适用场景接口调用、消息消费等。
  • 数据库事务+乐观锁:通过版本号或状态字段控制并发更新,确保多次更新等同于单次操作,适用场景数据库记录更新(如余额扣减、订单状态变更)。
  • 数据库唯一约束:利用数据库唯一索引防止重复数据写入,适用场景数据插入场景(如订单创建)。
  • 分布式锁:通过锁机制保证同一时刻仅有一个请求执行关键操作,适用场景高并发下的资源抢夺(如秒杀)。
  • 消息去重:消息队列生产者为每条消息生成唯一的消息ID,消费者在处理消息前,先检查该消息ID是否已经处理过,如果已经处理过则丢弃该消息。

如何解决消息堆积

生产者发送消息的速度远超过消费者处理速度,导致 MQ 中未处理的消息数量持续增长。

在动手解决前,必须先明确堆积根源,避免盲目操作。核心通过 3 个维度排查:

  1. 生产端:查看消息生产速率是否突然飙升(如峰值流量、重复发送),是否存在 “发而不管” 的无限流逻辑。
  2. 消费端:检查消费者实例数、单实例处理速率、是否有报错 / 阻塞(如数据库慢查询、远程调用超时)。
  3. 中间件:确认队列 /topic 的分区数、存储容量、是否存在配置限制(如最大堆积数、刷盘策略导致的性能瓶颈)。

1. 紧急缓解:快速降低堆积量

适用于堆积已经影响业务的场景,优先恢复服务可用性。

  • 水平扩展消费端:最直接的方式,增加消费者实例数量(如从 2 台机器扩到 10 台)。注意:需确保中间件支持并发消费(如 Kafka 分区数≥消费者数,RabbitMQ 队列允许多消费者监听)。
  • 临时扩容消费能力:在单个消费者实例内,通过线程池提高并发处理数(如从单线程改为 10 线程),但需避免线程过多导致资源竞争(如数据库连接池耗尽)。
  • 降级非核心消费逻辑:暂时关闭消费链路中的非必要步骤(如日志打印、非核心数据校验),优先保证消息 “快速处理完”,后续再补全逻辑。

2. 根源优化:避免再次堆积

解决紧急问题后,需从根源上优化流程,防止问题复发。

  • 优化消费端处理效率
    • 减少同步阻塞:将消费中的同步远程调用(如 HTTP 请求)改为异步调用,或增加超时时间限制(避免线程挂起)。
    • 批量处理消息:开启中间件的批量拉取功能(如 Kafka 的fetch.min.bytes配置),同时批量写入数据库(如 MySQL 的batch insert),减少 IO 次数。
    • 优化代码逻辑:修复消费端的慢代码(如 O (n²) 算法、未加索引的 SQL),通过压测提前发现性能瓶颈。
  • 管控生产端发送速率
    • 增加限流机制:在生产端设置流量阈值(如每秒最多发 1000 条),超过阈值时触发降级(如返回 “暂时繁忙”),避免消息 “爆冲”。
    • 避免重复发送:生产端增加幂等设计(如消息 ID 去重),防止因重试、网络波动导致的重复消息,减少消费端无效工作。
  • 调整中间件配置
    • 增加分区 / 分片数:对于 Kafka、RocketMQ 等中间件,分区数决定了最大并发消费能力,需根据预期消费速率合理设置(如目标每秒处理 5000 条,可设 10 个分区,每个分区承载 500 条 / 秒)。
    • 优化存储策略:若消息无需长期保存,缩短消息保留时间(如从 7 天改为 1 天),减少中间件存储压力;非核心消息可使用 “内存队列”,降低磁盘 IO 开销。

消息队列是参考哪种设计模式?

消息队列(MQ)的设计核心参考了发布 - 订阅模式(Publish-Subscribe Pattern),同时也融合了中介者模式(Mediator Pattern) 的思想。

1. 核心参考:发布 - 订阅模式(Publish-Subscribe Pattern)

发布 - 订阅模式是消息队列最直接的设计原型,其核心逻辑是:

  • 发布者(Publisher):负责产生消息并 “发布” 到一个中间载体(如 MQ 的 “主题 Topic” 或 “交换机 Exchange”),无需关心谁会接收消息。
  • 订阅者(Subscriber):通过 “订阅” 中间载体,接收并处理感兴趣的消息,无需关心消息来自哪个发布者。
  • 中间载体:作为消息的中转站,负责存储和分发消息(如 MQ 的 Broker 节点),实现发布者与订阅者的完全解耦。

2. 辅助支撑:中介者模式(Mediator Pattern)

中介者模式的核心是通过一个 “中介对象” 协调多个对象的交互,避免对象之间直接耦合(减少多对多依赖)。消息队列的 Broker 节点本质上就是一个 “中介者”:

  • 生产者无需知道消费者的存在,只需将消息发送给 Broker;
  • 消费者无需知道生产者的存在,只需从 Broker 获取消息;
  • 生产者与消费者的交互完全通过 Broker 中转,Broker 负责处理消息的存储、重试、路由等复杂逻辑,简化了生产者和消费者的实现。

如何设计一个消息队列

第一是伸缩性,以 Partition 为最小单元,每个 Partition 对应一个 Broker 存储部分数据。需要扩容时,要么直接给 Topic 加 Partition 提升并行度,要么加 Broker 后通过元数据服务自动迁移 Partition,快速提升吞吐量和容量。

第二是持久化,用磁盘顺序写日志文件,配合操作系统的 PageCache 优化:读的时候预加载相邻数据到缓存,写的时候先存缓存再异步刷盘,既避免进程挂了丢数据,又能保证读写效率。

第三是可用性,每个 Partition 做 1 主 N 从的副本,主副本对外提供服务,从副本同步数据。主副本故障时,会从同步正常的副本里选新主,自动切换不中断服务,靠冗余实现高可用。

RocketMQ延时消息的底层原理

RocketMQ 的延时消息(延迟队列)底层通过 “临时存储 + 定时扫描迁移” 机制实现,核心是将延迟消息先暂存到特殊队列,到期后再转发到目标队列。具体原理可拆解为 3 步:

1. 延迟消息的临时存储

  • 当生产者发送延迟消息时(指定delayLevel,如 10 秒、1 分钟),消息不会直接进入目标 Topic,而是被路由到 RocketMQ 内置的延迟主题(SCHEDULE_TOPIC_XXXX
  • 延迟主题下包含多个分区,每个分区对应一个延迟级别(如分区 0 对应 1 级延迟,分区 1 对应 2 级延迟)。消息根据指定的delayLevel被存入对应分区,实现按延迟级别分类存储。

2. 定时扫描与到期判断

  • Broker 内部有一个定时任务线程池,默认每 100ms 扫描一次延迟主题的所有分区。
  • 扫描时,线程会读取分区内的消息,根据消息的 “发送时间 + 延迟级别对应的时长” 计算到期时间,判断消息是否已到期。
  • 延迟级别(delayLevel)是预定义的(如 1 级 = 1s、2 级 = 5s、18 级 = 2h),不支持自定义时长(需修改源码扩展)。

3. 到期消息的迁移与投递

  • 对于已到期的消息,Broker 会将其从延迟主题中删除,并重新写入生产者指定的目标 Topic
  • 消息进入目标 Topic 后,就和普通消息一样,会被消费者正常拉取和消费。

RocektMQ怎么处理分布式事务?

RocketMQ 处理分布式事务的核心思路是 “两阶段提交 + 事务状态回查”,用通俗的话讲就是:先 “占个坑”,确认业务成功后再 “填坑”,失败了就 “撤坑”,确保消息和本地事务要么都成功,要么都失败。

1. 发送 “半消息”(第一阶段:占坑)

生产者(订单系统)先给 RocketMQ 发一条 “半消息”(Half Message)。

  • 半消息:能被 Broker 接收并持久化,但 暂时不会被消费者看到(像 “待确认” 状态)。
  • 作用:先在消息队列里 “占个位置”,确保消息不会丢,但也不会提前触发远程事务。

2. 执行本地事务

生产者发送半消息成功后,立即执行本地事务(比如创建订单)。

  • 本地事务成功:比如订单表插入成功。
  • 本地事务失败:比如订单创建时数据库报错。

3. 提交 / 回滚消息(第二阶段:填坑 / 撤坑)

生产者根据本地事务结果,给 Broker 发 “确认指令”:

  • 若本地事务成功:发 “提交消息” 指令,Broker 标记半消息为 “可用”,消费者就能收到消息(触发库存减少)。
  • 若本地事务失败:发 “回滚消息” 指令,Broker 删掉半消息,消费者不会收到(避免库存错误减少)。

关键:解决 “指令丢失” 问题(事务回查)

如果生产者在发 “确认指令” 时宕机了(比如网络断了),Broker 会定期主动 “回查” 生产者

  • Broker 问生产者:“你之前那个半消息对应的本地事务,到底成没成功?”
  • 生产者查本地事务日志(比如订单表状态),告诉 Broker 结果,Broker 再按结果处理(提交 / 回滚)。

RocketMQ消息顺序怎么保证?

RocketMQ 保证消息顺序的核心逻辑是 “分区内有序 + 消费绑定”

“通过 Key 路由到同一分区(保证写入顺序) + 分区与消费者一对一绑定(保证消费顺序)”

1. 为什么需要顺序?

比如 “订单创建→支付→发货” 这 3 条消息,必须按顺序消费,否则消费者先处理 “发货” 再处理 “创建” 就会出错。

2. 怎么保证顺序?(两步核心)

(1)生产者:确保消息 “按顺序进同一个分区”

  • RocketMQ 中,一个 Topic 可以分成多个 Partition(类似多个 “队伍”)。

  • 同一类需要顺序的消息(比如同一个订单的消息),必须通过“相同的 Key”路由到同一个 Partition。

    举例:用订单 ID 作为 Key,RocketMQ 会根据 Key 计算哈希值,让同一个订单的所有消息都进入同一个 Partition。

  • 单个 Partition 内部,消息是 “先进先出” 的(类似排队,先到的消息先存),所以分区内天然有序。

(2)消费者:确保 “一个分区只被一个消费者处理”

  • 消费者以 “消费组” 的形式工作,一个消费组可以有多个消费者(类似多个 “打饭窗口”)。

  • 为了避免顺序混乱,RocketMQ 会让一个 Partition 只分给消费组里的一个消费者。

    比如:Partition 0 只由消费者 A 处理,Partition 1 只由消费者 B 处理,各自按顺序消费自己分区的消息,不会交叉。

对Kafka有什么了解吗?

  • 高吞吐量:依赖 “顺序读写磁盘”(比随机 IO 快 10 倍以上)、“PageCache 缓存”(读写先经内存)、“批量处理”(批量发送 / 拉取),单 Broker 每秒可处理几十万甚至消息。
  • 高可靠性:每个分区可设置多个副本(Replica),1 个主副本(Leader)处理读写,其余从副本(Follower)同步数据,主副本故障时自动切换从副本,避免数据丢失。
  • 持久化:消息默认持久化到磁盘,支持按时间 / 大小清理旧数据(如保留 7 天),适合存储海量历史数据。
  • 流式处理友好:消费者通过Offset(偏移量)记录消费进度,支持 “从头消费”“指定位置消费”,与 Spark、Flink 等流处理框架集成紧密。

场景:

  • 日志收集(如 ELK 栈,收集分布式系统日志)。
  • 实时数据管道(如实时数仓,从业务库同步数据到分析系统)。
  • 高吞吐消息队列(如用户行为追踪,每秒百万级埋点数据传输)。

Kafka 为什么这么快?

Kafka 快的核心逻辑是:用顺序读写规避磁盘短板,用缓存减少实际 IO,用批量降低交互成本,用分区实现并行处理

机械硬盘的 “随机读写” 很慢(磁头要来回移动寻道),但 “顺序读写” 速度接近内存 —— 因为不需要频繁寻道,只需连续存储 / 读取。

Kafka 不自己管理内存缓存,而是直接依赖操作系统的 PageCache(磁盘缓存):

  • 写入消息时,先存到 PageCache(内存),就返回 “成功”,后续由操作系统异步批量刷到磁盘(减少磁盘 IO 次数)。
  • 读取消息时,先查 PageCache,命中的话直接从内存读(速度极快);没命中再读磁盘,且操作系统会 “预读” 相邻数据到缓存(比如读 1 条消息时,顺便把后面几条也加载到缓存),后续消费几乎都是内存操作。

网络传输和磁盘 IO 的 “单次操作成本” 很高(比如建立连接、寻址),Kafka 通过 “批量攒消息” 降低这种成本:

  • 生产者(Producer)会把多条消息攒成一个批次再发送(可配置批量大小和超时时间)。

  • 消费者(Consumer)一次拉取一批消息,而不是单条获取。

  • Broker 刷盘时,也是批量把 PageCache 中的消息同步到磁盘,而不是每条消息单独刷盘。

    批量操作能把 “多次小交互” 变成 “一次大交互”,大幅提升效率。

Kafka 的每个 Topic 会拆分成多个 Partition(分区),每个分区独立存储、独立处理读写:

  • 生产者可以同时向多个分区写消息(并行写入)。

  • 消费者组的多个消费者可以同时消费不同分区的消息(并行读取)。

    就像多车道高速公路,车流量(消息量)能随车道数(分区数)线性提升,吞吐量自然就高了。

kafka的模型介绍一下,kafka是推送还是拉取?

Kafka 是基于 “发布 - 订阅(Pub-Sub)” 的分布式消息系统,核心模型可以用 “生产者 - 主题 - 消费者” 三要素概括:

  1. 主题(Topic):消息的逻辑分类(比如 “用户行为日志”“订单数据”),是生产者和消费者的交互入口。
  2. 分区(Partition):每个 Topic 会被拆分成多个 Partition(物理存储单元),消息按规则(如哈希)分配到不同 Partition,实现并行读写(提升吞吐量)。单个 Partition 内的消息按写入顺序存储(保证分区内有序)。
  3. 生产者(Producer):向 Topic 发送消息,通过指定消息 Key 或轮询策略,将消息路由到具体 Partition。
  4. 消费者与消费组(Consumer Group):
    • 消费者从 Topic 的 Partition 中读取消息,通过 “偏移量(Offset)” 记录自己的消费进度(类似 “读书时的书签”)。
    • 消费组是多个消费者的集合,组内消费者共同消费一个 Topic 的所有 Partition(1 个 Partition 仅被组内 1 个消费者消费,避免重复)。

Kafka 采用 “消费者主动拉取(Pull)” 模式,即消费者根据自己的处理能力,主动向 Broker 请求消息。

为什么不用推送(Push)?

推送模式下,Broker 难以判断消费者的处理能力:如果 Broker 推送速度超过消费者的处理速度,会导致消费者过载(比如消息堆积、内存溢出)。

而拉取模式的优势是:

  • 消费者可以自主控制拉取频率和批量大小(比如处理快的消费者多拉点,处理慢的少拉点),避免被 “压垮”。
  • 配合 Offset 机制,消费者可以灵活选择重新消费(比如从最早的消息开始读),更适合流处理场景。

RabbitMQ死信消息的来源?

介绍一下死信的概念:无法被消费者消费的消息

  • 消息 TTL 过期
  • 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
  • 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false

RabbitMQ死信队列的用处?

(1)防止正常队列被 “垃圾消息” 阻塞

如果一条消息因为格式错误、依赖资源不可用等原因,无论消费多少次都会失败。如果没有 DLQ,这条消息会反复被重试,占用队列资源,甚至导致正常消息处理延迟。DLQ 会把这类 “无效消息” 移走,保证正常队列的处理效率。

(2)保留异常消息,便于排查问题

死信队列会暂存所有处理失败的消息,开发或运维人员可以从 DLQ 中查看这些消息的内容、元数据(如发送时间、重试次数),快速定位故障原因(比如消息格式错误、下游服务 bug)。

(3)支持消息二次处理

对于暂时失败但可能恢复的消息(如因网络波动导致的处理失败),可以从 DLQ 手动或自动将消息重新投递到正常队列重试,避免消息直接丢失。

RabbitMQ支持延迟队列吗?

RabbitMQ 支持延迟队列,但并非原生内置专门的 “延迟队列” 类型,而是通过两种方式实现,

TTL(消息过期时间)+ 死信队列(DLQ)

  1. 创建一个 “死信交换机” 和 “死信队列”(用于接收过期消息)。
  2. 创建普通队列,配置两个参数:
    • x-message-ttl:消息过期时间(如 30 分钟),消息在该队列中超过此时长未被消费,会变成 “死信”。
    • x-dead-letter-exchange:指定死信交换机(消息过期后会被转发到这里)。
  3. 生产者发送消息到普通队列,消息不会被立即消费,等待 TTL 过期后,自动进入死信队列,消费者从死信队列获取消息并处理(如订单超时取消)。

延迟消息插件(RabbitMQ Delayed Message Plugin)

通过自定义交换机(类型 x-delayed-message),消息发送时指定 x-delay 头部(延迟时间),交换机会按延迟时间排序,到期后再转发到绑定队列,延迟更精准。