← 学习门户 每日面试题 学习看板

2026-10-14

实践场景 & 问题排查 —— 从单点现象到系统排查到架构设计

10 题 · 平均 4/5 · low 3.33 · medium 4.25 · high 4.33 · 第 1 轮

#1 应用启动后前几分钟,Load、RT、CPU等飙高,如何定位,可能的原因是什么? low practice

题目

应用启动后前几分钟,Load、RT、CPU等飙高,如何定位,可能的原因是什么?

我的回答

首先看一下监控,确认这些标高的时间节点是什么情况?是启动后就开始逐渐的梯形的,就阶梯形的逐渐升高,还是怎么样?确认出现问题跟发布的根本关系,然后检查发布的内容是否有什么特殊的内容,是否会导致这个情况。 应用,如果是 Java 应用的话,看一下对应的 Java 的看板,包括 ZAL 加载类的情况、类加载器,然后内存的使用情况等一系列的具体的参数,相关的指标,GC,然后是否有大量的 GC,看一下 GC 的指标,是否有高并发进入在这个时间节点,然后类加载的参数的情况、类加载的数量的情况,是否有什么特殊的波动。 看一下请求接口方面,具体是所有接口都出现了问题,还是某些接口出现了问题。这个接口接口的监控,看一下是否是有个别接口并发量突增。如果是个别接口的话,检查一下这对应接口情况。 根据绩效指标、接口监控,磁盘的情况也要看一下磁盘跟机器的情况,有可能意外的日志导致磁盘方面满了,都是有可能的。然后观察一下磁盘的比例是否有相关的告警,磁盘是否拉满,这些都有可能产生 load、RT、CPU 标高。 检查到具体原因了,根据上面的指标看到了具体原因,然后开始进行相应的处理。如果是因为上线新功能导致的问题,那么对紧急对新功能进行回滚,然后把环境保留,把日志相关的下下来,然后看新功能具体是什么原因,然后回滚确保线上没有问题,正常可以使用。对新功能进行更改排查,具体原因是什么? 如果不是新功能导致的,而是突然的并发流量导致的。那么检查这个流量的持续情况,来源是否正常,是否是攻击,还是某些大促的活动导致的。 如果是攻击,对应攻击做相应的处理。比如 IP 过滤,然后就用户过滤、账号过滤、区域过滤等等,把非正常的请求拦截掉,防止服务继续出现问题。 如果是正常请求,大促活动,那么这个需要评估一下,为什么这个大促活动没有做?大促的活动产生的流量超出预期很多,对应需要做评估,然后做扩容也好,做一些临时的扩容,可能需要做临时扩容,临时加机器。如果是服务上存在问题,先回滚,业务存在问题先回滚,回滚,然后对应加缓存,提高并发也好,检查具体的哪出现了设计问题也好,然后流量问题也好,对应相应处理。

点评

优点:- 排查链路系统:时间轴关联(渐变 vs 突增)→ 发布内容 → Java 指标 → 接口层 → 磁盘/机器 → 处置,层次清晰 - 处置场景覆盖全:新功能回滚 / 攻击拦截(IP/账号/区域)/ 大促扩容 / 加缓存,且强调"保留环境 + 下日志" - 有资深工程意识:区分现象与根因,先定位再处理,不盲动
不足:- Java 冷启动特有原因未展开:JIT 预热、连接池冷启动、本地缓存空——这些是"启动后几分钟"最典型的三大原因 - Load 飙高未区分 CPU load 与 D 状态进程(IO wait),是 D 状态就必须看磁盘/网络 - 处置决策缺少止血优先级:先回滚 vs 先扩容 vs 先降级,哪个先?
补充:- **JIT 未预热**:HotSpot C1/C2 需数百次调用才编译到热点,冷启动字节码解释执行慢 - **连接池冷启动**:DB/Redis/HTTP client 连接池需要预热,连接建立几十到几百 ms - **本地缓存空**:Guava/Caffeine 全穿透到 DB/Redis - **arthas dashboard**:一键看板看 CPU/内存/线程状态;JFR 抓启动阶段热点方法、锁竞争、GC 事件 - **元空间不足** 触发 FullGC 的坑(动态生成代理类、CGLIB) - **预防手段**:预热接口 + 灰度发布 + 分批重启(蓝绿/滚动)

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察"性能问题排查方法论"——从现象到根因的分层下钻,以及 Java 应用冷启动特有的耗时来源。

**知识点拆解**:
1. **排查五步法**:定位现象(时间轴/范围)→ 关联变更(发布/流量/依赖)→ 分层下钻(应用/JVM/接口/中间件/机器)→ 抓证据(arthas/JFR/线程 dump)→ 处置止血
2. **Java 冷启动三大耗时源**:JIT 未预热(解释执行 vs 编译执行)、连接池冷启动(TCP/TLS 握手 + 认证)、本地缓存空(全穿透到远端)
3. **Load 的本质**:`load = 运行态进程数 + D 状态进程数`。CPU 打满看 C1/C2 编译和热点方法;D 状态多则看磁盘 IO / 网络阻塞
4. **处置止血优先级**:降级 > 限流 > 回滚 > 扩容(前两个秒级生效,回滚分钟级,扩容小时级)
5. **预防手段**:预热接口(启动后异步压热点)、灰度发布(1%→10%→50%)、滚动重启(避免同时启动导致惊群)

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:JIT 编译阶段(C1 解释 → C1 简单编译 → C2 优化)、连接池预热算法、FullGC 触发条件
- **广度**:Linux `vmstat 1`(d 状态)、`pidstat`(单进程 CPU)、`ss -s`(连接状态)、Prometheus JMX 指标
- **应用**:灰度发布系统、预热脚本、服务发现的健康检查窗口

**延伸递进面试题**:
1. **JIT 预热到底要多久?怎么验证?**
- 解答要点:C1 约 200-1000 次调用触发,C2 约 10000 次;用 `java -XX:+PrintCompilation` 或 JFR 观察编译事件;生产上可主动调预热接口触发
2. **load 飙高但 CPU 不高,可能是什么原因?**
- 解答要点:D 状态进程堆积(磁盘 IO 慢、NFS 卡、网络阻塞);`cat /proc/loadavg` 对比 `vmstat 1` 的 `r/b` 列;可能是 GC STW(CPU 时间没算到用户态)
3. **如何设计预热机制让应用启动即战?**
- 解答要点:启动后异步任务调 top-N 热点接口(限流避免冲击);连接池 keepalive 保活;预热脚本在 readiness 探针通过后触发

第三部分:面试者参考回答(三步法口语化,300-400 字)

**【一句话结论】**
面试官您好,"启动后几分钟飙高"是典型的冷启动问题,我先从时间轴确认是渐变还是突增,然后分三层排查:JVM 冷启动(JIT/连接池/缓存)、系统资源(CPU/D 状态/磁盘)、业务流量(灰度比例/依赖方)。

**【三点展开】**
第一,Java 冷启动三件事:JIT 没预热,热点方法还在解释执行;连接池刚建,TCP/TLS 握手开销大;本地缓存空,全部穿透到 DB/Redis。用 `arthas dashboard` 看类加载数是否还在涨,`jcmd <pid> VM.event` 看 C1/C2 编译事件,基本能锁定是不是 JIT 没追上。
第二,Load 飙高要区分是 CPU load 还是 D 状态。`vmstat 1` 看 d 列,d 大说明卡在 IO,可能是日志刷盘、NFS 卡住、或者 FullGC STW 把 CPU 时间挤到用户态之外。这时候看 arthas 的线程状态,WAITING/BLOCKED/DUMPED 多不多。
第三,处置有优先级:先降级/限流(秒级),再回滚(分钟级),最后扩容(小时级)。如果是新功能问题,回滚后要保留现场抓线程 dump 和 heap dump 分析根因,别急着清理环境。

**【一句边界】**
当然,如果是灰度只发了一台机器、且没有预热接口,用户流量直接被冷实例承接,这种场景下排查完还是要推预热机制,光靠排查没用。

#2 程序运行期发生ClassNotFoundException 可能是什么原因? low practice

题目

程序运行期发生ClassNotFoundException 可能是什么原因?

我的回答

运行期间发生 class not found 一般是因为包的异常,引入包的异常,存在包冲突,某些包应该被其他引入了,但是没有被引入。 另外就是反射,反射使用获取了一个不存在的包,也可能会是 class not find。 再有就是一些动态加载的东西,这时候也可能会产生 class not find。 核心原因还是找不到,找不到对应的包,对应的类。 找不到的原因,可能是包冲突导致相互之间的冲突无法正常使用,可能是使用反射读取到错误的类名,都是有可能。根据具体的报警位置,然后去排查对应的问题。如果是包冲突,就对进,对行,进行相应调整。排除依赖或者引入新依赖,引入共享依赖。 明显是类名错误反射引起的,那么就对应的检查是何处传来了错误的类。对应的进行处理。

点评

优点:- 覆盖了三个主要来源:包依赖(引入异常/冲突)、反射调用(错误类名)、动态类加载 - 有根因意识:包冲突、反射错误类名 - 处置思路对:定位位置 → 排查 → 调整依赖
不足:- 没抓住"运行期"这个关键词:运行期(非启动期)意味着类路径没变,问题来自"延迟加载"或"类加载器隔离" - 未区分 ClassNotFoundException(首次加载失败)与 NoClassDefFoundError(曾加载成功但依赖缺失或静态初始化失败) - 未提关键运行期场景:热替换、多 ClassLoader 隔离、序列化反序列化、SPI 机制
补充:- **ClassNotFoundException vs NoClassDefFoundError**:前者是"首次加载找不到",后者是"曾经找到,但依赖的类缺失/静态初始化失败",JVM 内部处理不同 - **运行期特有场景**: - 热部署(Arthas retransform、Spring DevTools)后类名/版本不一致 - 多 ClassLoader 隔离(OSGi、Tomcat WebAppCL、Spring Boot LaunchedURLClassLoader)之间的可见性 - 老数据用老类名序列化,类改名后反序列化失败 - Spring CGLIB/JDK 动态代理生成失败 - SPI(`META-INF/services`)配置类名拼写错误 - **排查工具**:`jcmd <pid> VM.classloader_stats` / `VM.classloader_dump`、`arthas sc -d <class>`、Maven `dependency:tree`、`-verbose:class` 打印类加载日志

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 JVM 类加载机制的深度理解——不只是"找不到类",而是"什么时候、在哪一层找不到"。

**知识点拆解**:
1. **CNFE vs NCDFE**:ClassNotFoundException 是运行时抛出的 checked exception,代表首次加载失败;NoClassDefFoundError 是 Error,代表曾经加载成功但依赖的类缺失或静态初始化抛 ExceptionInInitializerError
2. **类加载器委派模型**:Bootstrap → Extension → Application → 自定义 ClassLoader。任何一层找不到都会向上问父加载器,父找不到再找子类。跨 ClassLoader 的类"互相看不见"是运行期 CNFE 的核心场景
3. **运行期触发的三种路径**:
- 反射调用:`Class.forName("xxx")` 找不到
- 动态代理:CGLIB/JDK Proxy 生成代理类时父类找不到
- 延迟加载:代码路径首次执行才触发类加载
4. **常见根因**:Maven `provided` 作用域依赖漏打、shade/uber-jar 过滤规则错误、多 ClassLoader 隔离(Tomcat/OSGi)、SPI 配置错、JDK 版本升级(`javax.*` → `jakarta.*`)
5. **排查工具链**:`jcmd VM.classloader_dump`、`arthas sc -d`、`dependency:tree`、`-verbose:class`、`java -Xbootclasspath` 检查扩展

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:类加载器双亲委派的实现(`loadClass` 三阶段:findLoadedClass → findBootstrapClass → findClass)、双亲委派破坏场景(SPI、热部署)
- **广度**:与 `UnsatisfiedLinkError`(native 库缺失)、`ExceptionInInitializerError`(静态块失败)的区分
- **应用**:Maven shade 过滤规则、Spring Boot fat jar 的多 ClassLoader、Tomcat webapp 隔离

**延伸递进面试题**:
1. **CNFE 和 NCDFE 到底差在哪?给个具体例子。**
- 解答要点:CNFE 是 ClassLoader 找不到 .class 文件;NCDFE 是 JVM 已解析到但依赖的类缺失或静态初始化失败。例:静态块里调用的类被 shade 过滤掉,首次运行会抛 NCDFE
2. **Spring Boot fat jar 的类加载器和 Tomcat 有本质区别吗?**
- 解答要点:Spring Boot 用 LaunchedURLClassLoader 加载 BOOT-INF/lib/*.jar,破坏双亲委派(lib 下依赖先于 app 加载);Tomcat 用 WebAppClassLoader,隔离不同 webapp 的同名类
3. **线上运行期突然 CNFE,怎么快速定位是哪个 ClassLoader 出的问题?**
- 解答要点:`jcmd <pid> VM.classloader_dump <id>` 看每个 CL 的 URL;`arthas sc -d <class>` 直接查类的 ClassLoader 和 jar 路径;异常堆栈里找 `ClassLoader` 类型

第三部分:面试者参考回答(三步法口语化,300-400 字)

**【一句话结论】**
面试官您好,"运行期"这个关键词很重要——不是启动就报错,说明类路径没变,问题大概率来自"延迟加载触发"或"多 ClassLoader 隔离",我会先确认异常类型(CNFE 还是 NCDFE),再按 ClassLoader 层级排查。

**【三点展开】**
第一,先区分异常类型:ClassNotFoundException 是首次加载找不到 .class;NoClassDefFoundError 是曾经加载过但依赖缺失或静态初始化失败。后者更隐蔽,往往静态块里调用的类被 shade 过滤掉才会踩到。
第二,看调用栈定位触发路径:是反射 `Class.forName` 触发,还是 CGLIB 动态代理生成代理类失败,还是延迟执行的代码路径首次走。反射路径查传入的类名字符串对不对;动态代理查父类加载器和父类可见性。
第三,用 `arthas sc -d <className>` 看类的 ClassLoader 和 jar 路径,用 `jcmd <pid> VM.classloader_dump` 看每个 CL 的 URL 列表,确认类在哪个 ClassLoader 范围内。常见根因:Maven `provided` 依赖漏打、shade 过滤规则错误、Spring Boot 与 Tomcat 的 ClassLoader 差异、SPI 配置拼写错误。

**【一句边界】**
当然,如果是 JDK 9+ 模块系统,跨模块访问的类还会被 `IllegalAccessError` 拦截,那是另一个坑——运行期突然 CNFE 也可能只是模块名拼错触发的假象。

#3 什么是POI,为什么它会导致内存溢出? low practice

题目

什么是POI,为什么它会导致内存溢出?

我的回答

POI 是用来进行 excel 等表格解析的工具,内存中要加载 excel 等表格的内容,如果使用全量加载,单次加载过大的 excel 表格,就有可能导致内存溢出; 处理上可以使用流式加载,分批次的进行读取加载,而不是一次性全部读取,防止内存溢出

点评

优点:- 抓到了核心矛盾:全量加载 vs 流式加载 - 处置方向对:分批读取避免 OOM
不足:- 未提 POI 的两个实现模式:UserModel(DOM,全量内存)vs SAX(流式,事件驱动) - 未区分 xls/xlsx 的内存差异:HSSF/XSSF 全量 DOM,SXSSF/SAX 流式 - 未提具体内存模型:XSSF 里每个 cell 是完整对象,1GB xlsx 解压后可能占 10GB+ - 未提替代方案:EasyExcel(基于 SAX,内存恒定)、Poi-TL
补充:- **POI 两大 API**: - **UserModel(DOM)**:`HSSFWorkbook`(.xls)/ `XSSFWorkbook`(.xlsx),全量加载到内存 - **SAX API**:`XSSFSheetXMLHandler`,事件驱动,逐行回调,内存恒定 - **写场景用 SXSSF**:`SXSSFWorkbook` 滑动窗口(默认保留 100 行),超过窗口自动刷到临时文件 - **内存放大系数**:xlsx 是 zip,解压后 DOM 加载通常放大 5-10 倍,100MB xlsx 可能吃 1GB heap - **典型 OOM 堆栈**:`OutOfMemoryError: Java heap space` + 大量 `Row`/`Cell` 对象占 Old 区 - **优化 checklist**:及时 `close()`(Workbook/InputStream)、用 SXSSF 写大表、EasyExcel 替代、限制并发、监控堆使用率

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 Java IO 生态中"内存模型选择"的理解——不是"POI 会 OOM",而是"什么时候会、怎么避免"。

**知识点拆解**:
1. **POI 的四种实现**:
- **HSSF**(.xls):DOM 全量加载,1 万行就危险
- **XSSF**(.xlsx):DOM 全量加载,但 xlsx 是 zip 压缩,内存放大 5-10 倍
- **SXSSF**(.xlsx 写):滑动窗口,默认 100 行,超过自动刷临时文件
- **SAX API**(.xlsx 读):事件驱动 `XSSFSheetXMLHandler`,逐行回调,内存恒定
2. **内存放大系数**:xlsx 是 zip 格式,解压后 DOM 加载通常放大 5-10 倍;100MB xlsx → 500MB-1GB heap
3. **典型 OOM 场景**:读百万行 xlsx 全量加载、并发多个 workbook 未 close、SXSSF 忘调 `dispose()` 临时文件堆积
4. **替代方案对比**:
- EasyExcel(阿里):基于 SAX,1 万行内存恒定 < 10MB,API 更简洁
- Poi-TL:模板填充,不解析全表
- JXL:只支持 .xls,已停止维护
5. **工程实践**:限制并发读 xlsx 的线程数、用对象池复用 Workbook、监控 `-XX:+HeapDumpOnOutOfMemoryError`

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:DOM vs SAX 的内存模型本质差异、SXSSF 滑动窗口机制(`rowaccessWindowSize`)
- **广度**:与 Apache Commons CSV、OpenCSV 的对比;JDK 9+ `Files.lines` 处理大文本
- **应用**:大文件上传下载(分片 + 流式)、数据导入场景(EasyExcel + 分片校验)

**延伸递进面试题**:
1. **XSSF 和 HSSF 的内存差异具体有多大?给个数字。**
- 解答要点:10 万行 xlsx 用 XSSF 加载约 500MB heap,HSSF 的 .xls 只支持 65536 行且 DOM 占用相似;SXSSF 保持 100 行窗口只占几十 MB
2. **EasyExcel 为什么能保持内存恒定?**
- 解答要点:基于 SAX 事件驱动,每处理一行释放上一行对象;用 `@ExcelProperty` 注解做字段映射,避免反射全表
3. **线上跑了一个读 500MB xlsx 的任务,OOM 了,怎么事后复盘?**
- 解答要点:用 `-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp` 抓 dump;MAT 分析 dominator tree,看 `Workbook`/`Sheet`/`Row` 的 retained heap;结论是改 SXSSF/SAX

第三部分:面试者参考回答(三步法口语化,300-400 字)

**【一句话结论】**
面试官您好,POI 是 Apache 的 Excel 解析库,OOM 的根因是它默认用 DOM 模型全量加载到内存——xlsx 是 zip 压缩,解压后 DOM 通常放大 5-10 倍,100MB 文件能吃掉 1GB heap。

**【三点展开】**
第一,POI 有四个实现要分清楚:HSSF(.xls DOM)、XSSF(.xlsx DOM)、SXSSF(.xlsx 流式写,滑动窗口)、SAX API(.xlsx 流式读,事件驱动)。读大文件必须用 SAX,写大文件用 SXSSF,DOM 只适合小文件。
第二,内存放大是核心坑:xlsx 是 zip 格式,解压后每个 cell 在内存里是完整对象,100MB xlsx 解压后可能 1GB heap。典型 OOM 堆栈是 `OutOfMemoryError: Java heap space` + 大量 `Row`/`Cell` 占 Old 区。SXSSF 默认保留 100 行,超过自动刷到临时文件,用完必须调 `dispose()` 释放临时文件。
第三,生产上替代方案首选 EasyExcel——阿里基于 SAX 封装,1 万行内存恒定 < 10MB,API 用 `@ExcelProperty` 注解映射,比手写 SAX 回调简洁得多。配合堆监控 + `HeapDumpOnOutOfMemoryError` 抓 dump 分析 retained heap,能快速定位是不是 DOM 加载惹的祸。

**【一句边界】**
当然,如果是几十行的小文件做模板填充,XSSF 直接用完全没问题,不必为了流式而流式——SXSSF/EasyExcel 的 API 更啰嗦,小场景反而降低可读性。

#4 业务系统Redis出现超时如何排查和解决? medium practice

题目

业务系统Redis出现超时如何排查和解决?

我的回答

Redis 出现超时,这时候要排查的思路就看整个链路中都会涉及什么东西。首先是链接链接链接池请求是否被打满。然后其次是网络通讯,是否在访问 Redis 过程中出现了网络波动。还有就是是否有大 key 慢查询,可以通过 Redis 的监控去查看对应日志是否有相关的内容。再然后去考虑是否出现了大量的过期内内存超时。还有就是是否有大量的lua 脚本或者 redis 事务相关的大命令执行占用 redis 线程,导致无法正常运行其他请求; 针对上述问题处理上有不同方案; 如果连接池满了,考虑是否增加连接池配置,以及转用 redis 集群,提升 redis 承载能力; 如果是网络出现波动,看是否更换实例位置,选择和服务就近机房,减少网络影响; 如果是大 key 热 key,那么考虑 key 拆分,分析大key 是否合理?是否需要调整业务设计; 如果是大量 lua 脚本,那么看是否能进行分解,降低单个 lua 脚本复杂度,提升单个 lua 脚本的执行时间,不要在 lua 脚本中做循环之类的操作;

点评

优点:- 排查链路完整:连接池 → 网络 → 大key/慢查询 → 过期策略 → Lua/事务阻塞,覆盖主要场景 - 每类问题都有对应处置:扩容 / 就近机房 / key 拆分 / Lua 分解,处置手段合理 - 有分层意识:从客户端链路到 Redis 内部到网络层
不足:- 未提 Redis 慢日志(SLOWLOG)——排查 Redis 超时的第一手证据,`SLOWLOG GET 128` 直接看耗时命令 - 未提持久化阻塞:RDB/AOF fork 子进程导致短暂卡顿(`fork()` syscall 拷贝页表) - 未提主从同步阻塞:大 key 复制导致从库阻塞、全量同步期间主库卡顿 - 未提热 key 单分片热点:cluster 模式下某个 key QPS 极高打爆单分片 - 未提降级方案:熔断(Resilience4j)、缓存穿透保护、本地缓存兜底
补充:- **SLOWLOG**:`SLOWLOG GET 128` + `SLOWLOG LEN`,看耗时 > `slowlog-log-slower-than` 的命令 - **INFO 命令**:`INFO stats`(`rejected_connections`/`blocked_clients`)、`INFO commandstats`(`cmdstat_xxx` 的 usec/calls)、`INFO keyspace` - **持久化阻塞**:RDB `save`/`bgsave` fork 时 COW(copy-on-write)导致内存翻倍;AOF 重写也是 fork - **主从同步**:全量同步期间主库要 fork 生成 RDB,大 key 复制导致从库 `READONLY` - **客户端超时配置**:Lettuce/Jedis `commandTimeout`、`socketTimeout`、`retryTimes`;连接池 `maxTotal`/`maxWaitMillis` - **降级三件套**:Resilience4j 熔断 + 本地缓存(Caffeine)兜底 + 缓存击穿互斥锁

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察 Redis 超时排查的完整方法论——从现象(超时)到根因(命令阻塞/网络/资源)的分层下钻。

**知识点拆解**:
1. **超时来源分层**:
- 客户端侧:连接池打满、`commandTimeout` 过短、`maxWaitMillis` 超时
- 网络侧:抖动/丢包/机房跨域、TCP retransmit
- Redis 侧:单线程阻塞(大命令/Lua/KEYS)、持久化 fork、主从同步
- 集群侧:hash slot 迁移(cluster down)、热 key 单分片
2. **第一手证据**:`SLOWLOG GET 128` + `SLOWLOG LEN`;`INFO stats` 看 `rejected_connections`、`blocked_clients`、`instantaneous_ops_per_sec`
3. **命令阻塞典型**:`KEYS *`、`SMEMBERS`/`HGETALL` 大集合、`SORT`、Lua 循环、`BLPOP` 阻塞式命令
4. **持久化阻塞机制**:RDB `bgsave` 用 fork + COW,主进程 fork 瞬间可能卡顿(数百 ms 到数秒);内存越大 fork 越慢
5. **降级三件套**:
- **熔断**:Resilience4j CircuitBreaker,连续失败后短路
- **本地兜底**:Caffeine 本地缓存 + 随机 TTL 防击穿
- **互斥重建**:`setnx` + 后台线程重建热点 key

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:Redis 单线程模型、IO 多路复用(epoll)、fork COW 页表机制
- **广度**:与 Lettuce/Jedis 客户端配置对比;Redis Cluster 的 hash slot 与节点拓扑
- **应用**:生产监控(阿里云/腾讯云 Redis 监控 + 自建 exporter)、慢查询治理平台

**延伸递进面试题**:
1. **Redis 是单线程,为什么快?慢在哪?**
- 解答要点:快在 IO 多路复用 + 内存操作 + 无锁;慢在大命令(KEYS/SMEMBERS)、Lua 阻塞、fork、主从同步
2. **fork 为什么会阻塞主进程?怎么避免?**
- 解答要点:fork 要拷贝页表,内存越大拷贝时间越长;可调整 `save` 频率、用 AOF 代替 RDB、扩容降低单节点内存
3. **热 key 和热点分片怎么排查和治理?**
- 解答要点:`MONITOR`(生产慎用)或 `redis-cli --hotkeys`(LUA 统计);治理用本地缓存兜底、读写分离、客户端分片(一主多读)

第三部分:面试者参考回答(三步法口语化,300-400 字)

**【一句话结论】**
面试官您好,Redis 超时我按"客户端 → 网络 → Redis 内部"三层排查,第一手证据是 SLOWLOG,然后按根因分类处置:阻塞命令治理、连接池扩容、降级兜底。

**【三点展开】**
第一,先看客户端侧:连接池打满(Lettuce/Jedis 的 `maxWaitMillis` 超时)、`commandTimeout` 配置过短。用 `INFO stats` 看 `rejected_connections` 和 `blocked_clients`,用 `CLIENT LIST` 看空闲连接和异常 client。客户端侧问题改配置即可,秒级生效。
第二,看网络层:跨机房延迟、TCP 丢包。用 `ping`、`mtr`、`tcpdump` 抓包,看 retransmit 比例。网络波动通常持续几秒,改机房路由或就近部署 Redis 实例能解决。
第三,看 Redis 内部:`SLOWLOG GET 128` 定位耗时命令,典型是大集合 `SMEMBERS`/`HGETALL`、`KEYS *`、Lua 循环。持久化方面看 `INFO persistence` 的 `rdb_last_bgsave_status` 和 fork 时间;主从同步看 `INFO replication` 的 `master_sync_in_progress`。治理手段:大 key 拆分、Lua 分解、读写分离、Resilience4j 熔断 + Caffeine 本地缓存兜底。

**【一句边界】**
当然,如果是集群模式下的 hash slot 迁移导致的短暂超时,那是 Redis Cluster 的正常行为,用 `redis-cli cluster slots` 看拓扑,客户端会自动重定向,一般不用介入。

#5 如何解决消息重复消费、重复下单等问题? medium practice

题目

如何解决消息重复消费、重复下单等问题?

我的回答

类似这种重复性的问题,基本上都是幂等性没有设计好。主要是考虑如何处理对应的幂等性。常用的幂等性处理方案是业务 ID 幂等性,或者是 token 幂等性,然后入参幂等性。针对几种方案进行不同的处理和设计。 首先消息的重复,消息上使用唯一的消息 ID 去确保每个消息只重复消费,不重复消费,只消费一次,消费过就直接过掉,下次来了也不再进行二次消费。基于对消息 ID 的进行去重处理。 重复下单的话,可以涉及两个方向,一个是入参,入参所有入参都是一样的,并且时间间隔很短,这时候可以考虑基于 Redis 进行去重密等,处理对应的重复请求。另外可以在每个请求前给前端一个 Token,唯一的 Token,每个请求都唯一 Token,或者说 UUID,让它去唯一标识这个请求。这时候它请求过来,我之前这个已经使用过了,就不能再次请求,告知它这个请求已经重复了。 那就是基于订单的状态机去进行判定,状态只能从一个状态修改为另一个状态,如果二次修改的话是不能生效的,也可以实现对应的归档。

点评

优点:- 抓到了核心:幂等性是根本 - 三种方案覆盖全:业务 ID 幂等、Token 幂等、入参幂等 - 覆盖了消息侧(唯一消息 ID 去重)和请求侧(Token/状态机)两个方向 - 状态机思路到位:状态单向流转,二次修改不生效
不足:- 未提 MQ 重复消费的根本原因:at-least-once 语义、Consumer Rebalance、消费者崩溃重试 - 未提去重存储设计细节:Redis SETNX + TTL(一般 5-10 分钟)、数据库唯一索引、`INSERT ... ON DUPLICATE KEY UPDATE` - 未提状态机的乐观锁实现:`UPDATE ... WHERE status = '待支付'`,依赖唯一索引防并发 - 未提 MQ 事务消息:RocketMQ 事务消息、本地消息表(本地事务 + MQ 异步补偿)
补充:- **为什么 MQ 会重复**:at-least-once 投递(生产端确认前崩溃 → 重试、Consumer Rebalance 导致 offset 回退、消费者崩溃未提交 offset) - **幂等键设计三要素**:唯一性、稳定性、TTL(Token 5 分钟,业务 ID 永久) - **状态机组合拳**:状态机 + 唯一索引 + 乐观锁 SQL(`UPDATE orders SET status='已支付' WHERE id=? AND status='待支付'`) - **事务消息**:RocketMQ 半消息 + 本地事务检查、本地消息表(业务表 + 消息表同事务,定时扫表发 MQ) - **降级策略**:Redis 不可用时降级到本地内存 + 数据库唯一索引兜底

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察"幂等性"在分布式系统中的完整设计——从消息层到请求层到数据层的组合拳。

**知识点拆解**:
1. **MQ 重复的根本原因**:
- 生产端:发送后未收到 ack 就崩溃 → 重试发送(at-least-once)
- Broker:持久化前崩溃 → 重发同一消息
- Consumer:处理完业务但未提交 offset → Rebalance/崩溃后重新消费
2. **幂等性三种实现**:
- **业务 ID 幂等**:数据库唯一索引(`unique key on biz_id`),`INSERT ... ON DUPLICATE KEY UPDATE`
- **Token 幂等**:Redis `SET key value NX EX 300`,请求前发 token,请求时消耗
- **入参幂等**:hash(入参) 作为 key,短时间相同入参视为重复(注意误判)
3. **状态机 + 乐观锁**:`UPDATE orders SET status='已支付' WHERE id=? AND status='待支付'`,依赖唯一索引防并发,affected rows = 0 表示重复
4. **事务消息方案**:
- RocketMQ 事务消息:半消息 + 本地事务 + 回查
- 本地消息表:业务表 + 消息表同事务,定时扫表发 MQ,失败重试
- Outbox Pattern:业务表 + Outbox 表同事务,CDC(Debezium)监听 binlog 发 MQ
5. **降级策略**:Redis 不可用时降级到本地内存 Map + 数据库唯一索引兜底

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:Kafka 的 at-least-once 语义实现(commit offset 时机)、RocketMQ 事务消息的三阶段
- **广度**:与 Stripe/支付宝的"订单幂等"实践对比;Sagas 模式的补偿事务
- **应用**:支付系统、订单系统、消息驱动架构的幂等设计

**延伸递进面试题**:
1. **RocketMQ 事务消息的三阶段具体是什么?**
- 解答要点:①发送半消息 ②执行本地事务 ③提交/回滚半消息;Broker 定时回查本地事务状态
2. **本地消息表怎么保证消息不丢不重?**
- 解答要点:业务表 + 消息表同事务(insert 业务 + insert 消息表);定时扫表发 MQ;发送成功后 delete 消息;消息表加唯一索引防重复发送
3. **Token 幂等和状态机怎么选?**
- 解答要点:Token 适合请求级别去重(防双击);状态机适合业务级别(订单只能单向流转);生产上两者组合用

第三部分:面试者参考回答(三步法口语化,300-400 字)

**【一句话结论】**
面试官您好,重复消费和重复下单本质是幂等性问题,MQ 侧靠"业务 ID 去重 + at-least-once 容忍",请求侧靠"Token + 状态机 + 乐观锁"组合拳。

**【三点展开】**
第一,先理解为什么 MQ 会重复:Kafka/RocketMQ 都是 at-least-once 语义,生产端未收到 ack 就崩溃会重试、Consumer 处理完但未提交 offset 会 Rebalance 重新消费。所以不追求"不重复",而是"重复了也能正确处理"。消息侧用唯一消息 ID 去重,`INSERT ... ON DUPLICATE KEY UPDATE` 或 Redis `SET NX EX 600` 记已消费 ID。
第二,请求侧重复下单有三个层次:① 入口层用 Token(Redis `SETNX + TTL 5 分钟`)防双击;② 业务层用唯一索引(`unique key on out_order_no`)防重复写入;③ 状态机用乐观锁 `UPDATE orders SET status='已支付' WHERE id=? AND status='待支付'`,affected rows = 0 表示重复。三者组合拳,任一层兜住都不会重复。
第三,跨服务场景用事务消息:RocketMQ 事务消息(半消息 + 本地事务 + 回查)或本地消息表(业务表 + 消息表同事务,定时扫表发 MQ)。这样本地事务和消息发送的原子性才能保证,不会出现"下单成功但通知没发"的中间态。

**【一句边界】**
当然,幂等设计有个坑:Token TTL 太短会导致重复(Token 过期后客户端重试),太长会导致 Token 泄露被恶意利用。生产上一般是 5-10 分钟,配合限流 + 风控做二次防护。

#6 用@Scheduled执行定时任务,如何避免集群的并发问题 medium practice

题目

用@Scheduled执行定时任务,如何避免集群的并发问题

我的回答

首先,集群了为什么要使用单机的计划任务,这里应该去使用分布式调度任务平台,比如 xxljob 之类的; 如果一定要用单机计划任务那你就要考虑任务执行的幂等性了。首先确认这个任务是否是幂等的,它执行多次是否会有不同的影响。如果没有,那么就简单的控制做一个锁。这个任务在执行节点时候,通过类似于 Redis 锁这种分布式锁去控制,这个任务已经执行了,在这个节点已经执行了。执行过后,其他执行中的其他节点在执行的时候判定一下,如果当前已经在执行或者已经执行完了,其他节点不再二次执行,防止并发。 核心其实有很多可以处理的,比如说任务状态、任务记录,然后换成 Redis 锁,都可以处理这个问题。比如执行,执行完任务,在这个节点就查一条日志,下次执行前就查询数据库是否有这个日志。其他任务同时间做一个锁,执行的时候就是,有这个日志大家就不执行了,没这个日志的,就第一个就执行,就结束了,就可以控制这个问题。 而第一个要给计划任务加锁,防止并发。第二个就是做密等。当然不做密等的话,就做控制,控制限定,然后判定执行了,当前这个时间节点要求执行的执行了,其他就不执行。就能解决相应的问题。

点评

优点:- 第一个判断很关键:应该用分布式调度平台(xxl-job)而不是硬凑单机任务 - 抓到了核心思路:分布式锁 + 幂等性组合 - 覆盖了两种策略:幂等(任务记录日志表)+ 分布式锁(Redis 锁) - 有分场景意识:先判断任务是否幂等,再决定加锁还是做幂等
不足:- 未提分布式锁的实现细节:TTL 陷阱(业务执行超过 TTL 锁失效)、Redisson 看门狗机制、RedLock 争议 - 未提 xxl-job 的核心机制:分片广播、失败重试、执行器心跳、路由策略、阻塞处理策略 - 未提 @Scheduled 的单机坑:默认单线程串行、`fixedRate` vs `fixedDelay` 语义、TaskScheduler 线程池 - 未提其他分布式调度方案:Elastic-Job、Quartz JDBC store、阿里云 SchedulerX
补充:- **分布式锁三要素**:原子性(`SET key value NX PX 30000`)、唯一性(value 用 UUID 防误删)、续期(Redisson 看门狗 30s 续期) - **TTL 陷阱**:业务执行超过 TTL → 锁失效 → 其他节点获取锁 → 并发执行;解法是看门狗续期或业务执行前检查锁是否还在 - **xxl-job 核心机制**: - 分片广播:多节点按 `shardIndex/shardTotal` 分片执行 - 阻塞策略:覆盖(后触发覆盖前)、丢弃(后触发丢弃)、排队(串行执行) - 路由策略:轮询、一致性哈希、故障转移、执行器繁忙度 - 失败重试:支持配置重试次数 + 重试间隔 - **其他方案**: - Elastic-Job(ShardingSphere):分片 + 弹性扩缩 - Quartz JDBC store:`QRTZ_LOCKS` 表行级锁,简单但扩展性差 - 阿里云 SchedulerX / 腾讯云 TAT:托管服务,无需自维护

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察"单机调度"到"分布式调度"的架构演进,以及分布式锁的工程化细节。

**知识点拆解**:
1. **@Scheduled 的单机局限**:
- 默认单线程(`ScheduledExecutorService` 单线程),多个任务串行
- `fixedRate` 是"从上次开始计",`fixedDelay` 是"从上次结束计",语义不同
- 无执行日志、无失败重试、无告警、无路由
2. **分布式锁三大陷阱**:
- **TTL 陷阱**:业务执行超过 TTL → 锁失效 → 并发;解法:Redisson 看门狗(默认 30s 续期)
- **误删陷阱**:A 节点锁超时释放,B 节点获取锁,A 执行完误删 B 的锁;解法:value 用 UUID,删除前 CAS 校验
- **主从切换陷阱**:Redis 主节点写锁,未同步到从节点就宕机,从节点选主后锁丢失;解法:RedLock(有争议,Martin Kleppmann 反对)
3. **xxl-job 核心机制**:
- 分片广播:`shardIndex/shardTotal` 分片执行,天然避免并发
- 阻塞策略:覆盖(`DispatcherTimeOut` 触发覆盖)、丢弃(后触发丢弃)、排队(FIFO 串行)
- 路由策略:轮询、一致性哈希(同 shard 路由到同节点)、故障转移、执行器繁忙度
- 执行器心跳:每 10s 心跳,调度中心检测执行器健康
4. **方案对比**:
- **Redis 分布式锁**:轻量、简单、TTL 陷阱;适合单机任务临时上锁
- **xxl-job**:功能全、生态好、有 UI、有告警;适合生产级定时任务
- **Elastic-Job**:分片能力强、弹性扩缩;学习曲线陡
- **Quartz JDBC store**:简单、无需外部依赖;扩展性差、锁竞争
5. **兜底策略**:即使锁失效,业务幂等(唯一索引 + 状态机)要能兜住

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:Redis 分布式锁的原子性实现(Lua 脚本)、RedLock 的争议、Redisson 看门狗原理
- **广度**:与 Quartz、Elastic-Job、SchedulerX 的对比;Cron 表达式 vs Quartz Cron
- **应用**:大数据 ETL 调度(Airflow/DolphinScheduler)、任务血缘、失败补偿

**延伸递进面试题**:
1. **Redis 分布式锁的 TTL 怎么设置合理?**
- 解答要点:一般 30s + 看门狗续期;业务执行时间远超 TTL 时要评估是否真的需要锁(可能任务本身设计有问题)
2. **xxl-job 分片广播和 Quartz JDBC store 有什么区别?**
- 解答要点:xxl-job 是"分片执行"(每个节点执行自己那份),Quartz 是"单点执行"(多节点竞争同一份任务,靠锁选出一个);前者扩展性好,后者简单
3. **如果任务执行时间超过锁 TTL,怎么保证不并发?**
- 解答要点:Redisson 看门狗自动续期(默认 30s);业务执行前检查锁是否还在(CAS 校验 value);业务结束释放锁时再次 CAS 校验

第三部分:面试者参考回答(三步法口语化,300-400 字)

**【一句话结论】**
面试官您好,单机 @Scheduled 在集群下必然并发,首选上分布式调度平台(xxl-job);如果一定要在 @Scheduled 上救场,就用"分布式锁 + 业务幂等"组合拳。

**【三点展开】**
第一,先判断该不该用 @Scheduled:如果是生产级定时任务(有重试、告警、分片需求),直接上 xxl-job,它有分片广播(多节点分片执行)、阻塞策略(覆盖/丢弃/排队)、路由策略(轮询/一致性哈希)、失败重试、执行日志,单机 @Scheduled 缺的功能它都有。
第二,如果一定要 @Scheduled + 分布式锁:用 Redisson `RLock` 而不是裸 Redis `SETNX`,Redisson 有看门狗机制(30s 自动续期)解决 TTL 陷阱,value 用 UUID + CAS 校验防误删。锁的粒度要按业务场景选:全局唯一锁(任务级)、hash 分片锁(数据级)。锁竞争高时降级到数据库 CAS(`UPDATE task_lock SET locked_by=? WHERE status='free' AND expire_time>?`)。
第三,锁失效的兜底是业务幂等:任务处理的数据写入用唯一索引(`unique key on biz_id`),状态机用乐观锁 SQL(`UPDATE ... WHERE status=?`)。即使锁失效并发执行了,业务层也能兜住不重复处理。锁是"防并发",幂等是"防重复",两者组合才完整。

**【一句边界】**
当然,如果是分钟级以上的低频任务,甚至可以用"数据库唯一索引 + 时间窗口"做最简锁(`INSERT INTO task_log(task_name, exec_time) VALUES(?, ?) ON DUPLICATE KEY UPDATE ...`),不用引入 Redis 依赖——KISS 原则,能用数据库解决的别引入新组件。

#7 如何做平滑的数据迁移? medium practice

题目

如何做平滑的数据迁移?

我的回答

数据迁移想要平滑的,是大家所有人的目标。但是这个是否能达到,其实很难。 涉及的问题首先是双库的并行,然后数据同步的规则,然后切换节点业务是否要停机。 不停机的过程中产生的数据如何进行同步?双库之间互相之间在不同的节点都产生数据,如何互相同步?都是相应存在相应问题的。 我们之前做过几次数据库迁移,基本上都是大动干戈,而且产生了很多问题。有收获,但是也没有能完全解决。 数据库迁移,首先你要考虑的就是,你从这个数据库迁到另一个库,你的目的是什么?解决压力问题,还是更换存储介质,还是什么原因?相互之间所有的功能都能兼容吗?前置的这都需要调研。之前我们只是从 5.5 MySQL 5.5 升到 5.升到 5.8,都中间遇到了不少问题。默认的一些配置,迁过去测试环境整个就瘫了,一些配置之前的语法是不支持的。你只有对默认配置进行重新设置,之前写的一些东西才能支持。这个过程中你要做完善的测试,各种功能你需要测试到,否则就会产生各种意外。 书归正题,平滑迁移,首先你要确定相互之间的兼容性,你迁移过后可以兼容,所有功能原地保留。然后,这个其实你可以在测试环境验证,先搭一个子环境,把整个数据迁过去,做一个验证。 确认没有问题,然后才走下一步,是否要用这个。 过程基本上分两步骤,历史数据全量同步。基本上会在把历史数据全量的迁移过去,然后保留近期的一个,从一个时间节点近期的一个数据,做保留,之前的数据全部同步过去,全量的同步过去,后边的数据开始做增量同步。这样确保时间的稳定。 数据同步由增量同步跟全量同步有很多工具。我们当时用的是阿里的 DataX,然后它有直接的支持。这个迁移的过程,你要考虑你迁移数据的时候对原有库的压力。当时我们设了很高的并发,导致直接就崩了。直接线上业务就崩溃了。然后很及时的处理了,但是业务也宕了大概有半个小时,产生了直接的问题影响,这是当时没有预料到的。 增量同步完了全量同步完了之后,开始做增量的双向同步。这时候你可能会做一些需要做一些灰度。微服务下,有些服务使用老库,有些服务使用新库。然后数据可能会存在延迟。双向增量同步就是 A 库 A 服务写到老库,A1 服务写到新库,然后 A1 跟 A 服务之间的数据同步。这个都是会产生不同的影响的,看你业务是否会有这方面影响。如果没有还好,有的话需要考虑双写,同一份数据写两个库,同时写。 库并行执行了一段时间之后,然后再考虑单写,把老库的数据写入移除,只写入新库。这时候老库还要保留一段时间,防止新库出现问题,没法有冗余,需要做一个冗余。 最后执行完成之后,看整个要对整个库新库做压力测试,看是否能满足正常的业务需求。这个过程其实是在灰度的过程中执行的。线上服务有一部分走老库,一部分走新库,然后对外提供查的支持跟写的支持。写当时是双写,然后一部分库读从老库读,一部分库从新库读,然后对比压力情况,服务是否能正常使用,以及效率是否存在问题,这个都是要进行考虑的。 整个迁移完成之后,老库还要保持保留一段时间,从一个时间节点记录好。然后后边的数据都新库读新库写,但是新库的数据同步回老库,确保老库的数据不丢失。在这个过程中,有迭代也不要改数据库,不要增表、改表,这个要和业务侧说清楚,因为在业务侧执行过程中,执行迁库这个操作,你再改表,会产生很多不可预料的问题。 整个流程大概就是这样。先验证,在测试环境验证服务的数据库新数据库的可用性,所有东西都兼容。然后做数据的增量、全量同步,全量同步完成之后做增量同步,增量同步之后验证灰度,验证部分服务的性能,在新库上是否可以正常使用。然后增量的过程中,同时新增数据的话做双写,同时往两个库写,保证业务的可用性。然后当整个服务迁完之后,用老库先保留一个月到两个月,然后增量的把新库的数据同步回老库,防止业务出现问题,无法正常使用。

点评

优点:- 有真实实战经验:踩过 MySQL 5.5→5.8 迁移的坑(配置兼容性、并发过高导致线上崩溃半小时) - 流程完整:兼容性验证 → 历史全量+近期增量 → 双向增量 → 双写灰度 → 单写切换 → 回滚期 - 分阶段清晰:每阶段有明确目标和验证手段 - 工具意识:用 DataX 做数据同步,知道其能力边界 - 回滚意识:老库保留 1-2 个月,新库反向同步到老库 - 沟通意识:迁移期间不改表,跟业务侧说清楚
不足:- 未提 CDC(Change Data Capture):Canal、Debezium、Maxwell 基于 binlog 的增量同步,无侵入 - 未提数据一致性校验工具:数据 diff、抽样对比、checksum - 未提 binlog 格式要求:ROW 格式 binlog、GTID(全局事务标识)保证不丢不重 - 未提切换时机细节:低峰期切换、准备回滚脚本、灰度比例控制(1%→10%→50%→100%)
补充:- **CDC 工具对比**:Canal(阿里,MySQL binlog)、Debezium(基于 Kafka Connect,多数据库)、Maxwell、CloudCanal(阿里云) - **一致性校验**: - 抽样对比:按 id 分段抽样校验行数 + checksum - 全量校验:离线跑 SQL 对比(`SELECT COUNT(*)`, `SELECT CRC32(GROUP_CONCAT(...))`) - 业务校验:关键业务指标(订单数、支付数)对齐 - **binlog 三要素**:ROW 格式(保证字段级变更可追踪)、`binlog_row_image=FULL`、GTID 保证不丢不重 - **灰度切换**:DNS/负载均衡权重调整(1%→10%→50%→100%),配合回滚脚本 - **反向同步**:新库→老库的 binlog 同步,确保回滚期数据不丢 - **回滚预案**:切回时间点、数据校验脚本、切回步骤、验证脚本

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察"零停机数据迁移"的完整方法论——从架构设计到工程落地到回滚预案。

**知识点拆解**:
1. **迁移目标明确**:先回答"为什么迁"(容量/性能/技术栈升级/拆分),不同目标方案不同
- 容量:垂直扩容 + 分库分表
- 性能:读写分离 + 缓存 + 分库分表
- 技术栈:MySQL→PolarDB、Oracle→MySQL
- 拆分:单体库→分库分表
2. **迁移阶段(六步法)**:
- **兼容性验证**:测试环境搭子库,跑全量 SQL 兼容性测试
- **全量同步**:DataX/mysqldump 迁移历史数据(保留近期 N 天增量)
- **增量同步**:CDC(Canal/Debezium)基于 binlog 追增量
- **双写灰度**:应用层双写 + 读写分离,逐步切流
- **单写切换**:只写新库,老库保留 + 反向同步
- **回滚期**:老库保留 1-2 个月,反向同步防回滚
3. **数据一致性保障**:
- **写入侧**:binlog ROW 格式 + GTID,保证不丢不重
- **校验侧**:抽样对比(按 id 分段)+ 业务指标对齐
- **切换侧**:DNS/负载均衡灰度(1%→10%→50%→100%)
4. **回滚预案**:
- 切回时间点(记录老库最后 binlog 位点)
- 反向同步(新库→老库 binlog 同步)
- 数据校验脚本(切回前校验新库数据完整性)
- 切回步骤(DNS/负载均衡权重调回 100% 老库)
5. **工程细节**:
- 迁移工具:DataX(全量)、Canal(增量)、DTS(阿里云)、CloudCanal
- 并发控制:限流避免打爆老库(DataX 的 `channel` 参数)
- 大表处理:分片同步(按主键分段)+ 断点续传
- 锁与事务:避免 DDL、长事务阻塞同步

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:MySQL binlog 格式(ROW/STATEMENT/MIXED)、GTID 机制、Canal 的 parse 算法
- **广度**:与 ElasticSearch 数据同步、ClickHouse 实时同步对比;Kafka 作为 CDC 中间层
- **应用**:分库分表迁移(ShardingSphere-JDBC)、跨云迁移、异构数据库迁移(Oracle→MySQL)

**延伸递进面试题**:
1. **Canal 是怎么做到基于 binlog 的增量同步的?**
- 解答要点:伪装成 MySQL slave,订阅 binlog 事件(Write_rows/Update_rows/Delete_rows);ROW 格式 binlog 记录行级变更,parse 后转换为 DDL/DML 事件推送给下游
2. **迁移过程中如何保证数据一致性?**
- 解答要点:三阶段校验:全量同步后抽样对比、增量同步后延迟监控、切换后业务指标对齐;GTID 保证不丢不重,唯一索引防重复写入
3. **如果迁移过程中新库挂了,怎么回滚?**
- 解答要点:老库保留 + 反向同步(新库→老库 binlog);DNS/负载均衡权重切回老库;切回前校验新库数据完整性(记录切回时间点)

第三部分:面试者参考回答(三步法口语化,300-400 字)

**【一句话结论】**
面试官您好,平滑迁移的核心是"双写 + 灰度 + 反向同步"三件套,本质是把"一次性切换"拆成"渐进式切流",每一步都可验证、可回滚。

**【三点展开】**
第一,先验证再迁移:搭测试环境子库,跑全量 SQL 兼容性测试(特别是 MySQL 5.7→8.0 这种大版本升级,语法/配置/驱动都可能不兼容)。我们踩过坑:MySQL 5.5 升到 5.8,测试环境整个瘫了,因为默认配置不兼容旧语法,重新调配置才恢复。
第二,迁移分六步走:兼容性验证 → 全量同步(DataX,限流防打爆老库)→ 增量同步(Canal/Debezium 基于 binlog)→ 双写灰度(应用层双写 + 读写分离,DNS 权重 1%→10%→50%→100%)→ 单写切换(只写新库)→ 回滚期(老库保留 1-2 个月)。每步都有校验:全量后抽样对比、增量后延迟监控、切换后业务指标对齐。
第三,回滚预案是兜底:老库保留 + 新库→老库反向同步(binlog 同步),切回前校验新库数据完整性,DNS/负载均衡权重秒级切回老库。迁移期间跟业务侧约定不改表(增表/改表会破坏同步),这是踩坑后血泪教训。

**【一句边界】**
当然,如果是小表(< 100 万行)一次性迁移,直接停机 + mysqldump 就行,不必走这套流程——KISS 原则,能用简单方案解决的别引入 Canal/DTS 这种复杂组件。

#8 服务器有多个节点,线上出现用户进入缓慢,监控服务器cpu和缓存没有什么压力,可以从哪些方面排查? high practice

题目

服务器有多个节点,线上出现用户进入缓慢,监控服务器cpu和缓存没有什么压力,可以从哪些方面排查?

我的回答

从几个层面去排查,整个链路包括这么几个节点,用户->dns->服务网关->业务服务->缓存->数据库;我们可以从每个节点去对应进行排查; 首先如果是大规模的,那么基本上是服务端的问题。如果是个别反馈,那么可能是用户个人的问题。先从用户端开始查,这个就是看大量反馈还是少量的反馈。少量反馈先从用户端开始查。然后根据用户的 ID 信息查整个业务的调用链路,找到对应的链路追踪,比如 Skywalking。然后根据业务的用户的请求,找到对应的一个请求登录的地址。然后对应的去看整个调用链路的请求时长,是在业务侧整个链路某个节点出现了缓慢,还是说进入之后返回正常,只是用户侧感觉缓慢,这时候可能是用户的网络问题。 我们可以看一下用户和我们的网络所处环境,用户的 IP、用户的区域,对应是否有对应的区域服务器,是否存在网络比较高的延迟。我们可以,如果是 B 端的用户,我们可以找 B 端用户去对应的沟通,连接他们的网络,通过网络之间的转移,确认一下网络延迟是多少,从他们的网络到我们的服务,网络延迟是多少。 首先看一下他们在他们的位置 ping 一下我们的服务的域名,确认一下请求延迟是多少。如果延迟比较高,那是否确认是网络的问题。然后如果域名 ping 有问题,但是 ping IP 没有问题,那么是否是对应的 DNS 解析有问题?如果客户侧确认他们那没问题,而是链路中某个节点出现问题,比如业务健全或者是数据库缓存之类的存在问题,我们对应的再去排查处理对应的问题。 在整个链路节点上,看网关转发过程后,网关、DNS 到网关之间,网关转发的过程是否存在问题。因为看业务服务监控,CPU 跟缓存没有,内存没有压力,那么证明标准链路服务上是没什么问题的。然后看一下业务日志跟 Skywalking 日志,看具体是什么节点出现问题。如果是数据库层面的某些问题,去确认数据库是否出现其他操作导致数据库链接缓慢、链接被占之类的,或者是业务侧业务侧的链接、网络链接池之类的被占满。或者是服务器的有其他的开销,导致服务器的其他压力,使得用户访问服务器的时候,这个服务器存在问题。 基本上来说,就是从这几个对应的点去排查缓存,是否缓存出现丢失,然后大量的访问到数据库。但是或者是某一个请求的接口出现大范围的延迟,由于接口的阻塞导致整个业务链路的缓慢,这种也是有可能的。我当年碰见过某一个接口,因为有大量的查询。导致整个业务瘫痪,把所有数据库连接池打满了。噢,最终所有人都无法正常访问。这时候如果那个接口不是特别重要,或者是不影响主链路,可以先把接口临时下线掉,然后保证业务的正常运行。

点评

优点:- 排查思路非常系统:用户 → DNS → 网关 → 业务服务 → 缓存 → 数据库,分层下钻 - 分规模判断:大规模从服务端查,个别反馈从用户端查 - 有具体工具:Skywalking 链路追踪、ping 域名 vs ping IP 区分 DNS 问题 - 区分了"服务端问题"和"用户侧感觉缓慢"(网络问题) - 有真实案例:某接口大量查询打满数据库连接池导致整个业务瘫痪 - 有处置意识:非主链路接口临时下线,保证业务正常运行 - 有全局视角:从用户端 → 网络 → 服务端 → 数据库/缓存,层层下钻
不足:- 未提 DNS 层排查细节:DNS TTL、DNS 解析失败、DNS 服务商状态、DNS 劫持 - 未提 CDN/负载均衡层:CDN 节点故障、LB 会话保持、连接数打满 - 未提 JVM 层排查:FullGC 频繁(CPU 不高但 RT 高)、Tomcat 工作线程耗尽、连接池耗尽 - 未提网络层细节:丢包率(ping/iperf)、TCP retransmit(netstat -s)、traceroute
补充:- **DNS 层**:dig/nslookup 查解析、TTL 检查、DNS 服务商状态页、DNS 劫持排查 - **CDN/LB 层**:CDN 节点健康检查、LB 会话保持、连接数打满(`ss -s`) - **JVM 层**:`jstat -gc` 看 GC 频率、Tomcat `maxThreads` 耗尽、连接池(HikariCP)耗尽 - **网络层**:`ping`、`mtr`、`traceroute`、`iperf3`、`netstat -s` 看 retransmit - **数据库层**:SLOWLOG、锁竞争(`SHOW ENGINE INNODB STATUS`)、连接数(`max_connections`) - **中间件层**:MQ 消费堆积、第三方 API 超时、序列化性能 - **降级手段**:Resilience4j 熔断 + 限流 + 临时下线非核心接口

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察"分布式链路排查"的完整方法论——从现象(缓慢)到根因(哪一层阻塞)的分层下钻。

**知识点拆解**:
1. **链路分层(6 层模型)**:
- 用户端:客户端网络、DNS 解析、CDN 命中
- 网络层:丢包、延迟、TCP retransmit、跨地域
- 入口层:LB/Nginx 转发、连接数、会话保持
- 应用层:JVM(GC/线程池)、业务逻辑、序列化
- 缓存层:Redis 超时、缓存穿透、热点 key
- 数据层:慢查询、锁竞争、连接池耗尽、主从延迟
2. **规模判断**:
- 大规模:服务端问题(配置错误、依赖故障、流量突增)
- 个别:用户端问题(网络、设备、地域)
3. **关键排查手段**:
- **链路追踪**:Skywalking/Zipkin/Jaeger,看每个 span 的耗时
- **网络诊断**:`ping`/`mtr`/`traceroute`/`iperf3`/`netstat -s`
- **JVM 诊断**:`jstat -gc`、`jmap -histo`、Tomcat 工作线程数
- **数据库诊断**:SLOWLOG、`SHOW PROCESSLIST`、`SHOW ENGINE INNODB STATUS`
4. **真实案例复盘**:某接口大量查询打满数据库连接池 → 所有业务瘫痪;处置:临时下线非核心接口 + 加缓存 + 限流
5. **降级三件套**:
- **熔断**:Resilience4j CircuitBreaker,连续失败后短路
- **限流**:Sentinel/Resilience4j RateLimiter,保护核心链路
- **临时下线**:非核心接口直接返回空/降级文案

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:链路追踪的 span 采样率、JVM GC 对 RT 的影响、TCP retransmit 的代价
- **广度**:与 Chaos Engineering(故障注入)的对比;全链路压测(影子表)
- **应用**:生产监控(Prometheus + Grafana + Skywalking 三件套)、告警分级

**延伸递进面试题**:
1. **链路追踪的采样率怎么设置合理?**
- 解答要点:生产环境 1%-10%(高流量场景),核心链路 100%;采样用 head-based 或 tail-based,tail-based 按 trace 质量采样更精确
2. **CPU 不高但 RT 高,可能是哪些原因?**
- 解答要点:FullGC STW(用户态 CPU 不高)、线程池打满(等待队列堆积)、锁竞争(BLOCKED 状态多)、IO 等待(D 状态进程)
3. **连接池耗尽怎么排查和预防?**
- 解答要点:`SHOW PROCESSLIST` 看连接来源、监控 `active connections` 指标、连接池参数(`maxActive`/`maxWait`)、慢查询治理(连接池耗尽通常是慢查询引起)

第三部分:面试者参考回答(三步法口语化,300-400 字)

**【一句话结论】**
面试官您好,"CPU 不高但 RT 高"是典型的"非 CPU 密集型瓶颈",我按"用户端 → 网络 → 入口 → 应用 → 缓存 → 数据"六层排查,第一手证据是 Skywalking 链路追踪的 span 耗时分布。

**【三点展开】**
第一,先分规模:大规模反馈是服务端问题(配置错误、依赖故障、流量突增),个别反馈是用户端问题(网络、设备、地域)。用 Skywalking 查具体用户请求的 span 耗时,定位是哪个节点慢——是 DNS 解析慢、LB 转发慢、业务逻辑慢、还是数据库慢。
第二,按六层下钻:DNS 层用 `dig` 查解析时间和 TTL;网络层用 `ping`/`mtr` 查丢包和延迟,`netstat -s` 看 retransmit;入口层看 LB 连接数和会话保持;应用层用 `jstat -gc` 看 GC 频率,Tomcat `maxThreads` 是否耗尽;缓存层看 Redis SLOWLOG 和连接池;数据层看 SLOWLOG 和锁竞争。
第三,处置有优先级:先止血(熔断/限流/临时下线非核心接口),再定位(链路追踪 + 监控),最后根治(慢查询优化、连接池扩容、缓存预热)。我踩过一个坑:某接口大量查询打满数据库连接池,导致整个业务瘫痪——处置是临时下线该接口 + 加缓存 + 限流,事后复盘加连接池监控和慢查询告警。

**【一句边界】**
当然,如果是跨地域网络问题(用户在国内、服务在海外),那是 CDN 和边缘节点的问题,光靠服务端排查解决不了——得靠 CDN 部署和 Anycast 路由优化。

#9 假设你收到告警,某个消费组消息堆积从1000涨到100万,你会按照什么步骤排查?请模拟一次线上排查过程。 high practice

题目

假设你收到告警,某个消费组消息堆积从1000涨到100万,你会按照什么步骤排查?请模拟一次线上排查过程。

我的回答

首先确认整个链路是怎么回事。我们以 Kafka 为例,整个消息的链路应该是,生产者生成->broker->消费者消费; 出现堆积主要可能性就只有两个,1 是生产者创建的消息出现波动创建了大量等消息,下有消费者消费能力没有能提高,这时候按原本消费的能力去消费,就产生了大量的堆积。 这里要处理的,就是消费者方面,要进行扩容也好,提升消费能力也好,主要的是要把消费能消费的能力提上来,快速的把这些消息消费掉。如果是有序的消息,就只能控制消费者了。的提升自身能力。如果是无序的消息,可以考虑把消息先转到另一个 Topic 上。然后再缓慢地进行分流处理。 2 是消费者出现问题,生产者的生产速度没有变动,但是消费者的消费能力出现大幅降低,导致原有的消息进行堆积。这时候要排查的是消费者为什么出现问题,是出现了业务侧的数据库能力异常,还是消费者本身的 ZVM 或者是其他的消费的整个消费者出现了卡顿。然后消费者是否出现了特殊的情况,比如大量的 GC,或者是网络中断之类的情况导致消息积压。这时候就要针对的处理消费者自身的问题,可以先把所有的 Topic 消息转移到一个新的消息 Topic 上,然后进行临时处理,然后去处理消费者上的问题。处理好之后,恢复正常的消费能力,再慢慢的消费积压的消息。从新的 Topic,原来的备用的 Topic 上,把它的积压的消息消费掉,以及顺序的消费新的消息。 要区分是生产者还是消费者的问题,可以通过监控看生产者跟消费者的 TPS 具体是怎么样的。如果生产者的 TPS 生产特别变动特别大,提高特别高,那就是生产端出现了大的波峰的请求。如果是消费者的 TPS 变低了,那就是消费者自身出现了问题,对应的再进行上述的排查和处理

点评

优点:- 抓到了核心二分:生产者波动 vs 消费者问题 - 有监控指标意识:用 TPS 区分生产者 vs 消费者问题 - 分场景处置:生产端波动扩容消费者、消费者问题临时 Topic 分流 - 区分有序/无序消息的不同处置:有序只能提消费能力,无序可分流 - 有根因排查方向:业务侧 DB 异常、JVM GC、网络中断 - 有处置策略:临时 Topic 转移消息、慢慢消费积压
不足:- 未提 Lag 分布分析:`kafka-consumer-groups.sh --describe` 看每个分区的 Lag(均匀 vs 集中) - 未提 Kafka 关键配置:`max.poll.records`、`max.poll.interval.ms`、`session.timeout.ms` - 未提"消费者数不能超过分区数"的关键陷阱 - 未提 Rebalance 导致堆积(消费者崩溃/扩缩容导致 offset 回退) - 未提事后复盘:消费能力压测、告警阈值、幂等 + 死信队列兜底
补充:- **Lag 分布**:均匀增长 = 消费者整体慢;集中某分区 = 特定分区消息异常;突然从 0 到 100 万 = Producer 突发或 Consumer 组崩溃 - **Kafka 关键配置**:`max.poll.records`(单次 poll 拉取数)、`max.poll.interval.ms`(超时未 poll 会被踢出消费组)、`session.timeout.ms`(心跳超时) - **消费者数上限**:消费者数 > 分区数时多出的消费者空转,扩容无效 - **Rebalance 陷阱**:Rebalance 期间整个组暂停消费,几秒到几十秒的堆积是正常的 - **事后复盘**:消费能力压测(单消费者 TPS 上限)、堆积告警(lag > N 分钟)、幂等 + 死信队列(DLQ)兜底 > ℹ️ 完整讲解见上方首次记录(2026-09-22)

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察**系统性排查能力**——不是知识点的记忆,而是从"收到告警"到"定位根因"到"制定方案"的完整工程思维。

**知识点拆解**:
1. **第一步:确认现象**(10 分钟内)
- 用 `kafka-consumer-groups.sh --bootstrap-server ... --describe --group <group>` 查看每个分区的 Current-Offset / Log-End-Offset / Lag
- 观察 Lag 分布:均匀 vs 集中
- 观察堆积增长速度:线性 vs 指数
2. **第二步:分场景定位**
- **场景 A:堆积均匀增长**
- 消费者整体处理慢(下游 DB 慢、外部 API 慢)
- Consumer 端线程池打满、GC 频繁、CPU 打满
- **场景 B:堆积集中在某分区**
- 该分区消息体异常大
- 该分区消费逻辑死锁或异常
- 消费者组 Rebalance 期间,某分区未被接管
- **场景 C:堆积突然从 0 到 100 万**
- Producer 侧突发大量消息(下游积压、批量任务、重试风暴)
- Consumer 组突然崩溃或全部下线
- Rebalance 触发导致整个组暂停
3. **第三步:常见根因分类**
- **Consumer 侧**:
- 下游依赖(DB 慢查询、外部 API 超时、Redis 阻塞)
- JVM 问题(FullGC、OOM、CPU 100%)
- 业务逻辑(死循环、幂等去重表锁竞争、大事务)
- 配置问题(`max.poll.records` 太大、`max.poll.interval.ms` 超时)
- 网络问题(与 Broker 连接不稳定、DNS 解析失败)
- **Producer 侧**:
- 突发大量消息(大促、批量任务、下游积压补偿)
- 重试风暴(Consumer 消费失败反复重试)
- **Kafka 集群侧**:
- Broker 磁盘满、副本同步慢
- Controller 频繁切换、Leader 频繁迁移
- ZooKeeper / KRaft 异常
4. **第四步:解决方案分级**
- **P0(数据不能丢)**:扩容消费者(注意分区数上限)+ 限流生产者 + 下游扩容/优化 + 跳过已处理消息(消费幂等保证)
- **P1(可以延迟)**:降级消费逻辑、临时跳过非核心消息
- **P2(可以丢弃)**:跳过堆积消息(`--to-latest` 或修改 offset)
5. **关键陷阱**:
- **消费者数 > 分区数**:多出的消费者空转,扩容无效
- **Rebalance 期间**:整个组暂停消费,堆积是正常的(几秒到几十秒)
- **幂等设计**:堆积恢复时可能重复消费,消费端必须幂等
- **分区扩容**:Kafka 分区只能增不能减,扩容会改变消息路由

> 从「确认现象」→「分场景定位」→「根因分类」→「分级方案」,构成完整排查链条。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:Lag 分布分析、`kafka-consumer-groups.sh` 参数、Kafka Manager / Prometheus 监控指标
- **广度**:与 RocketMQ 堆积排查对比、与数据库慢查询排查类比、与分布式系统故障隔离的联系
- **应用**:如何设计堆积告警、如何做自动化预案、如何做日常演练

**延伸递进的顺序排查流程(生产实战)**:
1. `kafka-consumer-groups.sh --describe --group <group>` 看 Lag 分布
2. 看消费者进程是否存活、Rebalance 频率
3. 看消费者 Pod 状态(OOM、CPU、GC 日志)
4. 看下游依赖(DB 慢查询、外部 API 响应时间)
5. 看 Kafka Broker 状态(磁盘、副本同步、Controller 稳定性)
6. 看 Producer 侧(是否突发大量消息)

**延伸递进面试题**:
1. **堆积 100 万消息,业务方要求 10 分钟内追平,怎么做?**
- 扩容消费者(注意分区数上限)、优化消费逻辑、跳过非核心消息、降级下游依赖
2. **堆积集中在某一个分区,怎么排查?**
- 检查该分区消息 Key 是否特殊、消息体是否异常、消费逻辑是否有死锁、消费者是否被分配到该分区
3. **Rebalance 频繁导致堆积,怎么解决?**
- 用 CooperativeStickyAssignor、调大 `session.timeout.ms` 和 `max.poll.interval.ms`、减小 `max.poll.records`

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,收到堆积告警后,我会按"确认现象 → 分场景定位 → 找根因 → 分级方案"这四步走。

第一步是**确认现象**。我用 `kafka-consumer-groups.sh --describe --group <group>` 看每个分区的 Lag 分布。堆积 100 万到底是均匀分布还是集中在某个分区,这决定了后续排查方向。同时看堆积增长速度,是线性还是指数增长,指数增长往往是正反馈问题。

第二步是**分场景定位**:

**如果是堆积均匀增长**,说明消费者整体处理慢,重点查 Consumer 侧:下游 DB 慢查询、外部 API 超时、线程池打满、JVM FullGC、业务逻辑死锁、幂等去重表锁竞争。同时看 Rebalance 频率,如果 Rebalance 频繁,整个组会周期性暂停消费。

**如果是堆积集中在某个分区**,重点查该分区:消息 Key 是否特殊、消息体是否异常大、消费逻辑是否有死锁、消费者是否被正确分配到该分区。

**如果是堆积突然从 0 到 100 万**,重点查:Producer 侧是否突发大量消息(下游积压、批量任务、重试风暴)、Consumer 组是否突然崩溃、Rebalance 是否触发。

第三步是**根因分类**:

**Consumer 侧**:下游依赖问题、JVM 问题(FullGC、OOM)、业务逻辑问题、配置问题(`max.poll.records` 太大)、网络问题。

**Producer 侧**:突发大量消息、重试风暴。

**Kafka 集群侧**:Broker 磁盘满、副本同步慢、Controller 频繁切换。

第四步是**分级方案**:

**数据不能丢(P0)**:扩容消费者(注意**分区数上限**,消费者数不能超分区数)、限流生产者、下游扩容或优化、消费端保证幂等。

**可以延迟(P1)**:降级消费逻辑、临时跳过非核心消息。

**可以丢弃(P2)**:跳过堆积消息(`--to-latest` 或修改 offset)。

这里有几个坑要注意:第一,消费者数超过分区数是无效的,扩容前先确认分区数;第二,Rebalance 期间整个组会暂停消费,几秒到几十秒的堆积是正常的;第三,堆积恢复时可能重复消费,消费端必须做幂等;第四,扩容分区会改变消息路由,需要评估对顺序性的影响。

日常还要做好堆积告警、自动化预案、定期演练,让问题在早期被发现。

---

#10 设计一个支持万人在线的语音聊天室,如何保证房间状态(如麦位信息、在线人数)的实时一致性? high practice

题目

设计一个支持万人在线的语音聊天室,如何保证房间状态(如麦位信息、在线人数)的实时一致性?

我的回答

直播间这个东西本身就是一个独立的个体。然后直播间状态、麦位信息、在线人数这些,属于直播间个人的、直播间的属性,独立的进行维护。当用户上线或者申请麦位的时候,检查直播间属性上的值,然后进行更新。这个更新使用 Redis 更新,然后同步的展现就可以了。 用户上线的时候,对应的用户在线人数进行原子增,就可以更新上线在线的人数。当用户下线的时候,对应的更新对应的 Redis 就可以实现了。有时候为了保证业务更新,可能会用到 MQ。当用户上线下线的时候,发对应的消息到 MQ,然后监听 MQ 的变更,对应的去更新房间状态,来达到数据的最终一致性; 万人直播间的数据更新,直接用 Redis 本身就可以,并不一定需要 MQ 再去异步处理。在 MQ 异步处理可以提升它更高的兼容性,但是 Redis 本身万人直播间的更新就可以扛得住。新用户获取信息的时候直接获取直播间的信息,这个信息的更新通过长连接,进行对应的实时更新,或者使用服务端推送给客户端进行更新;但是不能使用客户端轮询,这样的客户端轮询产生的并发会让业务服务器崩溃

点评

优点:- 抓到了核心:直播间是独立个体,状态属性独立维护 - 数据更新用 Redis 原子增(在线人数) - 明确反对客户端轮询(会让业务服务器崩溃)——这是正确的工程判断 - 区分了 Redis 同步 vs MQ 异步的适用场景(最终一致性 vs 强一致) - 有架构意识:长连接/服务端推送是实时同步的正确方向
不足:- 未提分布式广播机制:用户长连接挂在不同服务器时,怎么广播状态变更(Redis Pub/Sub 或独立消息总线) - 未提麦位管理的复杂性:麦位数量限制、排队机制、麦位冲突(多用户同时申请)、自动释放 - 未提在线人数统计细节:心跳保活(30s)、超时清理、分布式会话 - 未提具体技术选型:WebSocket 网关(Netty)、Redis Pub/Sub、IM 系统(融云/环信)
补充:- **广播机制**:用户连接挂在 WS 网关 A,状态变更发生在服务器 B → 用 Redis Pub/Sub 或 Kafka 广播到所有 WS 网关 - **麦位管理**: - 麦位数量有限(4-10 个),用 Redis Set 存当前麦位 - 申请麦位用 `SADD + 唯一索引` 防并发(`SETNX` 占位) - 排队机制:申请失败进入排队列表(`List`),有空位时按顺序分配 - 自动释放:用户下线/心跳超时触发释放 - **在线人数**: - 心跳保活:客户端 30s 心跳,服务器 60s 超时清理 - 分布式会话:用户连接挂服务器 A,状态存 Redis,任意服务器可读 - 计数器:`INCR/DECR` 原子操作 - **实时同步协议**: - WebSocket(全双工、低延迟) - SSE(Server-Sent Events,单向推送) - 长轮询(兜底,延迟高) - **一致性模型**: - 强一致:写 Redis + 同步广播(延迟 100ms 内) - 最终一致:写 Redis + 异步广播(可能短暂不一致,秒级)

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察"分布式实时系统"的设计能力——状态存储、广播机制、一致性模型三者的权衡。

**知识点拆解**:
1. **系统架构分层**:
- **接入层**:WebSocket 网关(Netty),用户长连接挂在这里
- **业务层**:房间状态服务(麦位、在线人数、聊天消息)
- **存储层**:Redis(状态 + 计数器 + 麦位集合)
- **广播层**:Redis Pub/Sub 或 Kafka(跨服务器状态同步)
2. **状态存储设计**:
- 在线人数:`INCR/DECR` 原子操作,key 为 `room:{roomId}:online`
- 麦位集合:`SET` 存当前麦位用户列表,key 为 `room:{roomId}:mics`
- 麦位排队:`LIST` 存排队用户,key 为 `room:{roomId}:mics:queue`
- 用户状态:`HASH` 存用户详细信息,key 为 `room:{roomId}:user:{userId}`
3. **广播机制**:
- **Redis Pub/Sub**:轻量、无持久化,适合状态广播(延迟 < 10ms)
- **Kafka**:可靠、有持久化,适合聊天消息(延迟 < 100ms)
- **WebSocket 网关集群**:多 WS 网关订阅 Pub/Sub,收到消息后推送给本地连接
4. **麦位管理的复杂性**:
- **并发申请**:多用户同时申请同一麦位,用 `SETNX` 或 Lua 脚本保证原子性
- **排队机制**:申请失败进队列,空位时按顺序分配
- **自动释放**:用户下线/心跳超时触发释放,释放后通知下一个排队用户
- **麦位转让**:用户主动让麦,释放后分配给下一个人
5. **在线人数统计**:
- **心跳保活**:客户端 30s 心跳,服务器 60s 超时清理(`EXPIRE` 自动过期)
- **分布式会话**:用户连接挂服务器 A,状态存 Redis,任意服务器可读
- **计数器维护**:`INCR`/`DECR` 原子操作,避免并发错乱
6. **实时同步协议**:
- **WebSocket**:全双工、低延迟(< 100ms)、生产环境首选
- **SSE**:单向推送、HTTP 兼容、延迟略高
- **长轮询**:兜底方案、延迟高(10s)、连接开销大
7. **一致性模型**:
- **强一致**:写 Redis + 同步广播(所有服务器同时收到),延迟 100ms 内
- **最终一致**:写 Redis + 异步广播(通过 MQ),可能短暂不一致,秒级收敛

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:Redis Pub/Sub 的实现原理、Kafka 的分区与消费者组、WebSocket 的心跳机制
- **广度**:与 IM 系统(融云/环信/腾讯云 IM)对比;与弹幕系统(WebSocket + 分片)对比
- **应用**:实时弹幕、在线游戏房间、协同编辑(CRDT)、直播互动

**延伸递进面试题**:
1. **万人在线的 WebSocket 连接怎么横向扩展?**
- 解答要点:WS 网关无状态化(状态放 Redis),多副本部署;用 Redis Pub/Sub 广播状态变更;客户端通过域名访问,LB 做 WebSocket 路由
2. **麦位申请高并发怎么保证原子性?**
- 解答要点:Redis `SETNX` 占位(key 为 `mic:{micId}`),失败则排队;Lua 脚本保证"检查 + 占位"的原子性;空位时 `BLPOP` 阻塞等待下一个申请
3. **强一致和最终一致怎么选型?**
- 解答要点:麦位状态用强一致(同步广播),聊天消息用最终一致(MQ 异步);核心是"用户能感知到的实时"其实是"准实时"(秒级),不必追求毫秒级强一致

第三部分:面试者参考回答(三步法口语化,300-400 字)

**【一句话结论】**
面试官您好,万人在线的核心是"状态存 Redis + 广播走 Pub/Sub + 长连接走 WebSocket",麦位管理用 `SETNX + Lua` 保证原子性,在线人数用心跳保活 + 超时清理。

**【三点展开】**
第一,架构分层:WebSocket 网关(Netty)负责用户长连接,无状态化便于横向扩展;房间状态服务负责业务逻辑,状态全部存 Redis(在线人数 `INCR/DECR`、麦位 `SET`、排队 `LIST`);Redis Pub/Sub 负责跨服务器广播状态变更——用户连在 WS 网关 A,状态变更发生在服务器 B,B 发消息到 Pub/Sub,A 订阅后推送给本地连接。
第二,麦位管理有并发陷阱:多用户同时申请同一麦位,用 `SETNX mic:{micId} userId` 原子占位,失败则进排队队列(`LIST`);空位时 `BLPOP` 阻塞等待下一个申请;用户下线/心跳超时触发释放,释放后通知排队用户。Lua 脚本保证"检查麦位空 + 占位"的原子性,避免竞态。
第三,在线人数用心跳保活:客户端 30s 心跳,服务器 60s 超时清理(`EXPIRE` 自动过期,避免死连接);新用户进房直接读 Redis 拿当前状态,之后通过 WebSocket 订阅房间状态变更,秒级感知。不能用客户端轮询——万人 QPS 会打爆服务器。强一致(麦位状态同步广播,延迟 < 100ms)和最终一致(聊天消息走 MQ,秒级收敛)按场景选型。

**【一句边界】**
当然,如果是百万级并发(YY 语音、KOOK),自建成本高,直接用 IM 云(融云/环信/腾讯云 IM)+ WebSocket 自建弹幕系统更划算——核心是"麦位状态自建,聊天消息用云"。

📚 查看全部面试记录