文章

八股文-中间件篇

八股文-中间件篇

消息中间件

【问题】 什么是消息队列?为什么使用mq?使用mq会带来什么问题?使用场景有哪些?

【答案】 一、什么是消息队列? 消息队列(Message Queue,简称 MQ)是一种跨进程通信的中间件,用于在分布式系统中实现异步通信。它采用“生产者-消费者”模型,生产者将消息发送到队列中,消费者从队列中拉取或由队列推送消息进行处理。消息队列能够解耦系统组件、缓冲流量、实现异步处理,并保证消息的可靠传递。

二、为什么使用 MQ?

  1. 解耦:通过 MQ 作为中间层,生产者和消费者无需直接交互,降低了系统间的依赖。即使某个消费者故障,生产者仍可正常发送消息。
  2. 异步处理:将耗时操作放入消息队列,生产者无需等待处理完成,提升响应速度。例如,用户注册后发送邮件通知,可异步完成。
  3. 削峰填谷:应对突发流量,将大量请求暂存于队列,由消费者按自身能力平稳处理,避免系统被瞬间峰值冲垮。
  4. 数据同步:在多个系统间同步数据(如订单状态同步),利用 MQ 保证最终一致性。
  5. 可恢复性:消息持久化后,即使消费者故障,重启后仍能继续处理未消费的消息,提高系统容错能力。

三、使用 MQ 会带来什么问题?

  1. 系统复杂性增加:引入 MQ 需要处理消息丢失、重复消费、顺序性等问题,增加了运维和开发成本。
  2. 一致性问题:分布式场景下,生产者与消费者之间的数据一致性难以保证,可能需要引入分布式事务或最终一致性方案。
  3. 消息丢失风险:若 MQ 或网络故障,可能导致消息丢失,需要配置持久化、确认机制等来降低风险。
  4. 消息重复消费:在网络抖动或消费者重试时,可能收到重复消息,要求业务方实现幂等处理。
  5. 顺序性保障困难:在分布式队列中,保证全局顺序性往往牺牲性能和吞吐量。
  6. 可用性依赖:MQ 成为系统关键依赖,若 MQ 宕机,可能影响业务,需部署高可用集群。

四、使用场景有哪些?

  1. 异步处理:如用户注册后发送邮件/短信、订单创建后生成物流单等,提升主流程响应速度。
  2. 应用解耦:订单系统与库存系统、积分系统等通过 MQ 解耦,避免服务间直接调用。
  3. 流量削峰:秒杀、抢票等场景,将请求先写入队列,后端按能力处理,防止数据库被打垮。
  4. 日志收集:将日志发送到 MQ,由专门的分析系统消费,实现异步、批量处理。
  5. 数据同步:跨数据中心数据复制、业务系统间数据同步(如从 MySQL 同步到缓存或搜索引擎)。
  6. 分布式事务:利用 MQ 实现最终一致性(如本地消息表 + 消息队列)。
  7. 任务调度:延迟队列实现定时任务,如订单超时关闭、优惠券到期提醒等。

【大白话解释】

  • 消息队列是什么:就像一个信箱。你(生产者)把信(消息)塞进去,邮递员(消费者)有时间了再来取信。你不用等邮递员当场取走,也不需要知道邮递员是谁。
  • 为什么用
    • 解耦:你和收信人不用见面,想换个人取信也方便。
    • 异步:你塞完信就去干别的事,不用在那干等。
    • 削峰:如果突然有一万个人同时塞信,信箱会先把信存着,邮递员慢慢取,不会把邮局挤爆。
  • 带来什么问题
    • 信箱可能会被偷(消息丢失),需要加锁(持久化)。
    • 邮递员可能重复取同一封信(重复消费),你收信时得防重。
    • 如果信有顺序要求,比如必须先到先处理,在分布式环境下很难保证。
  • 使用场景
    • 你注册账号,系统立即返回成功,但邮件通知可以慢慢发(异步)。
    • 下单后,订单系统和库存系统不用互相等待,通过 MQ 通知减库存(解耦)。
    • 秒杀时,把抢购请求先放进队列,后端慢慢处理(削峰)。

【扩展知识点详解】

  1. 常见消息队列中间件:RabbitMQ(基于 AMQP,稳定)、RocketMQ(阿里开源,高吞吐)、Kafka(高吞吐,适合日志)、ActiveMQ(经典)、Pulsar(云原生)等。
  2. 消息模型
    • 点对点(P2P):队列,一条消息只被一个消费者消费。
    • 发布/订阅(Pub/Sub):主题,一条消息被所有订阅者消费。
  3. 消息可靠性:涉及生产者确认、消息持久化、消费者确认、死信队列等机制。
  4. 重复消费与幂等性:消费者需要实现幂等设计,如使用唯一 ID 判断是否已处理。
  5. 顺序消息:部分 MQ 支持分区顺序,将同一业务 ID 的消息路由到同一分区,保证该分区内顺序。
  6. 消息积压处理:监控队列深度,必要时增加消费者或临时扩容。
  7. 选型考量:业务场景、吞吐量、顺序性要求、可靠性、开发语言生态、运维成本等。

【问题】 消息队列中JMS和AMQP有什么区别?常见的消息队列有哪几个?他们分别用的是什么?

【答案】 一、JMS 和 AMQP 的区别 JMS(Java Message Service,Java消息服务)和 AMQP(Advanced Message Queuing Protocol,高级消息队列协议)是两种不同的消息中间件规范/协议,其主要区别如下:

对比维度JMSAMQP
定位Java 平台专用的 API 规范,定义了消息传递的标准接口,但不规定传输协议。一种线级协议(wire-level protocol),规定了消息格式、网络传输和交互方式,与语言和平台无关。
语言/平台仅限 Java 语言,要求消息提供方和消费方都使用 Java。平台中立,任何语言只要实现 AMQP 协议均可互通,支持跨语言通信。
消息模型支持两种模型:点对点(Queue)和发布/订阅(Topic)。支持更灵活的消息模型,通过交换器(Exchange)和绑定(Binding)实现多种路由策略(Direct、Topic、Fanout、Headers等)。
消息可靠性定义了事务、持久化、确认等机制,但依赖具体实现。协议层面定义了可靠传输机制,如生产者确认(Publisher Confirm)、消费者确认(Consumer Ack)、事务、持久化等,实现更规范。
跨平台性仅限 Java 内部互通。跨语言、跨平台,不同语言的客户端可通过同一协议通信。
互操作性不同 JMS 实现(如 ActiveMQ、WebLogic JMS)之间无法直接通信。遵循同一协议的不同实现(如 RabbitMQ、Qpid)可以互通(需配置)。

二、常见的消息队列及其使用的协议/实现

消息队列使用的协议/实现说明
RabbitMQAMQP 0-9-1(主要),也支持 STOMP、MQTT、HTTP 等基于 Erlang 开发,是最流行的 AMQP 实现之一,支持丰富的路由模式。
Apache RocketMQ自定义私有协议(基于 TCP),也提供 HTTP 等接口阿里开源,高性能、低延迟,支持事务消息、延迟消息,主要应用于阿里生态。
Apache Kafka自定义二进制协议(基于 TCP)专为高吞吐日志处理设计,采用分区+顺序写入,不遵循 JMS 或 AMQP,但提供了类似消息队列的功能。
ActiveMQ支持多种协议:OpenWire、STOMP、AMQP(1.0 和 0-9-1)、MQTT、WebSocket 等经典 Java 消息中间件,支持 JMS 1.1 规范,功能全面。
Apache Pulsar自定义协议(Binary),也支持 Kafka API 和 AMQP 等云原生消息队列,支持多租户、跨地域复制、分层存储。
Redis自定义协议(RESP),通过 List、Pub/Sub 等实现轻量级消息队列虽非专业 MQ,但常用于简单队列场景。
Amazon SQS私有云服务 API全托管消息队列服务,支持 HTTP/HTTPS 协议。

【大白话解释】

  • JMS:就像 Java 世界里的“标准说明书”,规定了用 Java 写消息系统应该怎么调用接口。但它不管底层怎么传数据,而且只懂 Java 语言,不同厂商的 JMS 实现之间不能直接对话。
  • AMQP:好比国际通用的“电报编码规则”,规定了电报的格式、发送方式、转发规则。任何国家(任何编程语言)只要遵循这个规则,就能互相收发消息,不分语言。
  • 常见消息队列
    • RabbitMQ:AMQP 的模范生,功能全面,适合各种业务。
    • RocketMQ:阿里自家兄弟,性能强悍,扛得住双十一。
    • Kafka:日志小能手,专治海量数据流。
    • ActiveMQ:老牌 Java 老兵,什么协议都能接。

【扩展知识点详解】

  1. JMS 的局限:随着微服务、多语言异构系统兴起,JMS 的 Java 绑定成为瓶颈,因此基于 AMQP、自定义协议的中间件更受欢迎。
  2. AMQP 版本差异:AMQP 0-9-1 和 AMQP 1.0 差异较大。RabbitMQ 主要支持 0-9-1,而 ActiveMQ 5.x 支持 0-9-1,ActiveMQ Artemis 支持 1.0。1.0 更精简,但许多高级路由功能由客户端或 broker 实现。
  3. RocketMQ 与 Kafka 对比:两者都基于自定义协议,RocketMQ 更侧重事务、顺序消息、定时消息等企业级特性,Kafka 更侧重高吞吐、持久化日志存储。
  4. 协议选择影响:如果需要跨语言通信,应选择支持 AMQP 或开放协议的 MQ;如果团队全栈 Java 且已有 JMS 经验,可选用 ActiveMQ 或 Artemis 并遵循 JMS 规范。

【问题】 常见的消息队列分别有哪些消息模型?是怎么实现的?都适用于哪些场景?有什么优劣势?

【答案】 常见的消息队列(RabbitMQ、RocketMQ、Kafka、ActiveMQ、Pulsar 等)在消息模型上主要分为两大类:点对点(队列)模型发布/订阅(主题)模型,但各中间件在实现细节、适用场景和优劣势上有所不同。以下是各主流消息队列的详细对比:

消息队列核心消息模型实现方式适用场景优势劣势
RabbitMQ基于 AMQP 的灵活路由模型(Exchange + Binding + Queue)生产者将消息发送到 Exchange,Exchange 根据路由规则(Direct、Topic、Fanout、Headers)将消息分发到绑定的 Queue;消费者从 Queue 消费。业务解耦、异步处理、高可靠消息、复杂路由场景协议标准、功能丰富(延迟队列、死信队列)、高可用、易用性强吞吐量相对较低(万级 QPS)、消息堆积能力有限
Apache RocketMQ基于 Topic 的发布/订阅模型,支持队列(Queue)分区每个 Topic 包含多个 Queue,生产者写入 Queue,消费者以集群(负载均衡)或广播模式消费;支持顺序消息、事务消息。金融、电商、大数据场景(如双十一)、高可靠、事务消息高吞吐(十万级 QPS)、低延迟、支持分布式事务、顺序消息社区生态相对较小、运维复杂度中等
Apache Kafka基于分区(Partition)的日志存储模型(发布/订阅)Topic 分为多个分区,消息追加到分区日志文件,消费者组内消费者独立消费分区,保证分区内顺序。日志收集、流处理、大数据管道、实时数据管道超高吞吐(百万级 QPS)、持久化、水平扩展能力强功能相对单一(无事务、延迟消息较弱)、可能会丢失消息(默认配置)、运维复杂
ActiveMQ支持 JMS 规范:点对点(Queue)和发布/订阅(Topic)基于 JMS 接口实现,支持持久化、事务、XA 事务;支持多种协议。传统 Java 应用、低并发、需要 JMS 规范兼容的场景协议支持丰富、稳定成熟、与 Java 生态集成好性能较低(万级 QPS)、社区更新缓慢
Apache Pulsar分层架构:Topic 分为多分区,采用存储计算分离消息存储在 BookKeeper 中,Broker 无状态;支持跨地域复制、多租户;提供统一的队列和流模型。云原生、多租户、跨地域复制、同时需要队列和流特性的场景存储计算分离、扩展性强、支持多租户、跨地域复制社区相对年轻、部署运维复杂

补充说明

  • 消息模型本质:点对点(队列)是 1:1(一条消息只被一个消费者消费);发布/订阅(主题)是 1:N(一条消息被所有订阅者消费)。
  • 现代消息队列(如 RocketMQ、Kafka)通过分区(分区内有序)实现了顺序消息和水平扩展。

【大白话解释】

  • RabbitMQ 像邮局,你写好信(消息),选择投递方式(路由),邮局帮你送到对应的信箱(队列),收件人(消费者)从信箱取信。适合需要复杂投递规则、各种业务场景。
  • RocketMQ 像高速物流中心,有多个流水线(分区),货物(消息)按订单(Topic)分类,每批货物在一条流水线上顺序处理。适合电商大促、金融交易等要求高可靠、高吞吐的场景。
  • Kafka 像日志仓库,所有记录(消息)按时间顺序堆在文件柜里,你可以按需批量读取。适合日志收集、大数据分析。
  • ActiveMQ 是传统的邮政系统,支持标准投递方式,适合老项目或简单需求。
  • Pulsar 是新型云邮局,仓库和前台分离,前台可以随时增减,仓库统一管理,适合云上弹性扩展。

【扩展知识点详解】

  1. 点对点模型:典型实现是队列(Queue),一条消息入队后,只会被一个消费者取走,常用于任务分发。
  2. 发布/订阅模型:典型实现是主题(Topic),生产者发布消息到主题,所有订阅该主题的消费者都能收到。Kafka 的消费者组(Consumer Group)实现了“订阅”的负载均衡,一个分区内消息只被组内一个消费者消费,实现了队列与主题的融合。
  3. RabbitMQ 的 Exchange 类型
    • Direct:消息路由到与 routing key 完全匹配的 Queue。
    • Topic:支持通配符匹配(* 匹配一个单词,# 匹配零个或多个)。
    • Fanout:广播,忽略 routing key,发送到所有绑定的 Queue。
    • Headers:根据消息头属性匹配。
  4. RocketMQ 的消息模型
    • 普通消息:无序,高吞吐。
    • 顺序消息:按分区内顺序消费,需确保同一业务 ID 落入同一 Queue。
    • 事务消息:实现分布式事务的最终一致性,通过两阶段提交和反查机制保证。
  5. Kafka 的存储与消费
    • 分区(Partition):物理上是一个日志文件,消息追加写入,消费者记录偏移量(offset)。
    • 消费者组(Consumer Group):一个分区只能被组内一个消费者消费,实现负载均衡;不同组可独立消费同一 Topic。
  6. 选型建议
    • 对吞吐量要求不高、需要复杂路由、易用性优先 → RabbitMQ。
    • 高并发、高可靠、事务消息、顺序消息 → RocketMQ。
    • 海量日志、流处理、大数据场景 → Kafka。
    • Java 老系统、JMS 标准要求 → ActiveMQ。
    • 云原生、多租户、跨地域复制 → Pulsar。

【问题】 各个消息队列是怎么保证消息不丢失的?分别从生产者和消费者的角度讲讲?什么是消息的幂等性?怎么做到强一致性和最终一致性?

【答案】 一、消息队列如何保证消息不丢失? 不同消息队列的实现机制略有差异,但核心思路都是通过生产端确认服务端持久化消费端确认三个环节来保障消息不丢失。以 RabbitMQ、RocketMQ、Kafka 为例:

环节机制RabbitMQRocketMQKafka
生产者确认机制发布确认(Publisher Confirm):生产者发送消息后等待 Broker 返回 ACK,若超时或收到 NACK 则重试。同步/异步发送:同步等待 SendResult,或异步回调;若失败则重试。生产者设置 acks=all,等待所有副本确认;retries 配置重试。
服务端持久化队列和消息均设置为持久化(durable=true, delivery_mode=2);消息写入磁盘后才返回 ACK。消息刷盘策略:同步刷盘(SYNC_FLUSH)或异步刷盘;Broker 主从同步。分区多副本:min.insync.replicas 配置最小同步副本数,ISR 机制保证同步。
消费者确认机制手动 ACK:消费者处理完成后手动确认(basicAck),未确认则消息会重新入队或进入死信队列。消费者返回 ConsumeConcurrentlyStatus.CONSUME_SUCCESS 后才删除消息;否则重试或进入死信队列。消费者提交 offset:处理完成后提交 offset,若未提交则重启后重新消费。

二、什么是消息的幂等性? 幂等性是指对同一消息进行多次操作所产生的结果与执行一次完全相同。在消息队列中,由于网络抖动、消费者重启等原因,可能导致同一条消息被重复消费,因此消费者必须实现幂等处理。

常见幂等实现方式

  • 唯一ID去重:为每条消息生成唯一ID,消费者在处理前查询数据库或缓存是否已处理过。
  • 业务表设计:利用数据库唯一约束,例如订单号作为唯一键,重复插入会失败。
  • 状态机:通过状态流转保证同一操作只执行一次(如订单从待支付到已支付)。

常见幂等场景

  • 微信支付结果通知场景:微信官方文档上提到微信支付通知结果可能会推送多次,需要开发者自行保证幂等性。第一次我们可以直接修改订单状态(如支付中 -> 支付成功),第二次就根据订单状态来判断,如果不是支付中,则不进行订单处理逻辑。
  • 数据库写操作场景:例如用户注册时,可能会收到重复的注册请求。我们可以在数据库中添加唯一索引(如手机号),重复插入会失败,从而实现幂等性。
  • 数据库读操作场景:例如查询订单状态时,可能会收到重复的查询请求。我们可以在数据库中添加索引(如订单号),避免重复查询。
  • 缓存场景:例如用户查询个人信息时,可能会收到重复的查询请求。我们可以在缓存中添加过期时间,过期后重新查询数据库,避免重复查询。
  • 消息队列场景:例如订单支付后,可能会收到重复的支付通知。我们可以在消息队列中添加唯一ID去重,避免重复处理。
  • 状态机场景:例如订单状态流转,可能会收到重复的状态更新请求。我们可以在状态机中添加状态校验,避免重复状态流转。
  • 其他场景:例如用户取消订单时,可能会收到重复的取消请求。我们可以在业务逻辑中添加状态校验,避免重复取消。

三、怎么做到强一致性和最终一致性? 在分布式系统中,消息队列常用于实现数据一致性,主要通过两种模式:

  1. 强一致性
    要求生产者、消息队列、消费者三者之间的事务同步完成,任一环节失败则整体回滚。典型实现是 XA 分布式事务(两阶段提交),但性能较低,很少在实际生产中使用。
    更常见的“强一致性”场景是在单个 Broker 内部通过同步刷盘 + 同步复制实现,如 RocketMQ 同步刷盘 + 主从同步,保证消息不丢失,但生产者和消费者之间仍是异步。

  2. 最终一致性
    通过事务消息、本地消息表等机制实现,允许短暂不一致,最终达到一致。

    • RocketMQ 事务消息:两阶段提交 + 反查机制。生产者发送半消息(对消费者不可见),执行本地事务,根据本地事务结果决定提交或回滚,若长时间未确认,Broker 会反查生产者状态,保证事务最终一致。
    • 本地消息表 + 定时任务:业务数据库保存消息记录,与业务操作在同一本地事务中;异步轮询发送消息,消费成功后更新状态。

【大白话解释】

  • 消息不丢失:好比快递,寄件人(生产者)要确认快递员(MQ)已揽收(确认机制);快递公司(服务端)要登记在册并锁进仓库(持久化);收件人(消费者)要签收(手动 ACK)后才算完成。三方都确认,才保证快递不丢。
  • 幂等性:就像快递员送同一个包裹,你签收了两次,但门卫只登记一次,第二次看到单号已签收就直接拒绝,这就是幂等。
  • 强一致性:如同银行转账,要么钱同时转出和转入,要么都不动,同步完成。
  • 最终一致性:比如下订单后发短信通知,允许短信晚几分钟发,但最终总会发出去。

【扩展知识点详解】

  1. RabbitMQ 不丢消息的关键配置
    • 生产者:channel.confirmSelect() 开启发布确认,waitForConfirms() 同步等待。
    • 服务端:队列声明时 durable=true,消息发送时 deliveryMode=2,并开启镜像队列提高可用性。
    • 消费者:channel.basicAck(deliveryTag, false) 手动确认。
  2. RocketMQ 不丢消息的关键配置
    • 生产者:send() 方法同步发送,设置 retryTimesWhenSendFailed
    • 服务端:flushDiskType=SYNC_FLUSH(同步刷盘),brokerRole=SYNC_MASTER(同步双写)。
    • 消费者:返回 CONSUME_SUCCESS 后才会更新消费进度。
  3. Kafka 不丢消息的关键配置
    • 生产者:acks=allretries=Integer.MAX_VALUEmax.in.flight.requests.per.connection=1(保证顺序)。
    • 服务端:min.insync.replicas=2replication.factor=3
    • 消费者:enable.auto.commit=false,手动调用 commitSync() 提交 offset。
  4. 幂等性实现细节
    • 使用 Redis 的 SETNX 或数据库唯一索引保证幂等。
    • 对于业务操作,可利用乐观锁(version 字段)或状态机控制。
  5. 最终一致性与分布式事务
    • 除了 RocketMQ 事务消息,还有 Seata(AT、TCC)等框架,以及通过本地消息表 + MQ 实现的可靠消息最终一致性方案。

rabbitMQ消息乱序场景

rabbitMQ消息乱序解决方案


【问题】 消息队列中,消息的重发、补充策略有哪些?如何应对消息堆积?比如一个实时性要求比较高的支付场景支付模块因为机器内存不足频繁GC导致消息大量堆积该怎样处理?

【答案】 一、消息的重发与补充策略 消息重发通常发生在生产者发送失败或消费者处理失败时,目的是保证消息最终被成功处理。

  1. 生产者端重发策略
    • 同步重试:发送消息时若未收到 Broker 确认,在超时后立即重试,可设置最大重试次数。
    • 异步重试:将发送失败的消息存入本地数据库或日志,由后台线程定时扫描并重发,适用于对延迟不敏感的场景。
    • 指数退避:重试间隔逐渐增大(如 1s、2s、4s…),避免频繁重试加剧系统压力。
  2. Broker 端重发策略
    • 死信队列:消息消费失败超过一定次数后,转入死信队列(Dead Letter Queue),由人工或专门程序处理。
    • 延迟重试:RocketMQ 等支持将失败消息放入指定延迟等级的重试队列,消费者稍后重新消费。
  3. 消费者端重试策略
    • 本地重试:消费者捕获异常后,在业务代码中立即重试若干次(适用于短暂异常)。
    • 手动 ACK 控制:不确认消息,让消息重新入队(RabbitMQ 的 basicNack + requeue=true),其他消费者可再次尝试。
    • 失败隔离:对可重试的异常(如网络超时)重试,对不可重试的异常(如数据格式错误)直接转入死信队列。
  4. 补充策略(补偿机制)
    • 定时扫描:在业务数据库中记录消息状态(如“待处理”),定时任务扫描长时间未处理的消息,重新发送或触发补偿逻辑。
    • 分布式事务补偿:通过事务消息或本地消息表,确保最终一致性,当主链路失败时由补偿服务执行回滚或重试。

二、如何应对消息堆积

  1. 水平扩容
    • 增加消费者实例数量,并确保消费者组内的消费者数不超过队列/分区数(如 Kafka 一个分区只能被同一组内一个消费者消费)。
    • 增加队列/分区数量,以支持更多消费者并行处理。
  2. 提升消费能力
    • 优化消费逻辑,减少单条消息处理耗时。
    • 调整为批量消费,一次拉取多条消息批量处理。
    • 增加消费者线程数(适用于 RabbitMQ、RocketMQ 等支持多线程消费的客户端)。
    • 使用异步消费模型(如 CompletableFuture),提高吞吐量。
  3. 限流与降级
    • 在生产者端临时限流,减少消息发送速度。
    • 对非核心消息进行降级(如暂不处理或记录日志后丢弃)。
    • 调整消费者拉取速度,避免消费过慢导致消息积压。
  4. 监控与预警
    • 设置队列深度告警,及时发现问题。
    • 分析消息积压的原因(消费慢、生产过快、网络问题等),针对性处理。
  5. 应急处理
    • 若因消费者故障导致堆积,可临时启动新消费者接管积压消息,同时修复原消费者。
    • 若因消息体积过大导致传输慢,可压缩消息或拆分。

三、支付场景内存不足导致 GC 频繁的堆积处理 场景描述:支付模块是实时性要求极高的系统,因 JVM 内存不足引发频繁 Full GC,导致消费者线程停顿,无法及时消费消息,队列消息大量堆积,进而可能引发超时、订单状态不一致等问题。

处理步骤

  1. 紧急止血
    • 临时增加消费者实例:快速部署更多支付消费者(同一服务多实例),分担消息压力,缓解单机 GC 影响。
    • 调整消费者拉取参数:减少每次拉取的消息数量(如调小 max.poll.records),降低单次处理压力。
    • 启用快速失败:对部分非关键消息(如日志、统计)直接丢弃或记录后确认,优先保障支付核心消息。
  2. 问题根因排查
    • 使用 jstatjmap 等工具分析内存占用,定位内存泄漏或大对象。
    • 检查消费逻辑是否存在大量临时对象、不合理的数据结构或缓存未释放。
    • 查看 GC 日志,确认 GC 类型、频率和耗时。
  3. 代码与配置优化
    • 优化消费代码:避免在消息处理中频繁创建大对象;使用对象池复用;将可异步处理的步骤(如日志、通知)剥离,使用独立线程池执行。
    • 调整 JVM 参数:适当增加堆内存(如 -Xmx),选择合适的垃圾回收器(如 G1、ZGC),调整新生代和老年代比例,降低 GC 停顿时间。
    • 启用消费端限流:结合信号量或线程池控制并发消费数量,防止因瞬时高负载导致内存飙升。
  4. 长期方案
    • 将支付模块拆分为更细粒度的服务,降低单点内存压力。
    • 引入多级缓存(如本地缓存 + Redis),减少重复对象创建。
    • 使用异步非阻塞框架(如 Netty、WebFlux)提升单机吞吐量,减少线程上下文切换。
    • 监控并设置 JVM 内存预警,在内存达到阈值时自动摘除节点,待 GC 恢复后再接入流量。

【大白话解释】

  • 重发策略:就像寄快递,第一次没送到,快递员会再送几次(重试),如果一直送不到,就退回仓库(死信队列),等查明原因再处理。
  • 补充策略:好比发工资,如果某次银行转账失败,财务会每天查一下,直到转账成功(定时补偿)。
  • 应对堆积:就像超市收银台排队太长,就多开几个收银台(扩容);或者让收银员动作快一点(优化消费);实在不行,先让一部分顾客晚点来(限流)。
  • 支付场景:支付系统就像高铁售票窗口,如果窗口电脑卡顿(GC),售票员干不了活,排队的人越来越多。这时先加开临时窗口(加实例),再把电脑修好(调优 JVM),同时把买票规则简化(优化代码),让队伍尽快消掉。

【扩展知识点详解】

  1. 重试策略的幂等性要求:重试可能导致消息重复消费,消费者必须实现幂等处理(如通过唯一 ID 去重)。
  2. 死信队列的典型用途:存放多次重试仍失败的消息,用于人工介入或事后分析。
  3. Kafka 的堆积处理:增加分区数(alter --partitions)和消费者数,但分区数增加后无法减少;消费者使用批量拉取(max.poll.records)并调整 session.timeout.ms 防止被踢出。
  4. RocketMQ 的堆积处理:可通过 CONSUMER 级别调整消费线程数(consumeThreadMinconsumeThreadMax),支持动态扩容。
  5. RabbitMQ 的堆积处理:可通过增加消费者(basicQos 控制预取数量)和队列长度监控(messages_ready)来定位。
  6. GC 调优关键参数
    • 使用 G1 垃圾回收器:-XX:+UseG1GC,设置 -XX:MaxGCPauseMillis=200 控制停顿时间。
    • 调整堆大小:-Xms-Xmx 设为相同值避免扩容。
    • 开启 GC 日志:-Xloggc:gc.log -XX:+PrintGCDetails
  7. 消费端的熔断机制:当消息堆积超过阈值时,可暂停消费或快速返回失败,避免系统过载。
  8. 支付场景特殊考虑:支付涉及资金安全,重试需谨慎,通常采用“先记账后发消息”模式,确保幂等和最终一致性,并配合对账系统兜底。

kafka

【问题】 介绍下 Kafka?

【答案】 Kafka 是一个分布式流式处理平台。

流平台具有三个关键功能:

  1. 消息队列:发布和订阅消息流,这个功能类似于消息队列,这也是 Kafka 也被归类为消息队列的原因。
  2. 容错的持久方式存储记录消息流:Kafka 会把消息持久化到磁盘,有效避免了消息丢失的风险。
  3. 流式处理平台:在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。

Kafka 主要有两大应用场景:

  1. 消息队列:建立实时流数据管道,以可靠地在系统或应用程序之间获取数据。
  2. 数据处理:构建实时的流数据处理程序来转换或处理数据流。

【大白话解释】 Kafka 就像一个超大的快递分拣中心。有人往里面投包裹(生产者),有人从里面取包裹(消费者),而 Kafka 不仅能暂存包裹,还能按规则分发、持久化保存,甚至对包裹流做实时处理。

【扩展知识点详解】

  1. Kafka 最初由 LinkedIn 开发,后成为 Apache 顶级项目,基于 Scala 和 Java 开发。
  2. Kafka 的“流式处理”能力通过 Kafka Streams 类库实现,可在消息发布时实时处理数据,无需依赖外部计算框架。
  3. Kafka 与传统消息队列(如 RabbitMQ)的核心区别:Kafka 更侧重高吞吐、持久化日志存储,而非复杂路由和消息确认机制。

【问题】 Kafka 有什么优势?

【答案】

  1. 极致的性能:基于 Scala 和 Java 语言开发,设计中大量使用了批量处理和异步的思想,最高可以每秒处理千万级别的消息。
  2. 生态系统兼容性无可匹敌:Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域。

【大白话解释】

  • 极致性能:Kafka 就像一条高速传送带,批量发货、异步装车,每秒能处理千万件包裹。
  • 生态兼容:Kafka 就像万能插头,跟 Hadoop、Spark、Flink 等大数据组件都能无缝对接。

【扩展知识点详解】

  1. Kafka 高性能的核心原因:顺序写入磁盘(比随机写入内存还快)、零拷贝(直接从页缓存发送到网卡,减少用户态拷贝)、批量压缩与发送分区并行
  2. Kafka 生态组件包括:Kafka Connect(数据集成)、Kafka Streams(流处理)、Schema Registry(schema 管理)等。

【问题】 Kafka 的消息模型是什么?

【答案】 Kafka 采用的就是发布-订阅模型

发布订阅模型(Pub-Sub)使用主题(Topic)作为消息通信载体,类似于广播模式;发布者发布一条消息,该消息通过主题传递给所有的订阅者,在一条消息广播之后才订阅的用户则是收不到该条消息的。

在发布-订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样的了。所以说,发布-订阅模型在功能层面上是可以兼容队列模型的。

【大白话解释】 Kafka 的消息模型就像电台广播:电台(发布者)在某个频道(Topic)播节目,所有收听这个频道的听众(订阅者)都能听到。如果只有一个听众,那就跟一对一打电话(队列模型)差不多了。

【扩展知识点详解】

  1. Kafka 通过消费者组(Consumer Group)实现了发布-订阅与点对点的融合:同一个消费者组内,一个分区只能被一个消费者消费(类似点对点);不同消费者组之间,各自独立消费(类似发布-订阅)。
  2. 与 RabbitMQ 的区别:RabbitMQ 通过 Exchange(Direct、Topic、Fanout等)实现路由,模型更灵活但吞吐量较低;Kafka 通过 Topic + Partition 实现简单高效的路由。

【问题】 什么是 Producer、Consumer、Broker、Topic、Partition?

【答案】 Kafka 将生产者发布的消息发送到 Topic(主题)中,需要这些消息的消费者可以订阅这些 Topic(主题)。Kafka 比较重要的几个概念:

  • Producer(生产者):产生消息的一方。
  • Consumer(消费者):消费消息的一方。
  • Broker(代理):可以看作是一个独立的 Kafka 实例。多个 Kafka Broker 组成一个 Kafka Cluster。

同时,每个 Broker 中又包含了 Topic 以及 Partition 这两个重要的概念:

  • Topic(主题):Producer 将消息发送到特定的主题,Consumer 通过订阅特定的 Topic 来消费消息。
  • Partition(分区):Partition 属于 Topic 的一部分。一个 Topic 可以有多个 Partition,并且同一 Topic 下的 Partition 可以分布在不同的 Broker 上,这也就表明一个 Topic 可以横跨多个 Broker。

【大白话解释】

  • Producer:寄件人,往邮局投信。
  • Consumer:收件人,从邮局取信。
  • Broker:邮局,负责暂存和转发信件。
  • Topic:信箱类别,比如“账单信箱”“广告信箱”。
  • Partition:同一个信箱分成了多个格子,不同格子的信可以并行处理。

【扩展知识点详解】

  1. Partition 的数量决定了最大并行度:一个 Topic 的消费者组内最多能有 Partition 数量个消费者同时消费,多余的消费者会空闲。
  2. 消息路由策略:Producer 发送消息时可以指定 Partition,也可以通过 Key 的 hash 值自动路由到对应 Partition,或者使用轮询策略。
  3. Consumer Group Rebalance:当消费者组内的消费者数量发生变化时,会触发再均衡,重新分配 Partition 与消费者的对应关系。

【问题】 什么是 Kafka 分区的多副本机制?

【答案】 分区(Partition)中的多个副本之间会有一个叫做 leader 的角色,其他副本称为 follower。我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。

生产者和消费者只与 leader 副本交互。你可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。当 leader 副本发生故障时会从 follower 中选举出一个 leader,但是 follower 中如果有和 leader 同步程度达不到要求的参加不了 leader 的竞选。

Kafka 通过给特定 Topic 指定多个 Partition,而各个 Partition 可以分布在不同的 Broker 上,这样便能提供比较好的并发能力(负载均衡)。

Partition 可以指定对应的 Replica 数,这也极大地提高了消息存储的安全性,提高了容灾能力,不过也相应的增加了所需要的存储空间。

【大白话解释】 Kafka 的副本机制就像班级里的正副班长:正班长(leader)负责处理所有事务,副班长(follower)只是跟着抄笔记保持同步。正班长请假了,就从副班长里选一个最跟得上进度的顶上。跟不上的副班长没资格竞选。

【扩展知识点详解】

  1. ISR(In-Sync Replicas):与 leader 保持同步的副本集合。只有 ISR 中的 follower 才有资格被选为新的 leader。
  2. OSR(Out-of-Sync Replicas):落后于 leader 的副本。当 follower 迟迟未同步数据时会被移出 ISR,进入 OSR。
  3. High Watermark(HW):消费者只能消费到 HW 之前的消息,HW 之前的消息表示所有 ISR 副本都已同步,保证了数据一致性。
  4. LEO(Log End Offset):每个副本的日志末尾偏移量,leader 的 LEO 决定了分区写入进度。

【问题】 Zookeeper 在 Kafka 中的作用是什么?

【答案】

  1. Broker 注册:在 Zookeeper 上会有一个专门用来进行 Broker 服务器列表记录的节点。每个 Broker 在启动时,都会到 Zookeeper 上进行注册,即到 /brokers/ids 下创建属于自己的节点。每个 Broker 就会将自己的 IP 地址和端口等信息记录到该节点中去。

  2. Topic 注册:在 Kafka 中,同一个 Topic 的消息会被分成多个分区并将其分布在多个 Broker 上,这些分区信息及与 Broker 的对应关系也都是由 Zookeeper 在维护。比如我创建了一个名字为 my-topic 的主题并且它有两个分区,对应到 Zookeeper 中会创建这些文件夹:/brokers/topics/my-topic/Partitions/0/brokers/topics/my-topic/Partitions/1

  3. 负载均衡:Kafka 通过给特定 Topic 指定多个 Partition,而各个 Partition 可以分布在不同的 Broker 上,这样便能提供比较好的并发能力。对于同一个 Topic 的不同 Partition,Kafka 会尽力将这些 Partition 分布到不同的 Broker 服务器上。当生产者产生消息后也会尽量投递到不同 Broker 的 Partition 里面。当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。

【大白话解释】 Zookeeper 在 Kafka 中就像小区物业管理系统:登记每个住户(Broker)的地址,记录每个信箱(Topic/Partition)放在哪个楼层,还帮分配快递员(Consumer)负责哪几个信箱。

【扩展知识点详解】

  1. Controller 选举:Kafka 集群中会有一个 Broker 被选为 Controller,负责分区 Leader 选举、副本分配等管理操作,Controller 的选举依赖 Zookeeper。
  2. Consumer Offset 管理:旧版本(0.9 之前)的 Kafka 将 Consumer 的消费偏移量存储在 Zookeeper 中,新版本改为存储在 Kafka 内部的 __consumer_offsets Topic 中,减轻了 Zookeeper 的压力。
  3. Kafka 去 Zookeeper 趋势:从 Kafka 2.8.0 开始引入 KRaft 模式(自研元数据管理),逐步摆脱对 Zookeeper 的依赖,Kafka 3.3+ 已支持生产环境无 Zookeeper 部署。

【问题】 Kafka 如何保证消息的消费顺序?

【答案】 Kafka 中 Partition(分区)是真正保存消息的地方,我们发送的消息都被放在了这里。而我们的 Partition(分区)又存在于 Topic(主题)这个概念中,并且我们可以给特定 Topic 指定多个 Partition。

每次添加消息到 Partition(分区)的时候都会采用尾加法。Kafka 只能为我们保证 Partition(分区)中的消息有序。

消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。

一种很简单的保证消息消费顺序的方法:1 个 Topic 只对应一个 Partition。这样当然可以解决问题,但是破坏了 Kafka 的设计初衷。

另一种是 Kafka 中发送 1 条消息的时候,可以指定 topic、partition、key、data(数据)4 个参数。如果你发送消息的时候指定了 Partition 的话,所有消息都会被发送到指定的 Partition。并且,同一个 key 的消息可以保证只发送到同一个 partition,这个我们可以采用表/对象的 id 来作为 key。

【大白话解释】

  • Kafka 保证顺序就像排队:每个窗口(Partition)里的人是按先来后到排好的,但不同窗口之间没有顺序关系。
  • 想要全局有序?只开一个窗口——但这样太慢了。
  • 实际做法:把同一个订单的消息都送到同一个窗口(用订单 ID 做 key),这样同一个订单的消息就是有序的。

【扩展知识点详解】

  1. 分区内有序,分区间无序:这是 Kafka 的基本设计,分区是并行度的基本单位,跨分区的顺序无法保证。
  2. Key 路由策略:Producer 发送消息时如果指定了 Key,Kafka 会对 Key 做 hash 运算,将消息路由到固定 Partition,从而保证同一 Key 的消息顺序。
  3. Consumer 单线程消费:即使同一 Partition 内消息有序,如果 Consumer 多线程消费,仍可能乱序。建议同一 Partition 使用单线程消费,或通过内存队列保证顺序。

【问题】 Kafka 如何保证消息不丢失?

【答案】 一、生产者丢失消息的情况

生产者(Producer)调用 send 方法发送消息之后,消息可能因为网络问题并没有发送过去。所以,我们不能默认在调用 send 方法发送消息之后消息发送成功了。为了确定消息是发送成功,我们要判断消息发送的结果。但是要注意的是 Kafka 生产者使用 send 方法发送消息实际上是异步的操作,我们可以通过 get() 方法获取调用结果,但是这样也让它变为了同步操作,一般不推荐这么做!可以采用为其添加回调函数的形式:

1
2
3
4
5
6
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, o);
future.addCallback(
    result -> logger.info("生产者成功发送消息到topic:{} partition:{}的消息",
        result.getRecordMetadata().topic(), result.getRecordMetadata().partition()),
    ex -> logger.error("生产者发送消息失败,原因:{}", ex.getMessage())
);

另外这里推荐为 Producer 的 retries(重试次数)设置一个比较合理的值,一般是 3,但是为了保证消息不丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网络波动一次你 3 次一下子就重试完了。

二、消费者丢失消息的情况

当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。

解决办法也比较粗暴,我们手动关闭自动提交 offset,每次在真正消费完消息之后再自己手动提交 offset。但是,细心的朋友一定会发现,这样会带来消息被重新消费的问题。比如你刚刚消费完消息之后,还没提交 offset,结果自己挂掉了,那么这个消息理论上就会被消费两次。

三、Kafka 弄丢了消息

假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选出一个 leader,但是 leader 的数据还有一些没有被 follower 副本同步的话,就会造成消息丢失。

  • 设置 acks = all:acks 是 Kafka 生产者很重要的一个参数。acks 的默认值即为 1,代表我们的消息被 leader 副本接收之后就算被成功发送。当我们配置 acks = all 代表则所有副本都要接收到该消息之后该消息才算真正成功被发送。
  • 设置 replication.factor >= 3:为了保证 leader 副本能有 follower 副本能同步消息,我们一般会为 topic 设置 replication.factor >= 3。这样就可以保证每个分区至少有 3 个副本。虽然造成了数据冗余,但是带来了数据的安全性。
  • 设置 min.insync.replicas > 1:这样配置代表消息至少要被写入到 2 个副本才算是被成功发送。min.insync.replicas 的默认值为 1,在实际生产中应尽量避免默认值 1。

但是,为了保证整个 Kafka 服务的高可用性,你需要确保 replication.factor > min.insync.replicas。为什么呢?设想一下假如两者相等的话,只要是有一个副本挂掉,整个分区就无法正常工作了。这明显违反高可用性!一般推荐设置成 replication.factor = min.insync.replicas + 1。

  • 设置 unclean.leader.election.enable = false:我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。多个 follower 副本之间的消息同步情况不一样,当我们配置了 unclean.leader.election.enable = false 的话,当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader,这样降低了消息丢失的可能性。

【大白话解释】 保证 Kafka 消息不丢失,就像保证快递安全送达:

  • 生产者端:寄件人要收到快递公司的签收回执(回调确认),寄丢了要重发(retries)。
  • 消费者端:收件人拆完包裹再确认签收(手动提交 offset),别包裹还没拆就点了“已签收”(自动提交)。
  • Broker 端:快递公司要保证每个包裹在多个仓库都有备份(多副本),而且要所有仓库都入库了才算完成(acks=all)。

【扩展知识点详解】

  1. acks 参数详解:acks=0(不等确认,最快但可能丢)、acks=1(只等 leader 确认,折中)、acks=all(等所有 ISR 确认,最安全但最慢)。
  2. enable.auto.commit:消费者端建议设为 false,手动提交 offset,但需在业务幂等性和消息不丢失之间做权衡。
  3. max.in.flight.requests.per.connection:设为 1 可保证重试时消息顺序,但会降低吞吐量;设为 5(默认)配合幂等 Producer 也可保证顺序。

【问题】 Kafka 如何保证消息不重复消费?

【答案】 Kafka 出现消息重复消费的原因:

  1. 服务端侧已经消费的数据没有成功提交 offset(根本原因)。
  2. Kafka 侧由于服务端处理业务时间长或者网络链接等原因让 Kafka 认为服务假死,触发了分区 rebalance。

解决方案:

  1. 消费消息服务做幂等校验,比如 Redis 的 set、MySQL 的主键等天然的幂等功能。这种方法最有效。
  2. 将 enable.auto.commit 参数设置为 false,关闭自动提交,开发者在代码中手动提交 offset。那么这里会有个问题:什么时候提交 offset 合适?
    • 处理完消息再提交:依旧有消息重复消费的风险,和自动提交一样。
    • 拉取到消息即提交:会有消息丢失的风险。允许消息延时的场景,一般会采用这种方式。然后,通过定时任务在业务不繁忙(比如凌晨)的时候做数据兜底。

【大白话解释】 重复消费就像快递员送了两次同一个包裹——可能是你签收了但系统没记录(offset 未提交),快递员以为没送过就又送了一次。解决办法就是:你自己记住哪些包裹已经收过了(幂等校验),第二次送来的直接拒收。

【扩展知识点详解】

  1. Kafka 幂等 Producer:从 0.11 版本开始,Kafka 支持 Producer 端幂等(设置 enable.idempotence=true),通过 Producer ID(PID)和 Sequence Number 保证单分区内的消息不会重复写入。
  2. Kafka 事务:Kafka 0.11+ 支持跨分区的事务(Exactly-Once 语义),通过 transactional.id 配置,可保证“消费-处理-生产”链路的 Exactly-Once。
  3. 业务层幂等设计:利用数据库唯一索引、Redis SETNX、状态机等方式,是最通用和可靠的防重复消费方案。

activeMQ

【问题】 ActiveMQ 与其他消息队列相比有什么特点?适用什么场景?

【答案】 ActiveMQ 是经典的开源消息中间件,支持 JMS 1.1 规范,具有以下特点:

  1. 协议支持丰富:支持 OpenWire、STOMP、AMQP、MQTT、WebSocket 等多种协议。
  2. 与 Java 生态集成好:完全支持 JMS 规范,与 Spring、Spring Boot 等无缝集成。
  3. 功能全面:支持持久化、事务、XA 事务、消息分组、虚拟主题等企业级特性。
  4. 性能相对较低:吞吐量一般在万级 QPS,不适合高并发场景。
  5. 社区更新缓慢:相比 Kafka、RocketMQ 等新兴中间件,社区活跃度较低。

适用场景:传统 Java 应用、低并发业务、需要 JMS 规范兼容的场景、多协议接入需求。

【大白话解释】 ActiveMQ 就像老牌国营邮局,什么业务都能办,服务标准齐全,但效率不如新兴快递公司。适合要求规范、并发量不大的老系统。

【扩展知识点详解】

  1. ActiveMQ 的高可用方案:主从复制(Shared Storage、Replicated LevelDB Store)、Network of Bridges 等。
  2. ActiveMQ vs ActiveMQ Artemis:Artemis 是 Red Hat 捐赠的新一代消息代理,支持 AMQP 1.0、MQTT 等,性能远超 ActiveMQ 5.x,已成为 ActiveMQ 的下一代默认 broker。
  3. ActiveMQ 的持久化方式:KahaDB(默认)、LevelDB、JDBC、内存存储等。

rocketMQ

【问题】 介绍下 RocketMQ?

【答案】 RocketMQ 是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式的特点。它是一个采用 Java 语言开发的分布式的消息系统。

队列模型:消息中间件的队列模型就真的只是一个队列;“广播”的概念,也就是说如果我们此时需要将一个消息发送给多个消费者(比如此时我需要将信息发送给短信系统和邮件系统),这个时候单个队列即不能满足需求了。

主题模型:在主题模型中,消息的生产者称为发布者(Publisher),消息的消费者称为订阅者(Subscriber),存放消息的容器称为主题(Topic)。其中,发布者将消息发送到指定主题中,订阅者需要提前订阅主题才能接受特定主题的消息。

【大白话解释】

  • 队列模型:就像一条单行道,一辆车(消息)只能被一个人(消费者)开走。
  • 主题模型:就像广播电台,一个频道(Topic)可以有很多听众(订阅者)同时收听。
  • RocketMQ 支持两种模型,适合各种业务场景,尤其是电商、金融等高可靠要求的场景。

【扩展知识点详解】

  1. RocketMQ 核心组件:NameServer(轻量级注册中心)、Broker(消息存储)、Producer(生产者)、Consumer(消费者)。
  2. RocketMQ 特有特性:事务消息、顺序消息、延迟消息、消息过滤(Tag/SQL92)、消息回溯等。
  3. RocketMQ vs Kafka:RocketMQ 更侧重事务、顺序消息、定时消息等企业级特性;Kafka 更侧重高吞吐、持久化日志存储。

zookeeper

【问题】 介绍下 ZooKeeper?

【答案】 ZooKeeper 是一个开源的分布式协调服务。它是一个为分布式应用提供一致性服务的软件,分布式应用程序可以基于 ZooKeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。

ZooKeeper 的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

ZooKeeper 保证了如下分布式一致性特性:

  1. 顺序一致性
  2. 原子性
  3. 单一视图
  4. 可靠性
  5. 实时性(最终一致性)

客户端的读请求可以被集群中的任意一台机器处理,如果读请求在节点上注册了监听器,这个监听器也是由所连接的 ZooKeeper 机器来处理。对于写请求,这些请求会同时发给其他 ZooKeeper 机器并且达成一致后,请求才会返回成功。因此,随着 ZooKeeper 的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。

有序性是 ZooKeeper 中非常重要的一个特性,所有的更新都是全局有序的,每个更新都有一个唯一的时间戳,这个时间戳称为 zxid(ZooKeeper Transaction Id)。而读请求只会相对于更新有序,也就是读请求的返回结果中会带有这个 ZooKeeper 最新的 zxid。

【大白话解释】 ZooKeeper 就像公司的行政秘书:帮各个部门(分布式服务)协调工作——谁当领导(Master 选举)、谁负责什么(负载均衡)、门牌号怎么分(命名服务)、会议室怎么锁(分布式锁)。

【扩展知识点详解】

  1. ZooKeeper 的典型应用场景:Dubbo/Kafka 的注册中心、Hadoop HA 的主备切换、HBase 的 Master 选举等。
  2. ZooKeeper vs Eureka vs Nacos:ZooKeeper 是 CP 模型(强一致性),Eureka 是 AP 模型(高可用),Nacos 支持 CP/AP 切换。
  3. Session 机制:客户端与 ZooKeeper 服务器之间通过心跳维持会话,会话超时则临时节点自动删除。

【问题】 ZooKeeper 提供了什么?文件系统是什么?

【答案】 ZooKeeper 提供了文件系统通知机制

ZooKeeper 提供一个多层级的节点命名空间(节点称为 znode)。与文件系统不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。

ZooKeeper 为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得 ZooKeeper 不能用于存放大量的数据,每个节点的存放数据上限为 1M。

【大白话解释】

  • 文件系统:ZooKeeper 就像一棵可挂标签的树,每个树权(节点)既能当文件夹又能当文件,上面还能贴便签(存数据)。
  • 通知机制:你可以在某个树权上挂个铃铛(Watch),一旦这个树权有变化,铃铛就响(通知你)。

【扩展知识点详解】

  1. Watch 机制:客户端在 znode 上注册 Watch,当 znode 数据变更、子节点变更或删除时,ZooKeeper 会向客户端发送事件通知。Watch 是一次性的,触发后需要重新注册。
  2. 数据量限制:每个 znode 默认最多存 1MB 数据,这是为了保持内存中的高性能,不适合存储大量业务数据。

【问题】 ZooKeeper 怎么保证主从节点的状态同步?

【答案】 ZooKeeper 的核心是原子广播机制,这个机制保证了各个 server 之间的同步。实现这个机制的协议叫做 Zab 协议。Zab 协议有两种模式,它们分别是恢复模式和广播模式。

恢复模式: 当服务启动或者在领导者崩溃后,Zab 就进入了恢复模式,当领导者被选举出来,且大多数 server 完成了和 leader 的状态同步以后,恢复模式就结束了。状态同步保证了 leader 和 server 具有相同的系统状态。

广播模式: 一旦 leader 已经和多数的 follower 进行了状态同步后,它就可以开始广播消息了,即进入广播状态。这时候当一个 server 加入 ZooKeeper 服务中,它会在恢复模式下启动,发现 leader,并和 leader 进行状态同步。待到同步结束,它也参与消息广播。ZooKeeper 服务一直维持在 Broadcast 状态,直到 leader 崩溃了或者 leader 失去了大部分的 followers 支持。

【大白话解释】

  • 恢复模式:就像公司换了新老板(leader),所有员工得先和老板对齐工作进度(状态同步),才能正常上班。
  • 广播模式:老板发通知,所有员工照着执行。新来的员工先补课(恢复模式),跟上进度后一起听通知。

【扩展知识点详解】

  1. Zab 协议 vs Paxos:Zab 是为 ZooKeeper 定制的崩溃可恢复原子广播协议,比 Paxos 更简化,专为主备模式设计。
  2. 过半机制:Zab 要求写请求获得超过半数 server 的确认才算成功,保证了在少数 server 故障时仍可正常工作。

【问题】 ZooKeeper 有哪几种数据节点?

【答案】

  1. PERSISTENT-持久节点:除非手动删除,否则节点一直存在于 ZooKeeper 上。
  2. EPHEMERAL-临时节点:临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与 ZooKeeper 连接断开不一定会话失效),那么这个客户端创建的所有临时节点都会被移除。
  3. PERSISTENT_SEQUENTIAL-持久顺序节点:基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。
  4. EPHEMERAL_SEQUENTIAL-临时顺序节点:基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。

【大白话解释】

  • 持久节点:像刻在石头上的字,不擦就一直存在。
  • 临时节点:像便签纸,你走了(会话结束)它就掉。
  • 顺序节点:像排队拿号,系统自动给你编个号,1、2、3……

【扩展知识点详解】

  1. 临时节点的典型应用:服务注册与发现(服务上线创建临时节点,宕机自动删除)、分布式锁(临时顺序节点实现公平锁)。
  2. 容器节点(Container Node):ZooKeeper 3.6+ 新增,当最后一个子节点被删除后,容器节点会被自动删除,适用于 Leader 选举等场景。
  3. TTL 节点:ZooKeeper 3.6+ 新增,持久节点可设置 TTL,超时后自动删除。

【问题】 ZooKeeper 是如何保证事务的顺序一致性的?

【答案】 ZooKeeper 采用了全局递增的事务 Id 来标识,所有的 proposal(提议)都在被提出的时候加上了 zxid,zxid 实际上是一个 64 位的数字,高 32 位是 epoch(时期/纪元)用来标识 leader 周期,如果有新的 leader 产生出来,epoch 会自增,低 32 位用来递增计数。当新产生 proposal 的时候,会依据数据库的两阶段过程,首先会向其他的 server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。

【大白话解释】 ZooKeeper 给每个操作都贴上一个带版本号的时间戳(zxid),就像快递单号,高32位是年份(leader 轮次),低32位是当天第几个件。这样所有操作都有顺序,谁先谁后一目了然。

【扩展知识点详解】

  1. zxid 的构成:高 32 位是 epoch(leader 任期),低 32 位是 counter(事务计数器)。新 leader 选举后 epoch+1,counter 重置,保证全局递增。
  2. 两阶段提交过程:leader 收到写请求后,先向所有 follower 发送 proposal,过半 follower 确认后,leader 发送 commit 消息,保证事务的原子性和顺序性。

【问题】 ZooKeeper 的选举机制是怎样的?

【答案】 当 leader 崩溃或者 leader 失去大多数的 follower,这时 ZK 进入恢复模式,恢复模式需要重新选举出一个新的 leader,让所有的 Server 都恢复到一个正确的状态。

ZK 的选举算法有两种:一种是基于 basic paxos 实现的,另外一种是基于 fast paxos 算法实现的。系统默认的选举算法为 fast paxos。

ZooKeeper 选主流程(basic paxos)

  1. 选举线程由当前 Server 发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的 Server。
  2. 选举线程首先向所有 Server 发起一次询问(包括自己)。
  3. 选举线程收到回复后,验证是否是自己发起的询问(验证 zxid 是否一致),然后获取对方的 id(myid),并存储到当前询问对象列表中,最后获取对方提议的 leader 相关信息(id, zxid),并将这些信息存储到当次选举的投票记录表中。
  4. 收到所有 Server 回复以后,就计算出 zxid 最大的那个 Server,并将这个 Server 相关信息设置成下一次要投票的 Server。
  5. 线程将当前 zxid 最大的 Server 设置为当前 Server 要推荐的 Leader,如果此时获胜的 Server 获得 n/2 + 1 的 Server 票数,设置当前推荐的 leader 为获胜的 Server,将根据获胜的 Server 相关信息设置自己的状态,否则,继续这个过程,直到 leader 被选举出来。

通过流程分析我们可以得出:要使 Leader 获得多数 Server 的支持,则 Server 总数必须是奇数 2n+1,且存活的 Server 的数目不得少于 n+1。每个 Server 启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的 server 还会从磁盘快照中恢复数据和会话信息,ZK 会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。

【大白话解释】 选举 leader 就像班级选班长:每个同学(Server)先投票给成绩最好(zxid 最大)的同学,然后统计票数。谁获得超过半数的票,谁就是班长。为了保证大多数同意,班级人数最好是奇数。

【扩展知识点详解】

  1. 选举比较规则:先比较 epoch(leader 轮次),大的优先;再比较 zxid(事务 ID),大的优先;最后比较 myid(服务器 ID),大的优先。
  2. fast paxos:相比 basic paxos,fast paxos 在选举过程中引入了“推选”机制,能更快地收敛到一致结果,减少选举轮次。
  3. 选举期间服务不可用:ZooKeeper 在选举过程中无法处理写请求,读请求可能仍可处理(取决于配置),这是 ZK 不适合高频写入场景的原因之一。

dubbo

【问题】 Dubbo 的分层是什么?

【答案】 从大的范围来说,Dubbo 分为三层,business 业务逻辑层由我们自己来提供接口和实现还有一些配置信息,RPC 层就是真正的 RPC 调用的核心层,封装整个 RPC 的调用过程、负载均衡、集群容错、代理,remoting 则是对网络传输协议和数据转换的封装。

划分到更细的层面,就是图中的 10 层模式,整个分层依赖由上至下,除开 business 业务逻辑之外,其他的几层都是 SPI 机制。

【大白话解释】 Dubbo 就像一个公司:业务层是你自己干的活,RPC 层是公司内部流程(怎么分工、怎么容错),remoting 层是快递和电话(怎么传数据)。越往下越底层,越通用。

【扩展知识点详解】

  1. Dubbo 的 10 层架构:Service(接口层)、Config(配置层)、Proxy(代理层)、Registry(注册中心层)、Cluster(路由层)、Monitor(监控层)、Protocol(协议层)、Exchange(信息交换层)、Transport(网络传输层)、Serialize(序列化层)。
  2. SPI 机制:Dubbo SPI 是对 Java SPI 的增强,支持按需加载、自动注入、自适应扩展等,是 Dubbo 灵活性的核心。

【问题】 Dubbo 的工作原理是什么?工作流程是什么?

【答案】

  1. 服务启动的时候,provider 和 consumer 根据配置信息,连接到注册中心 register 注册自己,包括自己的地址(ip+port),分别向注册中心注册和订阅服务。
  2. register 根据服务订阅关系,返回 provider 列表信息到 consumer,同时 consumer 会把 provider 信息缓存到本地。如果信息有变更,consumer 会收到来自 register 的推送。
  3. consumer 生成代理对象,通过动态代理来拦截方法的执行,同时根据配置选择负载均衡策略,通讯协议策略,请求封装序列化,网络通讯框架策略;选择一台 provider,同时定时向 monitor 记录接口的调用次数和时间信息。
  4. 拿到代理对象之后,consumer 通过代理对象发起接口调用。
  5. provider 收到请求后对数据进行反序列化,然后通过代理调用具体的接口实现。

【大白话解释】

  • 注册:就像餐厅开业先去工商局登记(provider 注册),顾客先去工商局查哪家餐厅开门(consumer 订阅)。
  • 调用:顾客(consumer)不去工商局,而是直接去餐厅(provider)吃饭,中间有个代购(代理对象)帮你点菜。
  • 监控:每次吃饭的记录都会报给市场监管局(monitor)。

【扩展知识点详解】

  1. 注册中心:Dubbo 支持 ZooKeeper、Nacos、Redis、Simple 等多种注册中心,推荐使用 ZooKeeper 或 Nacos。
  2. 本地缓存:consumer 会将 provider 列表缓存到本地,即使注册中心宕机,consumer 仍可通过本地缓存调用 provider。
  3. 监控中心:Monitor 只负责统计,不影响主流程,宕机不影响 RPC 调用。

【问题】 为什么要通过代理对象通信?

【答案】 主要是为了实现接口的透明代理,封装调用细节,让用户可以像调用本地方法一样调用远程方法,同时还可以通过代理实现一些其他的策略,比如:

  1. 调用的负载均衡策略
  2. 调用失败、超时、降级和容错机制
  3. 做一些过滤操作,比如加入缓存、mock 数据
  4. 接口调用数据统计

【大白话解释】 代理对象就像代购:你只想买个包(调用方法),代购帮你处理找店、比价、砍价、物流(负载均衡、容错、监控)等所有细节,你只需要说“我要这个包”,就像在自己家楼下买一样方便。

【扩展知识点详解】

  1. JDK 动态代理 vs Javassist 字节码增强:Dubbo 默认使用 Javassist 生成代理类,性能优于 JDK 动态代理。也可通过 SPI 切换为 JDK 动态代理。
  2. Invoker 抽象:Dubbo 的核心抽象是 Invoker,代理对象本质上是 Invoker 的包装,Invoker 封装了远程调用的全部细节。

【问题】 讲一下 Dubbo 的底层网络通讯机制原理?

【答案】 Dubbo 底层网络通信框架是 Netty NIO,核心优势是:

  • 异步非阻塞:通过事件驱动模型处理高并发请求。
  • 高性能:零拷贝、内存池优化减少 GC 压力。
  • 可扩展性:ChannelHandler 链式处理支持自定义协议。

Netty 的线程模型是主从 Reactor 多线程模型

  • BossGroup(主 Reactor):线程数通常为 1;职责:监听并接收客户端连接,将新连接注册到 WorkerGroup。
  • WorkerGroup(从 Reactor):线程数默认 CPU 核数 * 2;职责:处理 I/O 读写(Channel.read())、编解码等网络操作。
  • 业务线程池(Dubbo Dispatcher):职责:执行 Dubbo 服务接口的业务逻辑,避免阻塞 I/O 线程。

事件处理流程是:

  1. 连接建立:BossGroup 处理 OP_ACCEPT 事件,分配 Channel 到 WorkerGroup。
  2. I/O 就绪:WorkerGroup 处理 OP_READ/OP_WRITE 事件。
  3. 协议解析:解码器(如 LengthFieldBasedFrameDecoder)处理粘包/拆包。
  4. 业务逻辑:将请求派发至 Dubbo 业务线程池执行。
  5. 响应返回:业务线程处理完成后,通过 I/O 线程写回响应。

【大白话解释】

  • BossGroup:前台接待,只负责接客(接收连接)。
  • WorkerGroup:服务员,负责端菜送水(读写数据)。
  • 业务线程池:后厨,专门做菜(执行业务逻辑),不让服务员去炒菜(避免阻塞 I/O 线程)。

【扩展知识点详解】

  1. Dubbo 支持的通信框架:Netty(默认)、Mina、Grizzly 等,通过 SPI 可切换。
  2. Dubbo 支持的协议:Dubbo 协议(默认,基于 TCP 长连接)、HTTP、Hessian、RMI、WebSocket 等。
  3. Dispatcher 派发策略:all(所有消息派发到业务线程池)、direct(直接在 I/O 线程执行)、message(只有请求消息派发)、execution(只有请求消息派发,响应在 I/O 线程)等。

【问题】 阐述一下服务暴露的流程?

【答案】

  1. 在容器启动的时候,通过 ServiceConfig 解析标签,创建 Dubbo 标签解析器来解析 Dubbo 的标签,容器创建完成之后,触发 ContextRefreshEvent 事件回调开始暴露服务。
  2. 通过 ProxyFactory 获取到 invoker,invoker 包含了需要执行的方法的对象信息和具体的 URL 地址。
  3. 再通过 DubboProtocol 的实现把包装后的 invoker 转换成 exporter,然后启动服务器 server,监听端口。
  4. 最后 RegistryProtocol 保存 URL 地址和 invoker 的映射关系,同时注册到服务中心。

【大白话解释】 服务暴露就像餐厅开张:先装修好厨房(ServiceConfig 解析),再招厨师(创建 Invoker),然后开门营业(启动 Server 监听端口),最后去工商局注册(注册到注册中心)。

【扩展知识点详解】

  1. export vs refer:export 是服务提供者暴露服务,refer 是服务消费者引用服务,两者是对称的设计。
  2. 延迟暴露:Dubbo 支持延迟暴露(delay 参数),等 Spring 容器完全初始化后再暴露服务,避免服务未准备好就被调用。
  3. 动态注册与注销:服务运行期间可动态注册和注销,支持优雅停机。

【问题】 阐述一下服务引用的流程?

【答案】 服务暴露之后,客户端就要引用服务,然后才是调用的过程。

  1. 首先客户端根据配置文件信息从注册中心订阅服务。
  2. 之后 DubboProtocol 根据订阅得到的 provider 地址和接口信息连接到服务端 server,开启客户端 client,然后创建 invoker。
  3. invoker 创建完成之后,通过 invoker 为服务接口生成代理对象,这个代理对象用于远程调用 provider,服务的引用就完成了。

【大白话解释】 服务引用就像顾客找餐厅:先查工商局(订阅注册中心),拿到餐厅地址后去餐厅(建立连接),然后找个代购帮你点菜(生成代理对象)。

【扩展知识点详解】

  1. 本地引用 vs 远程引用:Dubbo 支持 injvm 本地调用(同一 JVM 内)和远程调用,通过 scope 参数控制。
  2. sticky 粘性连接:设置 sticky=true 后,consumer 会尽量使用同一个 provider,适用于有状态服务。
  3. 懒连接:lazy=true 时,consumer 不会在启动时立即建立连接,而是在第一次调用时才连接。

【问题】 负载均衡策略有哪些?

【答案】

  1. 加权随机:假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为 10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上就可以了。

  2. 最小活跃数:每个服务提供者对应一个活跃数 active,初始情况下,所有服务提供者活跃数均为 0。每收到一个请求,活跃数加 1,完成请求后则将活跃数减 1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求。

  3. 一致性 Hash:通过 hash 算法,把 provider 的 invoke 和随机节点生成 hash,并将这个 hash 投射到 [0, 2^32 - 1] 的圆环上,查询的时候根据 key 进行 md5 然后进行 hash,得到第一个节点的值大于等于当前 hash 的 invoker。

  4. 加权轮询:比如服务器 A、B、C 权重比为 5:2:1,那么在 8 次请求中,服务器 A 将收到其中的 5 次请求,服务器 B 会收到其中的 2 次请求,服务器 C 则收到其中的 1 次请求。

【大白话解释】

  • 加权随机:像抽类,权重大的区间长,抽中的概率大。
  • 最小活跃数:像看病挂与,谁排队少就挂谁号。
  • 一致性 Hash:像固定座位,每个人根据学号 hash 到固定座位,老师换座位也不受影响。
  • 加权轮询:像轮流值日,按权重比轮流干活。

【扩展知识点详解】

  1. ShortestResponse:Dubbo 2.7+ 新增策略,优先调用最近一次响应时间最短的 provider,适合对延迟敏感的场景。
  2. 一致性 Hash 虚拟节点:为避免数据倾斜,一致性 Hash 引入虚拟节点,每个真实节点对应多个虚拟节点,使分布更均匀。
  3. 配置方式:可在 provider 端和 consumer 端分别配置,consumer 端优先级更高。

【问题】 集群容错方式有哪些?

【答案】

  1. Failover Cluster 失败自动切换:Dubbo 的默认容错方案,当调用失败时自动切换到其他可用的节点,具体的重试次数和间隔时间可以通过引用服务的时候配置,默认重试次数为 1 也就是只调用一次。

  2. Failback Cluster 失败自动恢复:在调用失败,记录日志和调用信息,然后返回空结果给 consumer,并且通过定时任务每隔 5 秒对失败的调用进行重试。

  3. Failfast Cluster 快速失败:只会调用一次,失败后立刻抛出异常。

  4. Failsafe Cluster 失败安全:调用出现异常,记录日志不抛出,返回空结果。

  5. Forking Cluster 并行调用多个服务提供者:通过线程池创建多个线程,并发调用多个 provider,结果保存到阻塞队列,只要有一个 provider 成功返回了结果,就会立刻返回结果。

  6. Broadcast Cluster 广播模式:逐个调用每个 provider,如果其中一台报错,在循环调用结束后,抛出异常。

【大白话解释】

  • Failover:打不通就换号重打(适合读操作)。
  • Failback:打不通就先记下来,回头再打(适合实时性要求不高但最终要成功的操作)。
  • Failfast:打不通就挂电话(适合非幂等写操作)。
  • Failsafe:打不通就算了(适合日志、监控等可丢弃的操作)。
  • Forking:同时打多个电话,谁先接听就用谁的答案(适合实时性要求高的读操作)。
  • Broadcast:给所有人都打电话通知(适合缓存更新等广播场景)。

【扩展知识点详解】

  1. Mergeable Cluster:Dubbo 还支持合并调用结果(group 合并),适合聚合多个服务提供者的结果。
  2. 容错策略选择建议:读操作推荐 Failover(重试切换);写操作推荐 Failfast(避免重复);日志等可容忍失败的操作推荐 Failsafe。

【问题】 自定义一个 RPC 框架怎样设计?

【答案】

  1. 首先需要一个服务注册中心,这样 consumer 和 provider 才能去注册和订阅服务。
  2. 需要负载均衡的机制来决定 consumer 如何调用客户端,这其中当然还要包含容错和重试的机制。
  3. 需要通信协议和工具框架,比如通过 HTTP 或者 RMI 的协议通信,然后再根据协议选择使用什么框架和工具来进行通信,当然,数据的传输序列化要考虑。
  4. 除了基本的要素之外,像一些监控、配置管理页面、日志是额外的优化考虑因素。

【大白话解释】 自己造一个 RPC 框架,就像开一家快递公司:要有客户管理系统(注册中心)、分单系统(负载均衡)、运输车队和包装标准(通信协议+序列化),再配个客服中心(监控)就更完善了。

【扩展知识点详解】

  1. 序列化方式选择:JSON(可读性好,性能一般)、Hessian2(Dubbo 默认,性能和兼容性平衡)、Protobuf(高性能,需要 IDL 定义)、Kryo(高性能,不支持跨语言)。
  2. 动态代理:RPC 框架的核心,让远程调用像本地调用一样透明。常用 JDK 动态代理、CGLIB、Javassist 字节码生成。
  3. 参考实现:Dubbo、gRPC、Spring Cloud OpenFeign 都是优秀的 RPC 框架参考。

ES

【问题】 什么是 ELK?

【答案】

  • E:Elasticsearch,分布式搜索和分析引擎。
  • L:Logstash,Elastic Stack 的核心产品之一,可用来对数据进行聚合和处理,并将数据发送到 Elasticsearch。Logstash 是一个开源的服务器端数据处理管道,允许您在将数据索引到 Elasticsearch 之前同时从多个来源采集数据,并对数据进行充实和转换。
  • K:Kibana,一款适用于 Elasticsearch 的数据可视化和管理工具,可以提供实时的直方图、线性图等。

【大白话解释】 ELK 就像一个情报分析系统:Elasticsearch 是情报库(存储+搜索),Logstash 是情报收集员(采集+清洗数据),Kibana 是情报展示大屏(可视化)。

【扩展知识点详解】

  1. Elastic Stack:ELK 后来加入了 Beats(轻量级数据采集器),更名为 Elastic Stack。Beats 负责在各个节点上采集数据,发送给 Logstash 或 Elasticsearch。
  2. 常见架构:Beats → Logstash → Elasticsearch → Kibana,其中 Logstash 可选,Beats 也可直接写入 Elasticsearch。

【问题】 ES 索引是什么?

【答案】 Elasticsearch 索引指相互关联的文档集合。Elasticsearch 会以 JSON 文档的形式存储数据。每个文档都会在一组键(字段或属性的名称)和它们对应的值(字符串、数字、布尔值、日期、数值组、地理位置或其他类型的数据)之间建立联系。

Elasticsearch 使用的是一种名为倒排索引的数据结构,这一结构的设计可以允许十分快速地进行全文本搜索。倒排索引会列出在所有文档中出现的每个特有词汇,并且可以找到包含每个词汇的全部文档。

在索引过程中,Elasticsearch 会存储文档并构建倒排索引,这样用户便可以近实时地对文档数据进行搜索。索引过程是在索引 API 中启动的,通过此 API 您既可向特定索引中添加 JSON 文档,也可更改特定索引中的 JSON 文档。

【大白话解释】 ES 的索引就像图书的目录:普通目录是从页码找内容(正排索引),ES 的倒排索引是从关键词找页码——比如查“Java”,目录告诉你第 3、7、15 页都有这个词。

【扩展知识点详解】

  1. 索引 vs 数据库类比:ES 索引 ≈ 数据库(Database),Type ≈ 表(Table)(ES 7.x 已废弃 Type),Document ≈ 行(Row),Field ≈ 列(Column)。
  2. Mapping:定义索引中字段的类型、分词器、是否索引等属性,类似于数据库的 Schema。
  3. 索引模板(Index Template):预定义索引的 Mapping、Setting 等配置,新索引创建时自动匹配模板,减少重复配置。

【问题】 什么是分词?倒排索引的原理是什么?

【答案】 分词是将输入的文本数据切分成一个个独立的词语或词项的过程。Elasticsearch 使用分词器(Analyzer)来处理文本数据,以便更好地进行索引和搜索。

分词器可以切分文本、去除停顿词、小写化、词干提取。 分词器有标准分词器、中文分词器、自定义分词器。

倒排索引是 Elasticsearch 中用于快速检索文档的主要数据结构。它的工作原理可以简单理解为将文档中的词项映射到包含该词项的文档列表。

倒排索引的构建过程:

  1. 文档分词处理
  2. 构建索引,为每个词创建一个列表,记录包含该词项的所有文档 ID,例如:

文档 1:内容 “Elasticsearch 是一个搜索引擎” 文档 2:内容 “搜索引擎的工作原理” 文档 3:内容 “Elasticsearch 提供了强大的搜索功能”

经过分词后,倒排索引可能是:

  • “Elasticsearch” -> [1, 3]
  • “是” -> [1]
  • “一个” -> [1]
  • “搜索” -> [1, 2]
  • “引擎” -> [1, 2]
  • “提供” -> [3]
  • “了” -> [3]
  • “强大” -> [3]
  • “的” -> [2]
  • “工作” -> [2]
  • “原理” -> [2]

查询过程: 当用户发起搜索请求时,Elasticsearch 会首先对查询的文本进行分词,然后它会查找倒排索引,找到包含查询词项的所有文档 ID,最后系统会根据相关性评分等因素返回匹配的文档。

【大白话解释】

  • 分词:就像把一句话拆成单个词,方便查找。比如“我喜欢编程”拆成“我”“喜欢”“编程”。
  • 倒排索引:就像书的关键词索引——普通目录是按页码找内容,倒排索引是按关键词找页码。查“搜索”,索引告诉你第 1、2 页都有。

【扩展知识点详解】

  1. Analyzer 组成:Character Filters(字符过滤)→ Tokenizer(分词)→ Token Filters(词项过滤)。
  2. 常用中文分词器:IK Analyzer(最常用)、jieba、HanLP 等,ES 默认的标准分词器对中文只做单字切分,效果不好。
  3. 倒排索引的优化:使用 FST(Finite State Transducer)压缩存储词项字典,使用 Roaring Bitmap 压缩文档 ID 列表,大幅减少内存占用。

【问题】 什么是 ES 的分段存储思想?什么是段合并策略?

【答案】 Lucene 是著名的搜索开源软件,ElasticSearch 和 Solr 底层用的都是它。

分段存储是 Lucene 的思想。文档有个很小的改动,整个索引需要重新建立,速度慢、成本高;为了提高速度,定期更新那么时效性就差。

现在一个索引文件,拆分为多个子文件,每个子文件是段。修改的数据不影响的段不必做处理。每次新增数据就会新增加一个段,时间久了,一个文档对应的段非常多。段多了,也就影响检索性能了。

检索过程:

  1. 查询所有段中满足条件的数据
  2. 对每个段的结果集合并

所以,定期的对段进行合并是很必要的。

段合并策略:将段按大小排列分组,大到一定程度的不参与合并,小的组内合并,整体维持在一个合理的大小范围。

【大白话解释】

  • 分段存储:就像活页本,每次新增内容就加一页(段),不用每次改动都重写整个本子。但活页太多翻起来慢。
  • 段合并:定期把散页整理装订成厚一点的章节,方便翻阅。

【扩展知识点详解】

  1. Segment 的不可变性:一旦写入磁盘就不可修改,这使得并发读取无需加锁,但删除操作是标记删除(.del 文件),真正删除要等段合并。
  2. Refresh 与 Flush:Refresh 将内存 Buffer 写入新 Segment(可搜索),Flush 将 Segment 持久化到磁盘并提交事务。
  3. Force Merge:可通过 API 手动触发段合并,通常在索引不再更新时调用(如日志索引每天合并一次)。

【问题】 什么是文本相似度 TF-IDF?

【答案】 TF-IDF = TF / IDF

简单地说,就是你检索一个词,匹配出来的文章、网页太多了,比如 1000 个,这些内容再该怎么呈现,哪些在前面哪些在后面,这就需要有一个对匹配度的评分。

  • TF = Term Frequency 词频:一个词在这个文档中出现的频率。值越大,说明这文档越匹配,正向指标。
  • IDF = Inverse Document Frequency 反向文档频率:简单点说就是一个词在所有文档中都出现,那么这个词不重要。比如“的、了、我、好”这些词所有文档都出现,对检索毫无帮助,反向指标。

【大白话解释】

  • TF:就像点名率,一个词在班里被点到次数越多,说明这个词跟这个班越相关。
  • IDF:就像辨识度,“的”“了”这种词所有班都有,区分不了谁是谁,辨识度低。而“量子力学”只在物理班出现,辨识度高。
  • TF-IDF:点名率高且辨识度高的词,才是真正能区分文档的关键词。

【扩展知识点详解】

  1. BM25:ES 5.0+ 默认使用 BM25 替代 TF-IDF,BM25 对词频做了饱和处理(词频无限增大时评分趋于平稳),对短文档更友好。
  2. TF-IDF 的局限:没有考虑文档长度、词频饱和等问题,BM25 是改进版本。

【问题】 讲一下 ES 写索引逻辑

【答案】 集群 = 主分片 + 副本分片

写索引只能写主分片,然后主分片同步到副本分片上。但主分片不是固定的,可能网络原因,之前还是 Node1 是主分片,后来就变成了 Node2 经过选举成了主分片了。

客户端如何知道哪个是主分片呢?看下面过程:

  1. 客户端向某个节点 NodeX 发送写请求
  2. NodeX 通过文档信息,请求会转发到主分片的节点上
  3. 主分片处理完,通知到副本分片同步数据,向 NodeX 发送成功信息
  4. NodeX 将处理结果返回给客户端

【大白话解释】 写索引就像公司审批流程:只能找主管(主分片)签字,主管签完后把复印件给副主管(副本分片),副主管确认收到后,主管才告诉提交人“搞定了”。

【扩展知识点详解】

  1. 一致性级别:consistency=one(主分片写入即成功)、quorum(大多数分片写入成功)、all(所有分片写入成功)。默认 quorum。
  2. Refresh 间隔:默认 1 秒 Refresh 一次,写入后最多等 1 秒才能搜索到。可调整 refresh_interval 或手动调用 Refresh API。
  3. Bulk 批量写入:ES 推荐使用 Bulk API 批量写入,减少网络开销,提高吞吐量。

【问题】 在集群中搜索数据的过程?

【答案】

  1. 客户端向集群发送请求,集群随机选择一个 NodeX 处理这次请求。
  2. NodeX 先计算文档在哪个主分片上,比如是主分片 A,它有三个副本 A1、A2、A3。那么请求会轮询三个副本中的一个完成请求。
  3. 如果无法确认分片,比如检索的不是一个文档,就遍历所有分片。

一个节点的存储量是有限的,于是有了分片的概念。但是分片可能有丢失,于是有了副本的概念。

比如:ES 集群有 3 个分片,分片 A、分片 B、分片 C,那么分片 A + 分片 B + 分片 C = 所有数据,每个分片只有大概 1/3。分片 A 又有副本 A1 A2 A3,数据都是一样的。

【大白话解释】 搜索数据就像图书馆找书:前台(NodeX)帮你查书在哪个书架(分片),然后去对应书架取。同一本书有多个副本(副本分片),前台会轮流去不同的副本取,避免一个书架被挤爆。

【扩展知识点详解】

  1. Query Then Fetch:ES 搜索默认分两阶段——Query 阶段(各分片返回匹配文档 ID 和评分)和 Fetch 阶段(根据 ID 去对应分片取完整文档)。
  2. DFS Query Then Fetch:更精确的评分模式,先统计词频再计算评分,但开销更大,一般不推荐。
  3. 路由(Routing):自定义路由键可将相关文档分配到同一分片,查询时指定路由键可避免广播到所有分片,提升性能。

【问题】 讲一下深翻页问题和解决方案?

【答案】 深翻页(Deep Pagination)问题指的是在执行分页查询时,当页数较大时,性能会显著下降。这是因为 Elasticsearch 在处理深层分页时需要遍历大量的文档,导致查询速度变慢,资源消耗增加。

  • 性能开销:当请求较高页码(例如第 1000 页)时,Elasticsearch 需要从索引中读取并跳过之前的文档。这意味着它必须扫描和排序所有之前的文档,直到到达请求的页码。
  • 内存消耗:大量的文档需要被加载到内存中以进行排序和跳过,这可能导致内存使用过高,影响集群的整体性能。
  • 响应时间:随着页码的增加,查询的响应时间可能会显著延长,给用户体验带来负面影响。

解决方案:

  1. Search After:使用 search_after 参数进行基于游标的分页。用户需要在每次请求中提供上一页最后一个文档的排序值,而不是直接请求页码。避免了深层分页的性能开销,适用于需要频繁翻页的场景。

  2. 滚动搜索(Scroll API):使用 Scroll API 进行大数据集的遍历,适合需要处理大量数据的场景。提供了一种高效的方式来获取大量文档,而不会受到深翻页的性能影响。

【大白话解释】

  • 深翻页问题:就像翻一本厚字典,你要翻到第 1000 页,得先翻过前 999 页,越往后越慢越累。
  • Search After:每次记住当前页的书签,下次直接从书签位置继续翻,不用从头来。
  • Scroll:像一次性打包,把所有结果装进一个大箱子里,你慢慢取就行。

【扩展知识点详解】

  1. ES 默认限制index.max_result_window 默认 10000,超过此值的 from+size 查询会报错。不建议调大此值。
  2. Search After 适用场景:实时分页查询(如下拉加载更多),不适合跳页场景。
  3. Scroll 适用场景:数据导出、批量处理等离线场景,不适合实时查询。ES 7.10+ 推荐使用 Point in Time (PIT) + Search After 替代 Scroll。
  4. Pit + Search After:ES 7.10 引入的新方案,结合 PIT(时间点快照)保证搜索结果一致性,是官方推荐的深翻页方案。

【问题】 ES 查询优化方式有哪些?

【答案】 设计阶段调优

  1. 根据业务增量需求,采取基于日期模板创建索引,通过 roll over API 滚动索引;举例:设计阶段定义 blog 索引的模板格式为 blog_index_时间戳的形式,每天递增数据。
  2. 使用别名进行索引管理。
  3. 每天凌晨定时对索引做 force_merge 操作,以释放空间。
  4. 采取冷热分离机制,热数据存储到 SSD,提高检索效率;冷数据定期进行 shrink 操作,以缩减存储。
  5. 采取 curator 进行索引的生命周期管理。
  6. 仅针对需要分词的字段,合理地设置分词器。
  7. Mapping 阶段充分结合各个字段的属性,是否需要检索、是否需要存储等。

写入调优

  1. 写入前副本数设置为 0。
  2. 写入前关闭 refresh_interval 设置为 -1,禁用刷新机制。
  3. 写入过程中:采取 bulk 批量写入。
  4. 写入后恢复副本数和刷新间隔。
  5. 尽量使用自动生成的 id。

查询调优

  1. 禁用 wildcard。
  2. 禁用批量 terms(成百上千的场景)。
  3. 充分利用倒排索引机制,能 keyword 类型尽量 keyword。
  4. 数据量大时候,可以先基于时间敲定索引再检索。
  5. 设置合理的路由机制。

【大白话解释】

  • 设计调优:像盖房子,先规划好户型(Mapping)、冷热分区(SSD vs HDD),别等住进去了再拆墙。
  • 写入调优:像搬家,先把贵重物品收起来(副本设0),赶紧搬完再摆回来。
  • 查询调优:像找东西,别全屋翻(禁用 wildcard),先确定在哪个房间(路由/时间),用索引标签找(keyword)。

【扩展知识点详解】

  1. Index Lifecycle Management (ILM):ES 6.6+ 内置索引生命周期管理,可自动执行 rollover、shrink、force_merge、delete 等操作。
  2. doc_values vs fielddata:聚合排序默认使用 doc_values(磁盘),比 fielddata(内存)更省内存。text 字段默认关闭 doc_values,需排序聚合时应使用 keyword 子字段。
  3. filter vs query:filter 不计算评分,可利用查询缓存,性能优于 query;不需要评分的场景优先用 filter。

【问题】 ES 如何实现 Master 选举的?

【答案】

  1. 只有候选主节点(master: true)的节点才能成为主节点。
  2. 最小主节点数(min_master_nodes)的目的是防止脑裂。

第一步:确认候选主节点数达标。 第二步:比较——先判定是否具备 master 资格,具备候选主节点资格的优先返回;若两节点都为候选主节点,则 id 小的值为主节点。注意这里的 id 为 string 类型。

【大白话解释】 ES 选举 Master 就像班级选班长:只有报名参选的同学(候选主节点)才能被选;投票时,先看谁更“资历老”(master 资格),资历一样就看学号(id),学号小的当选。而且必须有超过半数的人在场才能选(防止脑裂)。

【扩展知识点详解】

  1. 脑裂问题:网络分区导致集群出现两个 Master,数据不一致。min_master_nodes 设置为 N/2+1 可防止脑裂。
  2. ES 7.x 选举改进:ES 7.x 引入基于 Quorum 的选举机制,不再依赖 Zen Discovery 的 ping 机制,选举更稳定、更快速。
  3. 候选主节点 vs 数据节点:生产环境建议分离 Master 节点(专责集群管理)和数据节点(专责存储和检索),避免数据节点负载影响 Master 选举。
本文由作者按照 CC BY 4.0 进行授权