java面试宝典

一、计算机基础

1. 计算机网络

1. OSI 七层模型和 TCP/IP 四层模型分别是什么?它们的映射关系是怎样的?

OSI 七层是:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。

TCP/IP 四层通常是:网络接口层、网络层、传输层、应用层。

大致映射关系是:

  • OSI 的物理层 + 数据链路层 ≈ TCP/IP 网络接口层
  • OSI 网络层 ≈ TCP/IP 网络层
  • OSI 传输层 ≈ TCP/IP 传输层
  • OSI 的会话层 + 表示层 + 应用层 ≈ TCP/IP 应用层

面试里重点不是死背层数,而是要知道:HTTP/HTTPS/DNS 属于应用层,TCP/UDP 属于传输层,IP 属于网络层。

2. HTTP、HTTPS、FTP、DNS、SSH、WebSocket、RPC 分别工作在什么层?常见用途是什么?

它们都属于应用层协议或应用层上的通信方式。

  • HTTP:Web 资源传输
  • HTTPS:在 HTTP 基础上加 TLS,提供加密和身份认证
  • FTP:文件传输
  • DNS:域名解析
  • SSH:远程安全登录和命令执行
  • WebSocket:建立持久双向通信
  • RPC:远程过程调用,常用于服务间通信

注意:RPC 不是一个单一协议名,更像一种调用模式,底层可以跑在 TCP、HTTP/2 等之上。

3. HTTP 和 RPC 的核心区别是什么?在内部服务调用中为什么很多公司倾向于 RPC?

HTTP 更偏通用资源访问协议 ,标准化强、跨语言跨平台好;RPC 更偏像本地方法一样调远程服务 ,通常更关注序列化效率、接口定义、连接复用、服务治理。

内部服务很多用 RPC,是因为它通常:

  • 序列化更紧凑
  • 协议开销更低
  • 更容易做接口约束、超时、重试、负载均衡、熔断
  • 对内网调用更友好

但对外开放接口,HTTP 仍然更通用。

4. WebSocket 和 HTTP 长轮询有什么区别?适合什么场景?

WebSocket 是在建立连接后保持长连接、双向通信

长轮询本质还是 HTTP,请求挂住一段时间,返回后客户端再发下一次。

WebSocket 更适合:

  • 即时聊天
  • 实时推送
  • 在线协同
  • 实时行情

长轮询实现更简单、兼容性好,但连接效率和实时性通常不如 WebSocket。

5. DNS 解析完整流程是什么?本地缓存、系统缓存、递归查询、迭代查询分别是什么?

一般流程是:

浏览器缓存 → 系统缓存 / hosts → 本地 DNS 解析器 → 递归 DNS 服务器 → 根域名服务器 → 顶级域名服务器 → 权威 DNS 服务器 → 返回 IP。

  • 本地缓存:浏览器、OS、应用层自己存过的结果
  • 递归查询:客户端只问一个 DNS 服务器,由它帮你一路查到底
  • 迭代查询:DNS 服务器一步步告诉你"下一站去问谁"

面试里要抓住重点:客户端通常更像发递归请求,DNS 服务器之间常走迭代查询。

6. TCP 和 UDP 的核心区别是什么?分别适合哪些业务场景?

TCP 是面向连接、可靠传输、按序到达、有流量控制和拥塞控制 ;UDP 是无连接、尽力而为、开销更小、时延更低

  • TCP:Web、数据库连接、RPC、文件传输
  • UDP:音视频、游戏、实时语音、DNS、对时服务

一句话:要可靠和顺序,用 TCP;更看重时延和实时性,用 UDP。

7. TCP 三次握手为什么不是两次,也不是四次?

三次握手的核心目的是让双方都确认两件事:

  1. 你能发我能收
  2. 我能发你能收

两次不够,因为服务端无法确认客户端是否收到了自己的 SYN-ACK;三次正好完成双方收发能力确认。四次可以,但没有必要,纯增加一次 RTT。

8. TCP 四次挥手为什么通常需要四次?TIME_WAIT 为什么存在?

TCP 是全双工,双方关闭连接要分别关闭各自的发送方向,所以常见是四次挥手。

TIME_WAIT 主要有两个作用:

  1. 确保最后一个 ACK 能让对方收到
  2. 让旧连接中的延迟报文在网络中自然消失,避免污染新连接

所以 TIME_WAIT 多不一定是故障,它是 TCP 可靠关闭机制的一部分。

9. TCP 的可靠传输是如何实现的?

主要靠这些机制:

  • 序列号:保证数据有序
  • ACK 确认:确认哪些数据已收到
  • 重传:丢包后重新发送
  • 校验和:发现传输损坏
  • 滑动窗口:控制可发送范围
  • 流量控制:防止接收端被压垮
  • 拥塞控制:防止网络被打爆

也就是说,TCP 的"可靠"不是绝对不丢,而是丢了可发现、乱了可重排、没到可重传。

10. 什么是滑动窗口?发送窗口和接收窗口分别解决什么问题?

滑动窗口就是:发送方不用等每个包单独确认后再发下一个,而是能在窗口允许范围内连续发送。

  • 发送窗口:限制当前最多可以"未确认地发出去多少数据"
  • 接收窗口:告诉发送方"我现在还能接收多少数据"

它解决的是吞吐问题,避免每发一个包都停下来等确认。

11. 流量控制和拥塞控制的区别是什么?
  • 流量控制:解决接收端处理不过来的问题,本质是端到端
  • 拥塞控制:解决网络路径本身承受不了的问题,本质是全局网络层面

一句话:
流量控制看对端,拥塞控制看网络。

12. TCP 的慢启动、拥塞避免、快速重传、快速恢复分别是什么?
  • 慢启动:刚开始不要猛发,先逐步试探可用带宽
  • 拥塞避免:窗口增长变缓,防止网络过载
  • 快速重传:收到多个重复 ACK,提前判断丢包并重传
  • 快速恢复:丢包后不是一刀切回很小窗口,而是适度回退后继续发

这套机制的本质是:既想跑快,又不能把网络冲崩。

13. 为什么有时候服务端压力不大,但客户端依然会出现超时或重传?

因为问题不一定在服务端 CPU。还可能在:

  • 网络抖动、丢包、重传
  • 客户端到服务端路径上的拥塞
  • 四层负载均衡设备异常
  • 服务端 accept 队列或连接队列拥塞
  • 服务端应用线程池、连接池卡住,表面 CPU 不高
  • DNS 或 TLS 建连慢

所以不能看到"服务端负载低"就排除服务端链路问题。

14. HTTPS 建立连接的完整过程是什么?

简化理解:

  1. 先建立 TCP 连接
  2. 再进行 TLS 握手
  3. 客户端验证服务端证书
  4. 双方协商密码套件和会话密钥
  5. 后续 HTTP 数据用对称加密传输

也就是说:HTTPS = HTTP + TLS,而 TLS 通常先跑在 TCP 之上。

15. 对称加密、非对称加密、数字证书、数字签名分别解决什么问题?
  • 对称加密:解决高效加密传输问题
  • 非对称加密:解决密钥交换和身份认证问题
  • 数字证书:把"公钥"和"域名/身份"绑定起来
  • 数字签名:验证消息是谁发的、有没有被篡改

面试里最容易答散,建议记成:
对称加密管速度,非对称加密管身份和密钥交换,证书管公钥可信,签名管来源和完整性。

16. 为什么 HTTPS 不直接全程使用非对称加密?

因为非对称加密计算成本高、速度慢,不适合大量业务数据全程加密。

所以 HTTPS 一般是:握手阶段用非对称能力做认证和密钥协商,数据传输阶段用对称加密保证效率。

17. HTTP/1.1、HTTP/2、HTTP/3 分别解决了什么问题?
  • HTTP/1.1:支持持久连接,减少重复建连
  • HTTP/2:引入二进制分帧、多路复用、头部压缩,改善并发请求效率
  • HTTP/3:基于 QUIC/UDP,进一步降低建连时延,改善传输层队头阻塞问题

一句话:
1.1 解决频繁建连,2 解决应用层并发效率,3 进一步解决底层传输时延和阻塞。

18. HTTP/2 的多路复用是否彻底解决了队头阻塞?

没有彻底解决。

HTTP/2 解决了应用层 一个连接里多个请求串行的问题,但底层还是 TCP;一旦 TCP 某个包丢了,后续数据仍会受影响,所以仍有传输层队头阻塞。HTTP/3/QUIC 正是在进一步解决这个问题。

19. QUIC 为什么能帮助改进弱网场景体验?

因为 QUIC 跑在 UDP 之上,把连接建立、加密、丢包恢复、多路复用这些能力上移到用户态协议里,能更快建连,并减少 TCP 级别的队头阻塞。弱网下,时延和丢包更容易暴露,QUIC 往往更有优势。

20. Cookie、Session、Token、JWT 各自适合什么场景?
  • Cookie:浏览器端存少量状态标识,适合 Web 场景
  • Session:服务端保存会话,适合传统服务端登录态
  • Token:客户端持有访问凭证,适合前后端分离、移动端、微服务网关鉴权
  • JWT:一种自包含 Token,适合跨服务传递用户声明、减少中心会话查询

但 JWT 不适合承载太大或频繁变化的信息。

21. JWT 有哪些优点,也有哪些天然缺点?

优点:

  • 无状态,服务端不必强依赖 session 存储
  • 易于跨服务传递
  • 适合网关统一鉴权

缺点:

  • 一旦签发,主动失效麻烦
  • Token 太大会增加传输开销
  • 负载里放敏感信息有风险
  • 权限变更不能天然实时生效

所以 JWT 适合"声明型访问令牌",不适合把它当万能会话容器。

22. 什么情况下你会明确反对使用 JWT 做登录态承载?

这些场景我会谨慎甚至反对:

  • 需要随时强制下线
  • 权限变动要求立刻生效
  • 单点注销要求很强
  • Token 内容很多、变化频繁
  • 安全合规要求服务端强控制会话

这类场景更适合:短期 access token + 服务端 session / token 黑名单 / 统一鉴权中心。

23. 一个接口 RT 突然从 50ms 上升到 2s,你会从网络层先排查哪些点?

先看:

  1. DNS 是否变慢
  2. TCP 建连是否异常
  3. TLS 握手是否变慢
  4. 是否有丢包、重传、连接重置
  5. 负载均衡或网关层是否异常
  6. 连接池是否耗尽,导致不是网络看起来像网络

排查顺序一般是:客户端指标 → 网关/SLB → 服务端连接状态 → 抓包或网络监控。

24. 服务之间大量出现 Connection resetBroken pipe,通常会怎么定位?

这类问题通常先分三类看:

  • 对端主动断开
  • 中间网络设备断开
  • 本端往一个已断开的连接继续写

我会重点查:

  • 连接是否被服务端 keep-alive 超时关闭
  • 网关/NAT/防火墙是否清理空闲连接
  • 客户端连接池是否复用失效连接
  • 服务端是否重启、Full GC、线程池打满
  • 是否存在大包、超时、半开连接问题

Broken pipe 往往是本端写入一个已被对方关闭的连接

25. 为什么线上会出现大量 CLOSE_WAIT 或 TIME_WAIT?分别说明什么问题?
  • TIME_WAIT 多:通常说明本机主动关闭连接较多,未必是故障,短连接服务很常见
  • CLOSE_WAIT 多:通常说明对方已经关闭连接,但本地应用没有及时 close,往往是应用代码或资源释放有问题

面试里最关键的一句是:
TIME_WAIT 常是协议正常现象,CLOSE_WAIT 更值得优先怀疑应用没正确关连接。

26. 负载均衡层使用四层和七层分别适合什么场景?
  • 四层:按 IP + 端口转发,性能高,适合 TCP/UDP 通用转发
  • 七层:理解 HTTP/HTTPS 请求内容,可做路径路由、Header 路由、鉴权、限流、灰度

所以:
追求通用高性能选四层;要做业务路由和治理能力,选七层。

27. 如果一个高并发服务频繁建立短连接,你会怎么优化?

常见优化思路:

  • 尽量复用长连接
  • 调整连接池参数
  • 开启 keep-alive
  • 减少 TLS 握手次数,必要时会话复用
  • 网关和服务端都优化 accept/backlog/连接上限
  • 缩短不必要的超时时间和错误重试链路

本质是:减少建连和挥手成本,把连接当稀缺资源来管理。


2. 操作系统

28. 进程和线程的核心区别是什么?

进程是资源分配的基本单位 ,线程是CPU 调度的基本单位

进程之间地址空间隔离;同一进程内线程共享堆、方法区、文件描述符等资源,但各自有自己的栈和程序计数位置。

29. 为什么线程切换成本通常低于进程切换?

因为线程共享同一进程的大部分资源,不需要像进程切换那样频繁切换独立地址空间和大量上下文。

但线程切换也不是免费,线程过多同样会带来调度开销和上下文切换成本。

30. 用户态和内核态分别是什么?系统调用为什么会带来开销?
  • 用户态:应用程序运行的受限模式
  • 内核态:操作系统核心代码运行的高权限模式

系统调用会带来开销,因为它涉及从用户态切到内核态,再切回来,中间还有参数校验、权限检查、上下文切换等成本。

31. CPU 从硬件角度是如何执行程序的?上下文切换的本质是什么?

CPU 按指令流执行程序,核心就是取指、译码、执行。

上下文切换的本质,是把当前任务的寄存器、程序计数器、栈等运行现场保存起来,再恢复另一个任务的现场。

所以线程和进程太多,哪怕 CPU 使用率不爆,也可能因为切换过多导致性能差。

32. 内存的页、页表、虚拟内存分别是什么?
  • 页:内存管理的固定大小单元
  • 页表:记录虚拟地址到物理地址的映射关系
  • 虚拟内存:给每个进程一个独立连续的地址空间视图,底层由操作系统和硬件做映射

它的价值是:隔离进程、简化编程、支持按需加载和换页。

33. 磁盘顺序读写和随机读写性能为什么差异很大?

因为顺序读写更符合底层存储设备和页缓存预读、合并写的模式;随机读写会带来更多寻址、更多离散 IO,吞吐和延迟都更差。

所以 Kafka、MySQL redo log 这类系统都非常重视顺序写。

34. 网络 IO 的完整链路大致是怎样的?

大致是:

应用发起系统调用 → 数据从用户态进入内核态 → 网络协议栈处理 → 网卡/DMA 发送;

接收时反过来:网卡收到数据 → DMA 写入内存 → 内核协议栈处理 → 应用 read/recv 读到用户空间。

所以网络 IO 的成本不只是"发包",还包括用户态/内核态切换、数据拷贝、协议栈处理。

35. 阻塞 / 非阻塞 / 同步 / 异步分别如何理解?
  • 阻塞 / 非阻塞:看调用线程会不会被挂住等结果
  • 同步 / 异步:看结果完成是调用方主动等,还是完成后被通知

常见误区是把"非阻塞"等同于"异步"。

例如 epoll + 非阻塞 socket,很多时候仍然是同步 IO 多路复用

36. select、poll、epoll 的区别是什么?
  • select:fd 数量上限受限,通常是 1024,且每次都要遍历
  • poll:去掉了固定 fd 数量限制,但本质仍然要线性扫描
  • epoll:事件驱动,适合大量连接场景,扩展性更好

一句话:
select/poll 更像"我每次都问一遍谁就绪",epoll 更像"谁就绪谁通知我"。

37. 什么是零拷贝?它到底"零"的是哪几次拷贝?

零拷贝不是完全没有拷贝,而是尽量减少用户态和内核态之间的多次数据拷贝

典型收益是:减少 CPU 拷贝开销、减少上下文切换、提高大文件或高吞吐网络传输效率。

38. mmap 和 sendfile 的核心思想是什么?
  • mmap:把文件映射到用户空间地址,减少显式 read/write 拷贝和系统调用次数
  • sendfile:直接在内核中把文件内容传给 socket,避免多次用户态参与

所以它们的核心思想都是:尽量少搬运、少切换、少经过用户态。

39. DMA 在 IO 传输中起到了什么作用?

DMA 让外设和内存之间可以直接进行数据搬运,不需要 CPU 一字节一字节参与复制。

CPU 更像负责发起和收尾,DMA 负责大部分搬运工作,这也是高性能 IO 的基础之一。

40. top、htop、ps、vmstat、iostat、sar、pidstat、netstat、ss、lsof 分别常用于看什么?

常见记法:

  • top / htop:看整体负载、CPU、内存、热点进程
  • ps:看进程详情
  • vmstat:看系统级 CPU、内存、上下文切换、运行队列
  • iostat:看磁盘 IO
  • sar:看历史系统指标
  • pidstat:看某个进程维度的 CPU、内存、IO
  • netstat / ss:看网络连接、端口、socket 状态,ss 往往更现代更快
  • lsof:看进程打开的文件、端口、句柄

面试里不要只背命令,最好顺手说一句:CPU 高先 top/pidstat,IO 高先 iostat,连接多先 ss,句柄问题先 lsof。

41. 线上 CPU 飙高时,你的排查顺序是什么?

我一般按这个顺序排:

  1. 先确认是不是整体 CPU 高

    tophtopuptime 看整体 CPU、load average、是否所有核都忙。

  2. 再定位到具体进程 / 线程

    top -Hp <pid>pidstat -p <pid> 1 看是哪个线程吃 CPU。Java 进程再结合线程 ID 转 16 进制,配合 jstack 看线程栈。

  3. 判断是用户态忙还是内核态忙

    ussywasihi

    • us 高:多半是业务代码、死循环、频繁计算
    • sy 高:系统调用、网络包、锁、内核开销
    • wa 高:不是 CPU 真忙,往往是 IO 等待
  4. 看最近有没有变更

    发布、流量突增、慢 SQL、缓存失效、MQ 积压、GC 抖动都可能把 CPU 拉高。

一句话,排障不是上来就猜代码问题,而是先做 系统级定位 → 进程级定位 → 线程级定位 → 调用栈定位

42. 线上内存持续上涨时,你的排查顺序是什么?

我通常分成"是不是正常增长"和"是不是泄漏"两层看:

  1. 先看 OS 维度topfree -hvmstat,看是进程 RSS 在涨,还是 page cache 在涨。
  2. 再看进程维度pspmappidstat -r 看哪个进程涨得快。
  3. Java 进程重点看 JVM
    • jstat -gc 看 Eden / Old 区变化
    • jcmd GC.heap_infojmap -histo 看对象分布
    • 必要时 dump 堆,用 MAT 分析大对象、引用链、GC Roots
  4. 区分几种情况
    • 用完能回收:可能只是流量上涨
    • Full GC 后还不降:要怀疑内存泄漏
    • 堆不高但进程内存高:可能是直接内存、线程栈、元空间、本地库

面试里要体现一句:"内存上涨"不等于"堆泄漏",要先区分 OS 内存、JVM 堆、堆外内存。

43. 磁盘 IO 打满时,你如何判断是顺序写、随机写还是日志刷盘问题?

我会先用 iostat -x 1pidstat -d 1 看磁盘 util、await、svctm、读写吞吐,再结合业务判断:

  • 顺序写:吞吐很高,IOPS 不一定特别高,常见于日志、MQ、append-only 写入
  • 随机写:IOPS 高、吞吐不一定高,await 容易高,常见于大量离散更新、B+ 树页分裂
  • 日志刷盘问题:业务写不大,但 fsync 频繁,延迟会明显抖,数据库 redo/binlog、AOF 都常见

如果是 Java 服务,我还会联动看:慢 SQL、事务提交、AOF 策略、Kafka 刷盘策略、应用日志打太猛。

本质上是看 吞吐特征 + IOPS 特征 + 延迟特征 + 业务写模式

44. 如何定位某个 Java 进程占用大量文件句柄?

常规做法:

  1. lsof -p <pid> 看这个进程打开了哪些文件、socket、pipe
  2. ls /proc/<pid>/fd | wc -l 看句柄总数
  3. cat /proc/<pid>/limits 看进程句柄上限
  4. 看是 文件泄漏连接泄漏 还是 日志句柄积压
  5. Java 里常见原因:
    • 连接池没归还
    • 文件流没 close
    • HTTP 客户端连接泄漏
    • 大量短连接堆积

如果是 socket 多,再用 ss -anp | grep <pid> 看连接状态。

一句话:先确认"多的是啥句柄",再追资源释放链路。

45. 如何定位某台机器网络连接数异常升高的原因?

我会这样查:

  1. ss -s 看整体连接概况
  2. ss -antp 看是哪些端口、哪些进程连接多
  3. 统计状态分布:ESTABTIME-WAITCLOSE-WAITSYN-RECV
  4. 判断方向:
    • TIME-WAIT 多:多半短连接多
    • CLOSE-WAIT 多:应用没及时 close
    • SYN-RECV 多:半连接多,可能是突发流量或攻击
  5. 再结合网关、连接池、发布变更、下游超时去判断

重点不是只看"连接数",而是看 是什么连接、什么状态、属于哪个进程、发生在什么时间点

46. load average 很高一定代表 CPU 忙吗?为什么?

不一定。
load average 统计的是一段时间内处于 可运行状态不可中断睡眠状态 的任务平均数。除了 CPU 等待队列,很多 IO 等待中的任务也会抬高 load。

所以会出现这种情况:

  • CPU 利用率不高
  • 但 load average 很高
  • 实际原因是磁盘、网络、NFS 或锁等待

面试里最好直接说:load 高不等于 CPU 高,它表示系统资源需求高,尤其要警惕 D 状态任务。

47. 僵尸进程和孤儿进程分别是什么?
  • 僵尸进程 :子进程已经退出,但父进程还没 wait 它,导致进程表项还留着
  • 孤儿进程 :父进程先退出了,子进程还在运行,后面会被 init/systemd 接管

僵尸进程真正占的不是大块内存,而是进程表项;少量僵尸问题不大,但大量僵尸会耗尽 PID / 进程表资源。

孤儿进程不一定有问题,它只是"没人养了,被系统接管"。

48. kill -9 为什么不是排障首选?

因为 kill -9SIGKILL ,进程没有机会做清理动作。

它的问题是:

  • 不能优雅释放连接、锁、文件
  • 不能 flush 日志或缓冲区
  • 数据库、消息消费、文件写入场景可能留下不一致状态
  • 还会掩盖真正原因,影响后续排查

一般先尝试 SIGTERM,给进程清理和退出机会;只有进程僵死、无法响应时才用 kill -9

一句话:kill -9 是止血手段,不是优雅处置手段。

49. 一个服务 CPU 不高,但 RT 很高,你会怀疑哪些问题?

我会优先怀疑这些:

  • 下游依赖慢:数据库、Redis、MQ、RPC
  • 线程池排队
  • 连接池耗尽
  • 锁竞争
  • 磁盘 / 网络 IO 等待
  • Full GC 或 safepoint 抖动
  • DNS / TLS 建连慢
  • 网关限流或重试风暴

因为 RT 高不代表一定是算不过来,更多时候是 在等

所以要结合线程状态、连接池指标、慢 SQL、下游 RT、GC 日志一起看。

50. 一个服务线程数暴涨但吞吐下降,你会如何分析?

这通常说明线程不是越多越好,反而可能已经进入反效果:

  1. 看线程是不是都在忙

    很多线程可能都在阻塞、排队、等待锁、等 IO。

  2. 看线程池配置

    核心线程太小、队列太长、最大线程数太大,都会把问题隐藏成"线程越来越多"。

  3. 看上下文切换

    线程过多会导致调度开销大、缓存命中下降、CPU 时间碎片化。

  4. 看资源瓶颈是不是在线程外

    比如数据库连接池 50 个,线程开到 500 个也没意义。

面试里一句漂亮的话是:线程数暴涨而吞吐下降,本质上常是排队和争抢变重了,不是算力增强了。

51. 线上机器频繁上下文切换会带来什么影响?怎么确认?

影响主要是:

  • CPU 花在调度上的时间变多
  • 缓存命中变差
  • 真实业务执行时间被挤占
  • RT 抖动,吞吐下降

确认方式一般是:

  • vmstat 1cs
  • pidstat -w 1 看进程级上下文切换
  • 再结合线程数、线程池、锁等待、IO 阻塞看原因

如果线程很多、锁很多、短任务很多、频繁 wakeup,都会导致上下文切换高。

52. 如果怀疑是内核参数导致 TCP 表现异常,你会关注哪些内核参数?

我会重点关注这些方向:

  • 监听队列相关:somaxconntcp_max_syn_backlog
  • 端口范围:ip_local_port_range
  • TIME_WAIT / 连接回收相关
  • socket buffer:rmemwmem
  • keepalive 相关参数
  • 重传、syn cookies、fin timeout 等参数

但排障时不能只背参数名,关键是先知道症状是什么:

  • 建连慢
  • 半连接多
  • 短连接端口耗尽
  • 大量 TIME_WAIT
  • 吞吐上不去

然后再反推参数。


3. 数据结构与算法

53. 数组和链表各自的优缺点是什么?
  • 数组:内存连续,随机访问快,按下标访问是 O(1),缓存友好;但中间插入删除成本高,扩容有代价。
  • 链表:插入删除节点方便,不要求连续内存;但随机访问慢,额外有指针开销,缓存局部性差。

工程上很多时候数组类结构性能更稳,因为 CPU 更喜欢连续内存。

54. 栈和队列的典型应用场景分别有哪些?
  • :函数调用、表达式求值、DFS、括号匹配、撤销操作
  • 队列:任务排队、消息消费、BFS、线程池任务缓冲、生产者消费者模型

一句话:栈适合后进先出,队列适合先进先出。

工程里线程池队列、消息队列,本质上就是队列思想的落地。

55. 二叉树、平衡树、堆、Trie、B+ 树分别适合解决什么问题?
  • 二叉树:表达层级关系
  • 平衡树:保持查找、插入、删除的稳定复杂度
  • :快速取最大值 / 最小值,适合优先级队列、TopK
  • Trie:前缀匹配、词典搜索、自动补全
  • B+ 树:磁盘友好,适合数据库索引和范围查询

面试里最好带一句:数据结构不是背定义,而是看它最适合什么访问模式。

56. 为什么数据库索引更偏向 B+ 树而不是红黑树?

因为数据库索引核心瓶颈常在磁盘 / 页访问,不是单纯 CPU 比较次数。

B+ 树的优势是:

  • 多叉,树更矮,磁盘 IO 次数更少
  • 叶子节点顺序链接,范围查询更友好
  • 一个节点能装更多 key,更适合页结构

红黑树更适合内存结构,数据库页存储场景下 B+ 树更合适。

57. 哈希表为什么会发生冲突?常见冲突解决方式有哪些?

哈希冲突本质是:不同 key 经过 hash 后落到同一个桶

常见处理方式:

  • 拉链法:桶里挂链表 / 树
  • 开放寻址法:冲突后往别的位置探测
  • 再哈希

Java 的 HashMap 主要是 数组 + 链表 / 红黑树 的思路。

58. HashMap 扩容为什么会影响性能?

因为扩容不是只申请一块新内存,还要做 rehash / 数据迁移

影响主要有:

  • 一次性搬迁成本高
  • 扩容期间会拉高延迟
  • 并发场景下如果用法不当风险更大

所以工程里如果预估数据量比较明确,最好给一个合理初始容量,减少扩容次数。

59. 图的邻接矩阵和邻接表分别适合什么场景?
  • 邻接矩阵:适合点不多、边较密、判断两点是否直接相连很频繁的场景
  • 邻接表:适合稀疏图,节省空间,遍历某个点的邻居更高效

一句话:稠密图偏矩阵,稀疏图偏邻接表。

工程上很多依赖关系、服务调用关系,其实更接近邻接表。

60. 快排、归并、堆排的时间复杂度和适用场景分别是什么?
  • 快排:平均 O(n log n),原地排序,实践里通常很快,但最坏 O(n²)
  • 归并:稳定,时间 O(n log n),但要额外空间,适合链表排序和外部排序
  • 堆排:O(n log n),原地,不稳定,适合需要边排序边取 Top 的场景

工程里不是一味背复杂度,还要考虑:稳定性、额外空间、局部性、常数项。

61. 二分查找除了查精确值,还常用于哪些"答案空间搜索"场景?

二分不只是"数组找数",还常用于:

  • 找最左 / 最右边界
  • 在单调条件下找最小可行值、最大可行值
  • 调参类问题,比如"最小机器数""最小超时时间""最大吞吐阈值"

本质上是:只要答案空间具有单调性,就可以二分。

很多工程题其实是"二分答案"而不是"二分元素"。

62. 动态规划和贪心的本质区别是什么?
  • 动态规划:把问题拆成子问题,保存子问题最优解,最后合成全局最优
  • 贪心:每一步都做当前看起来最优的选择,希望最后得到全局最优

关键区别是:
贪心不回头,DP 会系统性比较状态。

所以不是所有最优化问题都能贪心,能不能贪心要证明"局部最优能推出全局最优"。

63. 回溯算法适合解决哪类问题?

适合这类问题:

  • 全排列、组合、子集
  • N 皇后
  • 路径搜索
  • 约束满足问题

它的核心是:试一个选择,不行就撤销,继续试下一个。

也就是"深度优先 + 剪枝"。如果剪枝做得好,性能会好很多。

64. 你如何向面试官解释"时间复杂度"和"空间复杂度"而不只是背定义?

我会这么说:

  • 时间复杂度:输入规模增长时,执行步骤增长的趋势
  • 空间复杂度:输入规模增长时,额外内存占用增长的趋势

重点不是精确到每一步,而是看增长级别。

但工程里我会补一句:复杂度只反映增长趋势,不反映常数项、缓存命中、并发等待、IO 等现实成本。

这句话通常比较加分。

65. 为什么工程里很多时候 O(n) 不一定比 O(logn) 慢,反而可能更快?

因为真实系统里性能不只由渐进复杂度决定,还受这些因素影响:

  • 常数项大小
  • CPU 缓存局部性
  • 内存连续性
  • 分支预测
  • 锁和并发开销
  • IO / 网络等待

比如一个小规模数组顺序扫,可能就比复杂树结构更快。

所以工程里不能只看大 O,还要看 数据规模、访问模式、硬件特性

66. 在高并发系统里,为什么算法复杂度只是性能分析的一部分?

因为高并发系统的瓶颈常常不在纯计算,而在:

  • 锁竞争
  • 线程切换
  • 缓存命中率
  • 网络 IO
  • 磁盘 IO
  • 连接池 / 线程池排队
  • GC

所以一个理论上复杂度更优的方案,在真实线上未必更快。

面试里最好说一句:复杂度决定上限趋势,但系统性能还取决于资源争用和等待。

67. 你如何评估一个接口慢,是算法问题、锁竞争问题、IO 问题还是网络问题?

我会按"先分层,再定位"的思路:

  1. 看接口链路指标:本机耗时、下游耗时、网络耗时
  2. 看线程栈:在算、在等锁、在等 IO、在等下游
  3. 看系统指标:CPU、上下文切换、磁盘、网络、连接数
  4. 看热点调用:profile、trace、慢 SQL、GC

判断标准大概是:

  • CPU 高、热点方法集中:偏算法 / 计算问题
  • BLOCKED 多:偏锁竞争
  • wa 高 / await 高:偏 IO
  • 重传、连接异常、建连慢:偏网络

本质就是先区分:在算,还是在等。

68. 如果一个排行榜系统要支持 TopN、范围查询、附近排名,你会怎么选数据结构?

如果是工程落地,我通常会选:

  • Redis ZSet:最常见,天然支持按 score 排序、TopN、范围查、排名查询
  • 如果是单机内存结构:可以考虑 跳表 / 堆 + 索引结构
  • 如果还要做复杂筛选:可能要结合 ES / DB 做离线或混合方案

因为排行榜核心需求不是只有"排序",而是要同时支持:

  • 快速更新分数
  • 查 TopN
  • 查某个用户名次
  • 查某个区间
  • 附近排名

这类访问模式,ZSet 很合适。


二、Java 核心

1. Java 基础与集合

69. 面向对象的三大特性是什么?在工程实践里你最看重哪一个?

三大特性是:封装、继承、多态

工程里我最看重的是 封装。因为真实项目里,封装决定了:

  • 边界清不清楚
  • 代码改动会不会扩散
  • 业务规则是不是收敛在对象内部
  • 模块是不是容易演进

继承和多态当然也重要,但滥用继承反而容易把层次搞乱。

所以我一般会说:面向对象在工程里最重要的不是"会继承",而是"会做边界和抽象"。

70. 重载和重写有什么区别?
  • 重载(Overload):同一个类里,方法名相同,但参数列表不同;是编译期多态
  • 重写(Override):子类重新实现父类方法,方法签名兼容;是运行时多态

重载看的是"同名不同参",重写看的是"子类改父类行为"。

还有个常见点:只有返回值不同,不能算重载。

71. ==equals 的区别是什么?
  • ==:比较的是两个引用是不是指向同一个对象;基本类型时比较值
  • equals:默认在 Object 里和 == 一样,但很多类会重写它,用来比较"逻辑相等"

比如 String 重写了 equals,比较的是内容。

所以面试里最好答完整:== 偏身份相等,equals 偏语义相等。

72. finalfinallyfinalize 分别是什么?
  • final :关键字,可修饰类、方法、变量
    • final 类不能被继承
    • final 方法不能被重写
    • final 变量只能赋值一次
  • finally:异常处理里的代码块,通常用于资源清理
  • finalizeObject 的方法,历史上用于对象回收前的清理,但这个机制问题很多,已经不推荐依赖

面试里加一句会更稳:finalize 既不可靠,也不及时,现代 Java 更推荐 try-with-resources / AutoCloseable。

73. 泛型为什么是"伪泛型"?类型擦除会带来什么影响?

Java 泛型常被叫"伪泛型",因为编译后会发生 类型擦除

也就是:编译器在编译期做类型检查,生成字节码时把大部分泛型类型信息擦掉,必要时补强制类型转换和桥接方法。

影响主要有:

  • 运行时拿不到完整泛型参数信息
  • 不能直接 new T()
  • 不能创建 new List<String>[]
  • 某些反射场景要额外处理泛型信息

一句话:Java 泛型主要强化的是编译期类型安全,不是运行期模板实例化。

74. 反射的优缺点是什么?为什么框架大量使用反射?

优点:

  • 灵活,运行时可检查类、方法、字段、注解
  • 有利于做通用框架、依赖注入、ORM、序列化、插件机制

缺点:

  • 可读性和可维护性变差
  • 性能一般比直接调用差
  • 破坏封装边界的风险更大
  • 出错更晚,很多问题从编译期变成运行期

框架大量使用反射,是因为框架更在乎 通用性和扩展性

比如 Spring、JPA、Jackson,都需要在运行时根据类结构和注解决定行为。

75. 注解的本质是什么?运行时注解和编译时注解的区别是什么?

注解本质上是 给程序元素附加元数据。它不是业务逻辑本身,而是给编译器、框架、工具看的附加信息。

区别主要在保留策略:

  • SOURCE:只在源码阶段有效,编译后没了
  • CLASS:编译进 class,但运行时默认反射拿不到
  • RUNTIME:运行时还能通过反射读取

所以像 Spring 这类框架依赖的注解,通常要用 RUNTIME

而像 @Override 更偏编译器辅助。

76. Java 异常体系是怎样的?受检异常和非受检异常怎么取舍?

Java 异常体系顶层是 Throwable,下面主要分两大类:

  • Error:系统级严重错误,一般不处理
  • Exception:业务和程序里常处理的异常
    • 受检异常(checked)
    • 非受检异常(unchecked / RuntimeException)

取舍原则通常是:

  • 受检异常:调用方有可能恢复,值得显式处理
  • 非受检异常:编程错误、非法状态、不适合每层都强制捕获

工程里很多业务系统更偏 RuntimeException,因为 checked 异常滥用会让接口层层透传、代码很重。

但涉及明确可恢复场景时,checked 也有价值。

77. ArrayList 和 LinkedList 的底层区别是什么?
  • ArrayList:底层是动态数组,随机访问快,尾部追加快,扩容时有拷贝成本
  • LinkedList:底层是双向链表,插入删除节点方便,但随机访问慢,遍历局部性差

实际工程里,ArrayList 用得远多于 LinkedList。

因为大多数场景更在乎遍历、随机访问、缓存友好,而不是链表式插入删除。

78. ArrayList 扩容机制是什么?为什么不是每次只扩 1 个?

ArrayList 本质是动态数组,容量不够时会扩容并把原数据搬过去。

如果每次只扩 1 个,会导致:

  • 扩容次数太频繁
  • 每次都拷贝,整体代价很大
  • 追加元素的摊销性能很差

所以它会按一定比例扩,而不是一个一个加。

核心思想是:用少量空间浪费换更稳定的追加性能。

79. HashMap 的底层数据结构是什么?

HashMap 底层可以理解成:数组 + 链表 + 红黑树

key 先经过 hash 定位到桶,桶里冲突少时是链表,冲突严重时会树化成红黑树,以改善查找性能。

所以它不是单纯"哈希表",而是做了冲突治理的复合结构。

80. JDK 1.7 和 JDK 1.8 的 HashMap 有哪些关键差异?

面试里常答这几个点:

  1. 数据结构变化

    • JDK 1.7:数组 + 链表
    • JDK 1.8:数组 + 链表 + 红黑树
  2. 插入方式变化

    • 1.7 头插法
    • 1.8 尾插法为主,减少扩容迁移时链表反转问题
  3. 扩容 / rehash 处理更优化

    1.8 在元素迁移时利用容量翻倍的特性,判断元素是留在原位还是移动到 oldCap 偏移位置,迁移更高效。

  4. 并发风险认知

    1.7 时代并发误用 HashMap 更容易出现链表成环等严重问题;1.8 做了一些改进,但 HashMap 依然不是线程安全容器

一句话总结就是:1.8 主要在冲突治理和扩容迁移上更成熟。

81. HashMap 为什么长度通常设计成 2 的幂?

因为这样可以用 (n - 1) & hash 来代替取模运算,定位桶位更高效;同时在容量翻倍时,元素要么留在原位置,要么移动到"原索引 + oldCap",迁移逻辑更简单。

82. 红黑树化的条件是什么?为什么需要树化?

HashMap 在桶内冲突过多时会把链表转成红黑树,目的是把极端情况下的查找效率从 O(n) 改善到 O(log n)。树化本质上是为了解决哈希冲突严重时链表过长导致的性能退化问题。

83. ConcurrentHashMap 为什么比 Hashtable 性能更好?

Hashtable 基本是整张表级别的同步,粒度比较粗;ConcurrentHashMap 则把并发控制做得更细,读操作通常不需要全表加锁,更新时也尽量缩小竞争范围,所以并发性能明显更好。

84. ConcurrentHashMap 在 JDK 1.7 和 1.8 的实现思路有什么不同?

JDK 1.7 主要是 Segment + HashEntry,相当于分段锁;JDK 1.8 改成了更扁平的 Node[] + CAS + synchronized 方案,去掉了 Segment,结构更简单,也更利于优化。

85. fail-fast 是什么?它是如何实现的?

fail-fast 是指:在迭代集合时,如果集合发生了结构性修改,迭代器会尽快抛出 ConcurrentModificationException,避免继续在不确定状态下运行。常见实现方式是维护一个修改计数,迭代时检查该计数是否发生了意外变化。

86. 迭代器为什么会抛 ConcurrentModificationException?

因为在创建迭代器之后,如果集合被"非迭代器自身方式"做了结构性修改,集合内部的修改计数和迭代器预期值不一致,迭代器在下一次访问时就会抛出 ConcurrentModificationException。不过这种 fail-fast 是 best-effort,不是绝对同步保证。

87. CopyOnWriteArrayList 适合什么场景?为什么不适合写多场景?

它适合读多写少的场景,比如订阅者列表、配置快照、监听器集合。因为它的写入本质是"复制底层数组再替换引用",读时几乎无锁,但写成本高、内存开销也大,所以不适合高频写场景。

88. 如果一个热点接口内部大量使用 HashMap,你会关注哪些性能和安全问题?

我会关注四类问题:

  1. 初始容量是否合理,避免频繁扩容;
  2. key 的 hashCode/equals 是否设计得好,避免冲突严重;
  3. 是否存在并发误用 HashMap 的问题;
  4. 是否有大对象或临时对象过多,导致 GC 压力上升。
    也就是说,要同时看哈希分布、扩容成本、线程安全、对象生命周期
89. 为什么有些场景下明明 LinkedList 插入复杂度低,实际性能反而不如 ArrayList?

因为 LinkedList 虽然节点插入删除本身便宜,但它随机访问慢、对象分散、缓存局部性差,而且每个节点都有额外指针开销。现实工程里,ArrayList 往往因为连续内存和更好的 CPU cache 友好性,整体性能反而更好。

90. 你如何设计一个线程安全、支持高并发读写的本地缓存结构?

简单场景可以用 ConcurrentHashMap 做底层存储,再补 TTL、最大容量和淘汰策略;如果读多写少,还可以结合 volatile 快照或 Copy-On-Write 思路。再复杂一点要补统计、过期扫描、并发加载和可观测性。核心不是"线程安全"四个字,而是要同时考虑读性能、写竞争、过期淘汰、内存上限


2. 并发编程

91. Java 创建线程有哪些方式?各自优缺点是什么?

常见有三种:继承 Thread、实现 Runnable、实现 Callable 并配合 Future/FutureTask

  • 继承 Thread:简单,但和任务耦合太强;
  • Runnable:更推荐,任务和线程分离;
  • Callable:支持返回值和异常传播,更适合有结果的异步任务。
    工程里更常用的是线程池 + Runnable/Callable,而不是手动频繁 new Thread。
92. Runnable、Callable、Future、CompletableFuture 的区别是什么?
  • Runnable:无返回值;
  • Callable:有返回值,可抛异常;
  • Future:表示异步结果,可查询是否完成、可阻塞获取结果;
  • CompletableFuture:在 Future 基础上支持链式编排、组合、回调和异常处理。
    一句话:Runnable/Callable 是任务,Future 是结果句柄,CompletableFuture 是更强的异步编排工具。
93. 线程生命周期有哪些状态?分别在什么情况下发生流转?

Java 线程常见状态有:NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED

比如:

  • new 出来还没 start 是 NEW
  • 可运行或正在运行时是 RUNNABLE
  • 等待锁进入同步块是 BLOCKED
  • 调用 wait/join/park 进入等待;
  • 睡眠或带超时等待进入 TIMED_WAITING
  • 线程执行完毕后是 TERMINATED
94. waitsleepjoin 的区别是什么?
  • sleep:让当前线程休眠一段时间,不释放对象锁;
  • wait:让当前线程进入等待,并释放当前对象监视器,但必须在同步块中使用;
  • join:让当前线程等待另一个线程执行结束,本质上是一种线程间协作。
    面试里最关键的一句是:wait 会释放锁,sleep 不会。
95. wait/notify/notifyAll 为什么必须和同步块配合使用?

因为它们依赖对象监视器(monitor)工作,调用这些方法的前提是当前线程已经持有该对象的监视器。否则运行时会抛 IllegalMonitorStateException。也就是说,它们不是普通方法调用,而是和 Java 内置锁语义绑定的线程协作机制。

96. 什么是线程安全?原子性、可见性、有序性分别是什么?

线程安全就是:在多线程环境下,不管怎么交替执行,程序结果都符合预期。

  • 原子性:一个操作不可被中途打断;
  • 可见性:一个线程修改的结果,其他线程能及时看到;
  • 有序性 :程序执行结果不被指令重排破坏。
    并发问题本质上很多都能归到这三类基础属性。
97. synchronized 的底层原理是什么?

synchronized 基于对象监视器(monitor)实现,进入同步块前要先获得监视器,退出时释放。它既能保证互斥,也具备内存语义,能保证进入和退出同步块时的可见性。简单说,它既是 ,也是内存屏障语义的一部分

98. volatile 能保证什么,不能保证什么?

volatile 能保证:

  • 变量写入后对其他线程可见;
  • 禁止某些关键重排序。
    但它不能保证复合操作的原子性 ,比如 count++ 仍然不是线程安全的。所以 volatile 更适合状态标志、开关变量、单次发布等场景。
99. CAS 的原理是什么?ABA 问题如何解决?

CAS 是 Compare-And-Set:先比较某个内存位置当前值是否等于预期值,如果相等就更新成新值,否则失败重试。

ABA 问题是:值从 A 变成 B 又变回 A,CAS 只看当前值会误判"没变过"。常见解决方案是给值加版本号或时间戳,比如 AtomicStampedReference 这种思路。

100. 什么是 AQS?为什么很多并发工具都基于它实现?

AQS 是 AbstractQueuedSynchronizer,可以理解成一个用状态变量加等待队列来构建同步器的框架。像 ReentrantLock、Semaphore、CountDownLatch 这类工具都基于它来管理竞争线程、阻塞和唤醒。它的价值在于:把同步器通用骨架抽出来,具体规则由子类实现。

101. ReentrantLock 和 synchronized 如何选择?

简单场景优先 synchronized,语法更直接、可读性好;需要更强控制时选 ReentrantLock,比如:可中断获取锁、可轮询尝试、可设置公平锁、可配合多个 Condition。

所以不是谁绝对更高级,而是:普通互斥用 synchronized,复杂并发控制用 ReentrantLock。

102. ReadWriteLock 适合什么场景?为什么读多写少才有优势?

它适合读多写少的共享资源场景,因为多个读线程可以并发读,而写操作独占。

如果写很频繁,读锁经常被写锁阻塞,读写锁的优势就会下降,甚至还不如普通互斥锁简单高效。所以它的收益前提是:读占绝大多数,且读操作确实能并行。

103. StampedLock 有哪些特点?为什么它并不总是优于 ReadWriteLock?

StampedLock 支持读锁、写锁和乐观读,乐观读在冲突低时性能很好。

但它并不总更优,因为:

  • 使用复杂度更高;
  • 乐观读需要显式校验;
  • 不是可重入锁;
  • 用错更容易出问题。
    所以它更像一个"高性能但更偏专家模式"的工具,不是通用替代品。
104. 乐观锁、悲观锁、自旋锁分别适合什么场景?
  • 乐观锁:冲突少,失败可重试,比如 CAS、版本号更新;
  • 悲观锁:冲突多,宁可先锁住避免并发修改;
  • 自旋锁 :预期等待时间非常短,避免线程挂起/唤醒开销。
    一句话:冲突少偏乐观,冲突多偏悲观,等待极短才考虑自旋。
105. 什么是伪共享?如何避免?

伪共享是指多个线程修改彼此独立的变量,但这些变量恰好落在同一个 CPU cache line 上,导致缓存行反复失效,性能下降。

常见避免方法是做缓存行填充、让热点独立字段隔离存放,或者减少多线程对相邻内存位置的频繁写。它是硬件缓存层面的并发性能问题

106. happens-before 原则有哪些?

常见的 happens-before 包括:

  • 程序次序规则;
  • 监视器锁规则;
  • volatile 变量规则;
  • 线程启动规则;
  • 线程终止规则;
  • 线程中断规则;
  • 对象终结规则;
  • 传递性。
    它本质上定义了哪些操作的执行结果,对另外一些操作必须可见。
107. CountDownLatch、CyclicBarrier、Semaphore、Exchanger 分别适合什么场景?
  • CountDownLatch:一个或多个线程等待若干任务完成;
  • CyclicBarrier:一组线程互相等待,到齐后一起继续;
  • Semaphore:控制并发访问数量;
  • Exchanger:两个线程交换数据。
    它们的区别核心在于:等待谁、控制什么、是否可复用。
108. BlockingQueue 常见实现有哪些?它们的差异是什么?

常见有:

  • ArrayBlockingQueue:有界数组队列;
  • LinkedBlockingQueue:链表队列,可有界也可近似无界;
  • SynchronousQueue:不存元素,直接交接;
  • PriorityBlockingQueue:按优先级出队。
    差异主要在:底层结构、是否有界、吞吐特征、是否支持优先级
109. ConcurrentLinkedQueue 和 ArrayBlockingQueue 的使用边界是什么?

ConcurrentLinkedQueue 是无界、非阻塞队列,适合高并发下的轻量级异步投递;ArrayBlockingQueue 是有界阻塞队列,适合需要背压、限流和明确容量控制的场景。

一句话:想要无锁高并发偏 CLQ,想要容量上限和阻塞语义偏 ABQ。

110. ThreadLocal 的底层实现是什么?为什么会发生内存泄漏?

每个 Thread 内部都维护了一个 ThreadLocalMap,ThreadLocal 作为 key,线程局部变量作为 value。

泄漏风险来自:ThreadLocal 的 key 是弱引用,但 value 不是;如果 ThreadLocal 对象没了,而线程又长期存活,value 可能一直挂在这个线程的 map 里,直到下一次清理逻辑触发。所以在线程池场景里,用完要主动 remove()

111. InheritableThreadLocal 有什么坑?

它会在创建子线程时把父线程的值继承过去,看上去方便,但在线程池场景里问题很大:线程往往不是新建的,而是复用的,所以继承语义可能不符合预期,还容易带来脏数据串线程。

所以它更适合短生命周期、明确父子关系的线程,不适合在线程池里随便依赖。

112. CompletableFuture 为什么在复杂异步编排里非常常用?常见坑有哪些?

因为它支持串行、并行、合并、异常处理、回调编排,能把一连串异步逻辑写得比纯 Future 清晰得多。

常见坑有:

  • 默认线程池用得不明白;
  • 回调里做阻塞操作;
  • 异常处理漏掉;
  • 链路太复杂后难以排障。
    也就是说,它强在编排能力 ,但也会放大线程模型不清、异常流混乱的问题。
113. ThreadPoolExecutor 的核心参数分别是什么?

核心参数主要有:

  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数
  • keepAliveTime:非核心线程空闲存活时间
  • workQueue:任务队列
  • threadFactory:线程工厂
  • RejectedExecutionHandler:拒绝策略
    这些参数一起决定了线程池的容量、扩展方式和压力下的行为。
114. 线程池任务执行流程是怎样的?

大致流程是:

  1. 线程数小于核心线程数,直接创建线程执行;
  2. 否则任务先入队;
  3. 队列满了,如果线程数还没到最大线程数,再创建新线程;
  4. 如果线程数也到上限,就执行拒绝策略。
    一句话总结:先保核心线程,再用队列缓冲,再按需扩容,最后拒绝。
115. 核心线程数、最大线程数、队列长度应该如何权衡?

这三个参数要一起看:

  • 核心线程数决定常态吞吐能力;
  • 最大线程数决定突发流量时能否临时扩容;
  • 队列长度决定能缓存多少待处理任务。
    如果队列过大,问题会被"延迟暴露";如果最大线程过大,容易把系统拖进线程竞争和资源耗尽。所以调参本质是做吞吐、延迟、资源上限、背压能力的权衡。
116. 常见拒绝策略有哪些?各自适合什么场景?

常见有:

  • AbortPolicy:直接抛异常;
  • CallerRunsPolicy:让提交线程自己执行;
  • DiscardPolicy:直接丢弃;
  • DiscardOldestPolicy:丢最旧任务再尝试提交。
    适用上通常是:
  • 核心业务优先抛错,尽快暴露压力;
  • 非核心且可回压的场景可考虑 CallerRuns;
  • 可丢弃任务才考虑 discard。
117. 为什么很多公司不建议直接使用 Executors 创建线程池?

因为 Executors 某些工厂方法会隐藏关键参数,比如:

  • 可能使用无界队列,导致任务无限堆积;
  • 可能创建几乎无限线程,导致资源失控。
    企业里更推荐显式 new ThreadPoolExecutor,把容量边界、队列类型、拒绝策略都写清楚。
118. 线程池里任务堆积会带来哪些连锁反应?

任务堆积不仅仅是"慢一点",还会带来:

  • RT 上升;
  • 内存上涨;
  • 超时增多;
  • 重试风暴;
  • 上游继续打压线程池;
  • 最终影响整个链路稳定性。
    所以线程池排队本质上是一个背压失效信号,不能只把它看成局部小问题。
119. 线程池如何做隔离?为什么业务线程池不能乱共用?

线程池隔离就是按业务类型、任务特征、优先级拆分线程池,比如:RPC 调用池、MQ 消费池、异步通知池分开。

不能乱共用,是因为一个业务池被打满后,会把别的任务一并拖死,形成故障扩散。线程池本质上也是一种舱壁隔离资源

120. CPU 密集型和 IO 密集型线程池参数如何设计?

一般思路是:

  • CPU 密集型:线程数接近 CPU 核数,避免过多切换;

  • IO 密集型 :因为大量时间在等待 IO,线程数可以高于 CPU 核数,但前提是下游资源和内存能扛住。

    最终不是死背"几倍 CPU",而是结合:CPU 使用率、线程阻塞比例、下游连接数、排队长度、RT 和错误率一起调。

121. 虚拟线程是什么?它适合解决什么问题?

虚拟线程是 Java 提供的轻量级线程,由 JDK 调度到少量平台线程上执行,目标是降低高并发程序的开发和维护成本,特别适合"连接多、阻塞多、每个任务逻辑又比较直观"的场景。它最典型的价值是在高吞吐、IO 密集型应用里,让你继续用接近同步直写的代码风格,而不必一上来就把业务拆成复杂的回调或响应式链路。

122. 虚拟线程会不会完全替代线程池?为什么?

不会。虚拟线程能大幅降低"线程很贵"这个问题,但它并没有消灭资源上限。真正受限的往往还是数据库连接池、下游并发度、外部接口配额、内存、CPU 等资源,所以很多场景仍然需要线程池或并发控制来做隔离和背压。更直接地说:虚拟线程主要解决"线程调度成本",不是替你自动解决"系统容量治理"。

123. 虚拟线程在 IO 密集型服务中有什么优势?有什么边界?

优势是:可以用"一请求一线程"的直观写法承载大量并发阻塞任务,减少传统平台线程在高并发阻塞场景下的开销,尤其适合网络请求、文件读写、RPC 聚合这类 IO 密集型服务。边界在于,如果任务本身是 CPU 密集型,或者链路里有必须受限的下游资源,虚拟线程并不会神奇地把吞吐无限放大;它更像是把"线程本身"从瓶颈里拿掉。

124. 如果业务代码里有大量 ThreadLocal、阻塞调用、数据库连接池,迁移虚拟线程时要注意什么?

要重点注意三件事:

  1. ThreadLocal 使用习惯,因为虚拟线程很多,滥用 ThreadLocal 会放大上下文复制、污染和清理问题;
  2. 阻塞点是否真的能接受高并发放大,比如数据库连接池没变大,虚拟线程再多也会卡在池上;
  3. 不要把"虚拟线程很多"误当成"系统容量无限" ,仍然要做资源隔离、限流和超时。
    本质上,迁移虚拟线程时要重新检查"线程不是瓶颈以后,真正的瓶颈是谁"。

3. JVM

125. 一个接口吞吐下降,但 CPU 没满、线程很多、队列很长,你怎么排查?

这种情况我会先判断是不是"在等而不是在算"。优先看线程池队列、线程状态、连接池、慢 SQL、下游 RPC、Redis、MQ,以及 JVM 是否有 GC 抖动。因为 CPU 没满但线程很多、队列又长,通常说明任务在排队或等待外部资源,而不是纯计算不够。

126. 线上出现大量线程阻塞,你如何判断是锁竞争、线程池耗尽还是外部依赖慢?

最直接的是看线程栈和线程状态。

  • 如果大量线程是 BLOCKED,更像锁竞争;
  • 如果很多任务卡在线程池队列或提交被拒绝,更像线程池耗尽;
  • 如果线程大多在 socket read、数据库调用、远程调用等待,更像外部依赖慢。
    排障工具上,Oracle 现在更建议优先用 jcmd 做诊断,也可以结合 jstack、JFR、线程 dump 一起看。
127. 如何用 jstack 判断死锁?

做法通常是先抓线程栈,再看是否有一组线程互相持有对方需要的锁,形成循环等待。jstack 在线程 dump 里会直接给出明显的死锁提示信息;不过 Oracle 官方现在更建议优先使用更新的诊断工具 jcmd,因为 jstack 被列为较早期的实验性工具之一。

128. 如果一个高并发接口既要高吞吐又要强一致,你怎么平衡锁粒度和性能?

核心思路是:尽量缩小串行区,把真正必须强一致的关键步骤收敛到最小范围,其余部分异步化、分段化或幂等化。也就是说,不是"一把大锁锁全流程",而是通过更细粒度锁、状态机、乐观并发控制、唯一约束、幂等校验等手段,把一致性成本压缩到必要步骤里。否则吞吐往往会被锁竞争直接打垮。这个问题本质更偏系统设计,但 JVM 层面会直接反映为线程 BLOCKED 增多、上下文切换升高、吞吐下降。

129. 如何设计一个"防止重复提交"的并发控制方案?

常见做法是多层防线一起上:前端防抖、幂等 token、服务端唯一约束、状态机校验、必要时短期锁或去重表。真正可靠的关键不在"加一把锁",而在"业务语义上是否有唯一性约束和幂等边界"。因为单靠 JVM 内锁只能保护单实例,到了分布式场景还得把幂等和唯一性落到共享存储或统一协调机制上。

130. 在秒杀系统里,JUC 工具和 Redis 锁分别应该放在哪一层使用?

JUC 工具更适合单 JVM 进程内的并发控制,比如线程池、信号量、本地限流、任务协调;Redis 锁这类分布式锁更适合多实例之间做跨进程协调。换句话说:JUC 管单机并发,Redis 锁管分布式互斥。如果这个边界搞混,常见结果就是单机看起来正确,一上集群就失效。

131. JVM 运行时内存区域有哪些?

按 JVM 规范,比较核心的运行时内存区域包括:程序计数器、Java 虚拟机栈、本地方法栈、堆和方法区。面试里经常会把"方法区/元空间"一起讲,但要注意:元空间是 HotSpot 的实现方式,而运行时数据区是规范层的概念。

132. 堆、虚拟机栈、方法区/元空间、本地方法栈、程序计数器分别存什么?
  • :主要存对象实例和数组,是垃圾回收的主战场;
  • 虚拟机栈:每个线程私有,存方法调用对应的栈帧、局部变量、操作数栈等;
  • 方法区:存类元数据、运行时常量池、方法信息等;HotSpot 里常用"元空间"来承载这部分元数据;
  • 本地方法栈:服务于 native 方法调用;
  • 程序计数器:线程私有,用来记录当前执行位置。
133. Java 对象从创建到可访问,中间经历了哪些步骤?

大致可以概括为:类已加载完成后,JVM 先为对象分配内存,再做必要的初始化,然后把对象引用返回给程序使用。具体到 HotSpot 实现,还会涉及对象头初始化、实例字段默认值、构造方法执行等步骤。面试里答到"先分配、再初始化、再执行构造逻辑、最后引用可用"就够稳了。

134. 对象头里通常包含哪些信息?

在 HotSpot 里,常见会讲对象头主要包括两部分:一部分是运行时元数据,如 hash、GC 年龄、锁状态等;另一部分和类元数据关联,用来知道这个对象属于哪个类。具体布局会随 JVM 实现和对象类型变化,所以面试里更重要的是讲清"对象头承载了对象运行时管理信息"。

135. 一个对象在内存中的布局大致是什么?

通常可以理解为三部分:对象头、实例数据、对齐填充。对象头放运行时管理信息,实例数据放字段内容,对齐填充是为了满足 JVM / 平台的内存对齐要求。这里不用死抠字节数,重点是知道对象布局不是"只有字段值"这么简单。

136. TLAB 是什么?为什么它能提高对象分配效率?

TLAB 可以理解成线程在 Eden 里预先申请的一小块私有分配区域。线程优先在自己的 TLAB 里分配对象,能减少多个线程同时在堆上直接竞争分配指针的冲突,所以对象分配通常会更快。它的本质是:用局部私有空间换更少的并发分配竞争

137. 什么对象会进入老年代?有哪些典型路径?

典型会进入老年代的对象包括:经历多次年轻代回收后仍存活的对象、体积较大的对象,以及某些分配担保或晋升策略触发下直接或较早进入老年代的对象。面试里不需要把所有参数背全,关键是知道:老年代主要放"活得久"或者"比较大"的对象。

138. 如何判断对象是否可回收?

现代 JVM 主要不是靠引用计数,而是靠可达性分析:从一组 GC Roots 出发,看对象是否还能被引用链到。能连到的一般认为是存活对象,连不到的对象才会被视为可回收候选。

139. 引用计数和可达性分析分别是什么?为什么 JVM 选可达性分析?

引用计数就是给对象维护一个计数器,有引用加一、失去引用减一,减到零就回收;可达性分析则是从 GC Roots 出发判断对象是否还能被访问。JVM 主要选可达性分析,是因为引用计数很难处理循环引用,而可达性分析能更可靠地解决这个问题。

140. Minor GC、Major GC、Full GC 有什么区别?

通俗说法里:

  • Minor GC 主要回收年轻代;
  • Major GC 常指老年代回收;
  • Full GC 则通常意味着更全面的回收,往往涉及整个堆,甚至还可能伴随类元数据相关区域处理。
    不过要注意,这几个词在不同文档和不同 GC 实现里并不是永远完全等价,面试里最好强调"核心区别在回收范围和停顿代价"。
141. 为什么要采用分代回收?

因为绝大多数对象"生得快,死得也快",只有少部分对象会长期存活。把对象按年龄或存活特征分区后,可以对年轻代采用更高效的回收策略,对老年代采用不同策略,从而整体提升 GC 效率。分代的本质是利用对象生命周期分布的不均匀性。

142. CMS、G1、ZGC 的设计目标分别是什么?
  • CMS:早期以减少长停顿为目标的并发回收器;
  • G1:面向更大堆和可预测停顿目标的服务器端 GC,把堆切成很多 region 来回收;
  • ZGC :面向低延迟和大堆场景,尽量把昂贵工作并发化。
    如果一句话概括:CMS 是早期低停顿路线,G1 是更现代的通用低停顿方案,ZGC 则进一步强调超低停顿。
143. G1 为什么适合较大堆场景?

因为 G1 把堆分成很多大小相等的 region,并优先回收"垃圾收益高"的区域,目标是更好地控制停顿时间。相比传统固定分代分区思路,G1 在大堆下更容易做增量、分批、可预测的回收,所以更适合较大内存、对停顿有要求的服务端应用。

144. ZGC 的核心优化方向是什么?

ZGC 的核心方向就是低延迟。Oracle 文档明确把它定位为可扩展、低延迟的垃圾回收器,并强调它把昂贵工作尽量并发执行,目标是把停顿压得很低,适合大堆和对响应时间敏感的应用。

145. Stop-The-World 为什么不可怕,但 Full GC 频繁很可怕?

因为 STW 本身是 GC 过程中的正常机制,关键不在"有没有 STW",而在"停多久、发生多频繁、是否影响业务"。很多年轻代回收也会 STW,但停顿很短,系统完全能接受;真正可怕的是 Full GC 频繁发生,说明堆压力、内存布局、对象生命周期或参数配置可能已经不健康了,这会把 RT 和吞吐一起拖垮。

146. 类加载的完整过程是什么?

按 JVM 规范,类的生命周期里,常见可概括为:加载、链接、初始化;其中链接又包括验证、准备、解析。面试里常说的"加载-验证-准备-解析-初始化"就是这个过程的展开形式。

147. 双亲委派模型是什么?它解决了什么问题?

双亲委派可以理解成:类加载请求先往上委托给父加载器,只有父加载器无法完成时,子加载器才自己尝试加载。它带来的核心好处是避免核心类被重复加载、减少类冲突,并在很大程度上保护 Java 基础类库的统一性和安全性。

148. 什么情况下会打破双亲委派?

常见场景包括:容器或插件体系需要不同模块隔离同名类、SPI 需要由下层代码去加载上层看不见的实现类、以及某些热部署或自定义类加载器场景。面试里不用把历史案例背太细,抓住一点就够:打破双亲委派通常是为了实现更灵活的隔离或反向查找能力。

149. 类加载器有哪些?分别负责什么?

常见会讲三类:

  • 启动类加载器:负责加载 Java 核心类库;
  • 平台类加载器:负责平台相关类;
  • 应用类加载器 :通常负责应用 classpath 上的类。
    另外,应用也可以自定义类加载器,用来做插件化、隔离加载、热替换等。
150. SPI 机制为什么常用线程上下文类加载器?

因为很多 SPI 的接口定义在上层公共类库里,而实现类可能在应用或容器自己的 classpath 中。单纯按传统父加载方向找,父加载器不一定能看到子加载器里的实现类,所以常借助线程上下文类加载器,让"上层代码"有机会去加载"下层提供的实现"。这本质上是一种反向类加载协作。

151. 常见 OOM 类型有哪些?分别如何定位?

常见会分成:Java 堆 OOM、元空间 OOM、直接内存相关 OOM、线程过多导致的内存问题等。定位思路通常是:

  • 先区分到底是堆内、元空间还是堆外;
  • 再结合 jcmdjstat、堆直方图、heap dump、线程数、GC 日志来判断。
    Oracle 也明确建议优先考虑使用 jcmd 这类更新的诊断工具。
152. 堆 OOM 和元空间 OOM 的排查思路有什么区别?
  • 堆 OOM 更关注对象实例:哪些对象太多、谁在持有、是不是泄漏、是不是缓存没边界;
  • 元空间 OOM 更关注类元数据:是不是动态生成类过多、类加载器泄漏、热部署/代理类不断累积。
    也就是说,一个偏"对象实例问题",一个偏"类元数据和类加载生命周期问题"。
153. 如何判断线上问题是内存泄漏还是内存溢出?

可以这么区分:

  • 内存溢出 是结果,表示申请内存失败;
  • 内存泄漏 是原因之一,表示本来应该释放的对象一直被引用。
    判断上通常看:Full GC 后内存能不能明显回落;如果始终不怎么降,还在持续增长,就更像泄漏。再配合堆 dump 看大对象、引用链和 GC Roots,才能最终坐实。
154. 频繁 Full GC 通常有哪些原因?

常见原因包括:老年代空间压力过大、对象晋升过快、内存泄漏、大对象分配、元空间压力、显式触发 GC、参数设置不合理等。真正排查时,不能只看到"Full GC 多"就直接调参数,而是要先回答三个问题:谁占内存、为什么回不掉、为什么老年代压力这么大。

155. 如何结合 jstatjmapjcmdjstack、GC 日志进行排查?

一个比较稳的顺序是:

  1. 先用 jstat/GC 日志 看内存区变化和 GC 频率;
  2. 再用 jcmdjmap 看堆信息、对象分布、必要时导出 dump;
  3. 再用 jstack/线程 dump 看线程是否大量阻塞、等待、死锁;
  4. 最后把"内存、线程、下游依赖、发布变更"串起来。
    另外,Oracle 官方明确提到 jstackjmap 这些较早工具是实验性的,建议优先用 jcmd
156. 如何分析堆 dump 文件?你一般会重点看什么?

我一般会重点看四类信息:

  1. 大对象和大集合
  2. 对象数量异常多的类型
  3. GC Roots 到可疑对象的引用链
  4. ClassLoader 相关引用 ,以排查类加载器泄漏。
    思路上不是"看到对象多就结束",而是要顺着引用链找到"为什么它还活着"。
157. 为什么对象不多但 Old 区占用很高?

因为"对象数量少"不等于"对象占用小"。常见情况是:少量超大对象、长生命周期缓存对象、对象图很深、数组很大,或者对象虽然数量不多,但都活得久、被强引用链稳定持有。还有一种情况是年轻代配置和晋升策略导致对象过早进入老年代。

158. Survivor 区设置不合理会带来什么问题?

如果 Survivor 太小,很多本该在年轻代继续存活观察的对象会更快晋升到老年代,进而增加老年代压力;如果比例明显不合理,也可能影响复制成本和 GC 效率。换句话说,Survivor 设置不只是"小区大小问题",它会直接影响对象年龄分布和晋升路径。

159. GC 调优时为什么不能只盯着吞吐量?

因为吞吐量高不等于系统体验好。很多业务更敏感的是停顿时间、尾延迟、RT 抖动和可预测性。Oracle 的 GC 文档也一直强调,不同收集器和参数是在吞吐、暂停、内存占用之间做权衡。面试里这题的关键是表达:GC 调优不是只追求"总量快",还要关注"单次停顿和业务 SLA"。

160. 高并发接口频繁创建短生命周期对象会有什么后果?

短命对象很多时,年轻代分配和回收会非常频繁,虽然年轻代 GC 本来就擅长处理这类对象,但在高并发下仍可能带来更高的分配速率、更多 GC 次数和更明显的抖动。如果对象创建速度远高于回收节奏,还会把晋升压力传导到老年代。简单说:短命对象并不可怕,但短时间海量短命对象会把 GC 频率和分配压力一起抬高。

161. Java 内存模型(JMM)和 JVM 运行时内存结构是一回事吗?

不是一回事。

JMM 关注的是多线程并发下共享变量的可见性、有序性、原子性语义 ,本质上是内存访问规则;而 JVM 运行时内存结构讲的是程序运行时的数据区划分,比如堆、虚拟机栈、方法区、程序计数器等。一个偏"并发语义模型",一个偏"运行时内存分区"。

162. safepoint 和 saferegion 是什么?

safepoint 可以理解成 JVM 选定的一些"全局安全停顿点",在这些位置线程更容易停下来,便于做 GC、栈遍历、偏向锁撤销等需要观察一致性状态的工作。
saferegion 则是针对某些线程长时间不执行 Java 字节码、没法及时跑到 safepoint 的情况,告诉 JVM:"我现在处在一个安全区域里,停顿期间你可以先不用等我主动跑到 safepoint。"

163. 偏向锁、轻量级锁、重量级锁在新版本 JDK 中如何理解?

面试里可以这么答:这几个概念本质上都是 HotSpot 为了降低同步开销而做的锁优化路径,目标是在不同竞争强度下用不同成本的同步方式。

不过回答时要注意一句:这些属于 HotSpot 的具体实现优化细节,并不是 Java 语言规范层面的概念;新版本 JDK 对部分锁优化策略也经历过调整,所以面试更重要的是讲清"锁会随着竞争程度升级,目的是在无竞争和低竞争下减少代价"。

164. JIT 编译在 JVM 优化里起了什么作用?

JIT 的作用是把热点字节码在运行过程中编译成更高效的本地机器码,从而提升执行效率。

也就是说,Java 程序不是永远都"纯解释执行",而是会在运行过程中把真正经常执行的热点路径优化起来,所以 Java 的实际性能很大程度上取决于运行时优化,而不仅仅是源码长什么样。

165. 逃逸分析、标量替换、锁消除分别是什么?
  • 逃逸分析:分析一个对象是否会逃出当前方法或线程;
  • 标量替换:如果对象不会逃逸,JIT 可能把对象拆成若干标量变量来处理,而不一定真在堆上分配;
  • 锁消除:如果经过分析发现某个同步不会发生真实竞争,JIT 可能直接把锁相关开销优化掉。

这几个优化是一条链:先分析对象和作用域,再决定能不能减少分配、减少同步。

166. 为什么你看到的 Java 代码并不等于最终执行性能表现?

因为最终性能不仅取决于源码逻辑,还取决于类加载、JIT 编译、逃逸分析、锁优化、GC、线程调度、内存分配、CPU 缓存命中等一整套运行时行为。

所以两段"看上去差不多"的 Java 代码,在线上真实执行效果可能差很多。面试里说这题时,最好强调一句:Java 是强运行时优化语言,源码只是起点,不是终点。

167. 线上某服务机器内存够用,但 Full GC 频繁,可能是什么原因?

常见原因有:老年代对象回收不掉、对象晋升过快、大对象分配频繁、缓存无边界、元空间压力、显式触发 GC,或者 GC 参数和对象生命周期不匹配。

"机器内存够用"不代表"JVM 堆布局健康"。Full GC 频繁更说明的是 JVM 内部内存区压力和回收节奏出了问题,不是简单看 OS 空闲内存就能判断。

168. 一个服务重启后正常,运行几天后变慢,你怎么判断是 GC 问题还是资源泄漏?

我会先看几条趋势线:堆占用、Old 区变化、Full GC 频率、线程数、句柄数、连接池占用、直接内存、类数量。

如果 Full GC 后内存始终降不下来,或者某类对象、某类 ClassLoader、某类线程资源持续累积,就更像泄漏;如果主要是 GC 次数上来、停顿增多、对象 churn 很高,那更偏 GC 压力问题。判断关键是看 "重启后恢复"背后到底是释放了哪类累积资源

169. 如何排查"接口偶发性抖动很大,但平均 RT 还行"的 JVM 问题?

这种场景我会重点看尾延迟而不是均值,关注:短时间 STW、Mixed GC/Full GC 抖动、safepoint 停顿、JIT 编译预热、线程池瞬时拥塞、锁竞争尖刺。

工具上优先看 GC 日志、JFR、jcmd 输出和线程 dump,而不是只看平均指标。因为"平均 RT 还行"非常容易掩盖偶发性长尾。

170. 如果你要给支付核心链路选 GC 策略,你会怎么考虑?

我会优先考虑 停顿可预测性、尾延迟、稳定性、成熟度 ,而不是只看峰值吞吐。

支付核心链路通常更怕偶发长停顿,所以会更倾向低停顿、行为稳定、线上经验充分的方案,再结合堆大小、对象生命周期、发布环境做压测验证。面试里不要直接背"就选某个收集器",更好的回答是:先按业务 SLA 定目标,再按延迟/吞吐/堆规模权衡收集器。

171. BIO、NIO、AIO 的区别是什么?
  • BIO:阻塞式 IO,一个线程常常要盯一个连接或一次调用;
  • NIO:非阻塞 + 多路复用,一个线程可以管理多个连接就绪事件;
  • AIO:异步 IO,更强调"操作完成后通知我",而不是我反复去轮询就绪。

面试里简洁说就是:BIO 更直接,NIO 更适合高连接数,AIO 更强调异步完成通知。

172. 为什么 BIO 在高连接场景下扩展性差?

因为 BIO 模型里,连接多时往往需要大量线程去阻塞等待 IO,线程本身会带来栈空间、调度、上下文切换和资源管理成本。

所以连接一多,问题通常还没出在业务逻辑上,先出在线程数量和系统调度成本上了。

173. Channel、Buffer、Selector 分别是什么?

这是 Java NIO 的三个核心概念:

  • Channel:可以理解成更像"通道"的数据读写抽象;
  • Buffer:读写数据时的内存缓冲区;
  • Selector:多路复用器,用来监听多个 Channel 的事件。

一句话理解:Channel 负责连,Buffer 负责装,Selector 负责挑谁就绪。

174. Buffer 的 position、limit、capacity 分别表示什么?
  • capacity:缓冲区总容量;
  • position:当前读写位置;
  • limit:当前模式下可操作的边界。

常见理解方式是:写模式时 position 往后推进;切成读模式后,limit 变成原来的 position,position 回到开头,然后从头开始读到 limit。

175. Selector 为什么能支持单线程处理多连接?

因为它不是让一个线程同步阻塞盯一个连接,而是让一个线程统一监听多个 Channel 的就绪事件,谁可读、可写、可连接了,再去处理谁。

所以单线程不是"同时真的在处理所有连接",而是"用事件驱动避免了大量空等"。

176. Reactor 模型是什么?单 Reactor 和主从 Reactor 有什么区别?

Reactor 模型本质是:事件来了先分发,再由对应处理器处理。

  • 单 Reactor:一个 Reactor 负责连接接入和读写分发,结构简单,但单点压力大;
  • 主从 Reactor:主 Reactor 负责接入,从 Reactor 负责后续读写事件处理,更适合高并发场景。

一句话:单 Reactor 更简单,主从 Reactor 更容易扩展。

177. Reactor 和 Proactor 的差异是什么?

Reactor 更偏"事件就绪后我来处理",Proactor 更偏"异步操作完成后通知我结果"。

也就是说,Reactor 关注的是"准备好了",Proactor 更强调"已经做完了"。面试里别把这两个都笼统答成"异步 IO",最好点出它们处理时机不同。

178. Netty 为什么比原生 NIO 更易用?

因为 Netty 把原生 NIO 很多繁琐和容易出错的部分封装掉了,比如事件循环、编解码、连接管理、粘包拆包、内存管理、Pipeline 扩展、心跳机制等。

原生 NIO 能做,但工程复杂度高;Netty 的价值就在于把这些通用套路抽象成了更成熟的网络编程框架。

179. Netty 的 EventLoop、ChannelPipeline、ByteBuf 分别是什么?
  • EventLoop:负责事件循环和任务执行;
  • ChannelPipeline:责任链式处理器管道,入站出站事件都沿着它流转;
  • ByteBuf:Netty 自己的缓冲区抽象,比原生 ByteBuffer 更灵活。

如果一句话概括:EventLoop 管调度,Pipeline 管处理流程,ByteBuf 管数据承载。

180. ByteBuf 为什么比 ByteBuffer 更适合工程使用?

因为 ByteBuf 在读写索引、扩容、切片、引用计数、池化等工程能力上更灵活,使用起来也更顺手。

而且很多网络框架场景对内存管理、零拷贝、池化复用都很敏感,ByteBuf 更贴合这些需求。

181. Netty 如何处理粘包拆包?

核心思路是:应用层自己定义消息边界。常见方式有固定长度、分隔符、长度字段、特定协议头等。

Netty 价值在于它已经提供了成熟的编解码器和 Pipeline 机制来承接这些方案,所以不是"Netty 自动帮你知道一条消息在哪结束",而是"Netty 帮你更方便地落地消息边界协议"。

182. 心跳机制在长连接系统中的作用是什么?

心跳主要用来做三件事:

  1. 判断连接是否还活着;
  2. 更快发现断链、半开连接、空闲连接异常;
  3. 维持某些中间设备下的连接活性。

在长连接系统里,真正的问题往往不是"有没有建连成功",而是"连接是不是早就死了但双方还没意识到"。心跳就是为了解决这个感知滞后问题。

183. 连接池、线程池、对象池分别解决什么问题?
  • 连接池:复用昂贵连接资源,减少频繁创建和销毁;
  • 线程池:复用线程并控制并发度,减少线程创建成本和资源失控;
  • 对象池:复用构造昂贵或初始化成本高的对象。

三者本质上都是"池化复用",但各自控制的是不同类型的稀缺资源。

184. 数据库连接池为什么不能无限增大?

因为连接不是越多越好。数据库端处理能力、锁竞争、事务管理、上下文切换、内存占用都有限,连接池无限增大只会把压力更快打到数据库核心资源上。

所以连接池配置本质上是系统级并发闸门,而不是"越大越稳"。

185. 连接池参数应该如何设计?你一般关注哪些指标?

通常会结合业务 RT、下游并发能力、数据库容量、峰值流量来定,重点关注:池大小、等待时间、获取超时、活跃连接数、空闲连接数、借还耗时、错误率。

设计连接池时不能只看"平时够不够",还要看高峰、毛刺流量和故障放大时会不会排队雪崩。

186. 高并发网关服务为什么更适合 NIO / Reactor 模型?

因为网关通常连接数多、请求短、网络等待多,如果按 BIO 一连接一线程,线程和上下文切换成本会非常高。

NIO / Reactor 更适合把大量连接事件集中管理,用少量线程处理大量 socket 就绪事件,所以在高连接、高并发入口层更有优势。

187. 如果连接数很高但吞吐不高,你会重点看什么?

我会重点看:事件循环线程是否忙、是否有大量空闲连接、下游依赖是否拖慢、业务处理是否阻塞了 IO 线程、心跳和长连接保活是否占了太多资源。

高连接数不等于高吞吐,很多系统其实是"挂着很多连接,但真正业务推进很慢"。所以要先判断瓶颈在 连接管理、事件循环,还是业务处理

188. Netty 服务出现内存泄漏,你会怀疑哪些点?

首要怀疑的是 ByteBuf 的引用计数没处理好,比如 retain/release 不匹配;其次是 Pipeline 里自定义 handler 留住了对象引用,或者连接、任务、缓存没有及时释放。

这类问题的排查思路和普通 JVM 泄漏类似,都是先定位对象增长,再追引用链,只不过网络框架里要更关注池化内存和缓冲区生命周期。

189. 大量短连接接入时,应用层和内核层分别应该如何优化?

应用层会更关注连接复用、减少不必要握手、控制线程模型、优化超时和队列;内核层则更关注监听队列、半连接队列、端口范围、TCP 参数、连接回收等。

一句话:应用层要减少"建连成本",内核层要提升"接住和处理连接洪峰的能力"。

190. B+ 树为什么适合作为数据库索引结构?

因为数据库索引核心目标不是单纯减少比较次数,而是减少磁盘页访问次数、支持范围查询并提升顺序访问效率。

B+ 树多叉、层高低、叶子节点天然有序,非常适合页式存储和范围扫描,所以比红黑树这类更偏内存结构的方案更适合数据库索引。

191. 聚簇索引和非聚簇索引的区别是什么?

聚簇索引的叶子节点通常直接存放整行数据,表数据本身就按主索引顺序组织;非聚簇索引的叶子节点更多是索引键加定位信息,查到索引后可能还要再回到主数据位置取整行。

所以它们的差别不只是"一个主键一个普通键",更在于数据和索引是否组织在一起

192. 什么是回表?什么是覆盖索引?
  • 回表:先通过二级索引找到记录定位信息,再回到主数据位置取需要的列;
  • 覆盖索引:查询需要的列都已经在索引里,不需要再回主表取数据。

一句话:回表多一次取数动作,覆盖索引省掉这一步。

193. 最左前缀原则是什么?

它说的是联合索引在匹配条件时,通常要从索引最左边开始连续使用,才能更有效地利用索引能力。

简单说,联合索引不是"想用哪个字段就能单独完美命中哪个字段",而是和字段顺序强相关。

194. 联合索引失效的常见场景有哪些?

常见包括:没从最左列开始用、在索引列上做函数或计算、范围条件过早截断后续匹配、类型隐式转换、模糊匹配不走前缀、条件分布让优化器觉得全表更划算。

回答这题时,不要只背口诀,最好补一句:"失效"很多时候不是物理上完全不能用,而是优化器综合判断后没有选它。

195. 为什么索引能加速查询,却可能拖慢写入?

因为每次插入、删除、更新数据时,相关索引也要同步维护。索引越多,写放大越明显;而且索引维护还会带来页分裂、日志增加、缓存压力和锁竞争。

所以索引本质上是在"读性能"和"写成本"之间做交换。

196. 你平时怎么看执行计划?重点关注哪些字段?

我会重点看:访问方式、扫描行数、是否走索引、是否回表、是否出现排序/临时表/聚合代价、连接顺序,以及估算行数是否合理。

执行计划不是只看"有没有索引",而是看优化器打算怎么走、代价高在哪里、估算是否失真。

197. MySQL 和 PostgreSQL 在执行计划分析上,你分别会看哪些核心信息?

共同点都是看访问路径、行数估算、过滤比例、连接顺序和高成本节点。

区别上,PostgreSQL 更常结合 EXPLAIN ANALYZE 去看"估算"和"实际"差距,而 MySQL 面试里更常会讲是否走索引、回表、Using filesort、Using temporary 这类执行特征。核心仍然是:看优化器判断和真实执行是否一致。

198. PostgreSQL 里的 EXPLAIN ANALYZE 能帮你看到什么?

它不仅能展示优化器的执行计划,还能展示真正执行时的耗时和实际行数。

所以它很适合用来判断:到底是计划估错了,还是计划本身就不合理。这个思路比单看"预计怎么跑"更接近真实问题定位。

199. 为什么"有索引"不等于"一定走索引"?

因为数据库优化器会做代价评估。如果它判断走索引需要扫描太多、回表太贵、排序成本太高,或者统计信息让它认为全表扫描更划算,那就可能不选索引。

所以索引只是"提供一种可能的访问路径",不是"强制必走"的命令。

200. ACID 分别是什么?

ACID 是事务的四个核心特性:

  • Atomicity(原子性):要么都成功,要么都失败;
  • Consistency(一致性):事务前后数据满足约束和规则;
  • Isolation(隔离性):并发事务之间彼此隔离;
  • Durability(持久性):提交后的结果应被持久保存。

面试里最好补一句:ACID 不是四个孤立概念,而是数据库事务设计要一起满足的一组目标。

201. 四种事务隔离级别分别解决了什么问题?

标准四种隔离级别是:READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE。隔离级别越高,并发异常越少,但并发开销通常也越大。MySQL InnoDB 支持这四级,默认是 REPEATABLE READ;PostgreSQL 名义上也支持四级,但内部实际上实现为三种,READ UNCOMMITTED 会按 READ COMMITTED 处理。

202. 脏读、不可重复读、幻读分别是什么?
  • 脏读:读到了别的事务尚未提交的数据。
  • 不可重复读:同一事务里,两次读取同一行,结果不一样,通常是别的事务提交了更新。
  • 幻读 :同一事务里,两次按相同条件查询,结果集行数变了,通常是别的事务插入或删除了满足条件的行。
    记法可以是:脏读看"未提交",不可重复读看"同一行值变化",幻读看"结果集成员变化"。
203. MySQL 的 RR 为什么很多时候能避免幻读?

因为 InnoDB 在需要加锁的范围查询里会使用 next-key locking ,也就是"记录锁 + gap lock",不仅锁住已存在的索引记录,还会锁住记录之间的间隙,从而阻止别的事务在这个范围里插入新行,所以很多面试场景下会说 MySQL 的 REPEATABLE READ 能避免幻读。要注意,这个结论主要针对 InnoDB 的锁定读/更新删除等场景,不是说所有读都靠同一种机制解决。

204. MVCC 的核心思想是什么?

MVCC 的核心思想是:不给读操作和写操作强行互相阻塞,而是让读在某个一致性视图上看"数据版本",从而提高并发能力。PostgreSQL 官方明确强调,MVCC 的重要优势就是读锁不和写锁冲突,从而尽量做到读不阻塞写、写不阻塞读;InnoDB 也把一致性非锁定读作为 RC 和 RR 下普通 SELECT 的默认模式。

205. Read View 是什么?

在 InnoDB 里,Read View 可以理解成一次一致性读所依据的"可见性快照规则"。在 READ COMMITTED 下,通常每次一致性读都会建立新快照;在 REPEATABLE READ 下,同一事务内普通一致性读一般会复用第一次读时建立的快照,所以同一事务里多次普通 SELECT 能看到一致结果。

206. undo log 在 MVCC 里起了什么作用?

undo log 记录的是"如何撤销最近一次修改"的信息。它一方面服务于事务回滚,另一方面也为 MVCC 提供历史版本基础,使一致性读能够沿着版本链看到更早的可见数据版本。MySQL 官方对 undo tablespace 的定义就是:其中包含 undo logs,而 undo logs 记录了如何撤销事务对聚簇索引记录最近一次修改的信息。

207. redo log 和 binlog 的区别是什么?

可以这样记:

  • redo log 更偏 InnoDB 存储引擎内部的物理/页级恢复日志,核心作用是崩溃恢复,保证已提交事务的持久性。
  • binlog 更偏 MySQL Server 层的逻辑变更日志,核心用途包括复制和基于时间点恢复。
    MySQL 官方对 redo log 的表述是:它是崩溃恢复时用来修正未完整写入数据页的磁盘结构;对 binlog 的表述则是:它记录描述数据库变更的 events,并用于复制与 point-in-time recovery。
208. 两阶段提交为什么存在?

两阶段提交存在的根本原因,是为了让多个参与方在"要么都提交、要么都回滚"这件事上达成一致。MySQL 在 XA 事务文档里明确写到,全局事务使用 two-phase commit:第一阶段各分支进入 prepare,记录到稳定存储并表态能否提交;第二阶段再统一 commit 或 rollback。工程上你可以把它理解成:先让大家都说"我准备好了",再统一拍板。

209. PostgreSQL 的 MVCC 思想和 MySQL 有哪些相似与差异?

相似点是两者都用多版本并发控制来降低读写冲突,提高并发读性能。差异上,PostgreSQL 官方明确说明它的 READ UNCOMMITTED 实际按 READ COMMITTED 实现,并且在最严格级别下通过 SSI 来保证更强隔离;而 InnoDB 在 REPEATABLE READ 下则大量依赖一致性读和 next-key/gap locking 的组合。简单说:二者都靠 MVCC,但具体的快照语义、锁策略和隔离级别实现细节并不一样。

210. 行锁、表锁、间隙锁、Next-Key Lock 分别是什么?
  • 行锁:锁住具体索引记录。
  • 表锁:锁住整张表。
  • 间隙锁(gap lock):锁住索引记录之间的区间,甚至可能是空区间。
  • next-key lock :记录锁 + 前面间隙上的 gap lock。
    MySQL 文档对 next-key lock 的定义非常直接:它是"对索引记录的记录锁 + 其前面间隙的 gap lock"的组合。
211. 什么 SQL 可能触发间隙锁?

典型是带范围条件的锁定读、UPDATEDELETE,尤其是基于索引范围扫描时。MySQL 官方说明:锁定读、UPDATEDELETE 一般会对扫描到的每个索引记录加锁,而且通常是 next-key locks,因此也会阻止在相应 gap 中插入。一个非常典型的例子就是 SELECT ... WHERE c1 BETWEEN 10 AND 20 FOR UPDATE 这类范围锁定读。

212. 为什么范围查询更容易引发锁冲突?

因为数据库锁住的往往不是"最终返回的结果行",而是"扫描过的索引范围"。MySQL 官方明确说过,InnoDB 只知道扫描了哪些索引范围,并不记住精确的 WHERE 条件,所以锁定读、更新、删除通常会锁住扫描到的索引记录,且经常伴随 next-key lock。范围一大,覆盖的记录和 gap 都更大,自然更容易冲突。

213. 死锁是如何产生的?数据库如何检测死锁?

死锁本质是两个或多个事务互相等待对方持有的锁,形成循环等待。InnoDB 默认开启死锁检测,会自动检测事务之间的死锁并回滚一个事务来打破死锁;MySQL 官方还说明它会尽量选择"较小"的事务回滚,大小依据是受影响的行数。

214. 遇到数据库死锁你会怎么定位?

MySQL 官方给出的直接办法是看 SHOW ENGINE INNODB STATUS,它可以显示最近一次 InnoDB 用户事务死锁;如果死锁频繁,还可以打开 innodb_print_all_deadlocks 把全部死锁信息打印到错误日志。实际排查时,我会结合 SQL 模板、索引情况、事务顺序、是否存在范围锁/批量更新一起看。

215. 一条 SQL 很慢,你会从哪些角度优化?

我一般按这条线看:执行计划、索引是否命中、扫描行数、是否回表、是否排序/临时表、统计信息是否准确、SQL 写法是否导致函数/隐式转换、表数据分布是否偏斜。PostgreSQL 官方也强调,EXPLAIN/EXPLAIN ANALYZE 的关键价值就是看计划是否合理,以及估算值和真实执行是否接近。

216. count(*) 一定慢吗?什么时候慢?什么时候不慢?

不一定。count(*) 慢不慢主要取决于访问路径和需要扫描多少数据。对小表、覆盖索引、条件很强的情况,它未必慢;对大表全量统计、过滤条件差、索引帮不上忙时,就会慢。本质不是 count(*) 这个写法本身有罪,而是"为了算这个 count,数据库到底要扫多少、怎么扫"。这和执行计划、统计信息、索引设计直接相关。

217. 为什么 select * 常常不是好习惯?

因为它会增加不必要的列读取、网络传输和对象映射开销,还会降低覆盖索引命中的可能性。很多时候你本来只需要几个字段,但 select * 逼数据库把整行都拿出来,二级索引场景下还更容易回表。对大宽表、热点接口和高并发链路,这个坏处会更明显。这个结论本质上是执行计划和数据访问成本的直接结果。

218. limit 深分页为什么会慢?

因为像 LIMIT 100000, 20 这种深分页,数据库通常还是得先找到前面的 100000 行,再丢掉,只返回后 20 行;如果还伴随排序、回表,代价会更高。很多系统会改用"基于上次游标/主键/时间戳"的 seek 方式,而不是纯 offset 分页。这个原理本质上和执行计划的扫描成本有关。

219. order by、group by、join 分别有哪些常见性能坑?
  • ORDER BY:排序列没法利用索引时,容易出现额外排序开销。
  • GROUP BY:分组键选择差、数据量大时,容易出现高内存/临时结构开销。
  • JOIN:驱动表选错、连接条件无索引、行数估算失真时,性能会很差。
    核心不是背某个关键词,而是看:有没有走到高代价的排序、聚合和大范围连接路径。
220. 大事务为什么危险?

大事务常见风险是:锁持有时间长、阻塞别的事务、undo/redo/binlog 压力大、回滚代价高、复制延迟放大、失败重试成本高。事务一旦很大,问题往往不是单条 SQL 快不快,而是它把并发和恢复都拖慢了。尤其在复制链路和高写入场景里,大事务很容易放大抖动。

221. 线上慢查询突增,但 CPU 不高,你先查什么?

我会先查:是不是锁等待、磁盘/日志刷盘、复制延迟导致读到旧副本、连接池排队、统计信息失真、执行计划突变。CPU 不高往往说明数据库不是"算不过来",而是"在等":等锁、等 IO、等资源。这个时候看慢日志、执行计划、锁等待和磁盘指标通常比盯 CPU 更有效。

222. 某张表数据量很大,更新突然变慢,你会怀疑什么?

我会优先怀疑:索引过多导致维护成本高、更新列是否命中多个索引、是否发生页分裂、是否被范围锁/行锁阻塞、是否有大事务、是否 binlog/redo 刷盘压力上升、统计信息不准导致执行路径变差。大表更新变慢很少只是"表大"三个字,更多是"写路径上的放大环节变多了"。

223. 读多写少场景下,你会如何设计索引体系?

读多写少时,我会更积极地为高频查询模式设计联合索引、覆盖索引,并按最常用过滤条件和排序条件排字段顺序,尽量减少回表和排序。因为这类场景索引维护成本相对能接受,收益更多体现在查询延迟下降和数据库 CPU/IO 压力下降上。核心是围绕"主要查询路径"设计,而不是盲目给每个字段都加索引。

224. 写多读少场景下,你会如何控制索引数量?

写多读少时,索引会更谨慎,因为每多一个索引,写入时都要维护一次。我的原则通常是:只保留真正能支撑核心查询和约束的索引,能合并就合并,能删掉低收益索引就删掉,避免把写链路拖垮。因为索引本身就是用写放大换读优化。

225. 如何排查"数据库没挂,但应用 RT 飙高"这种问题?

我会把"数据库没挂"拆开看:连接池是不是满了、慢 SQL 是不是增多了、是不是锁等待、是不是读写分离读到了延迟副本、是不是事务变大了、是不是应用端重试把数据库压在边缘状态。很多时候数据库还能响应,不代表它没成为链路瓶颈,只是瓶颈表现成排队和等待,而不是直接宕机。

2. 数据库高可用与分布式

226. 主从复制的基本原理是什么?

MySQL 官方定义得很直接:复制就是把一个 source 上的数据变化复制到一个或多个 replica。binlog 记录变更事件,副本再去接收并重放这些事件;而且 MySQL 复制默认是异步的。你可以把它理解成:主库负责写和产生日志,从库负责按顺序追日志。

227. 为什么会出现主从延迟?主从延迟会带来什么业务问题?

因为复制默认异步,副本要接收、传输、落地、重放主库事件,这个链条任何一环变慢都可能造成延迟。常见原因包括大事务、从库压力大、IO 慢、单线程重放瓶颈或网络抖动。业务上最直接的问题就是"刚写完就读不到"、读到旧数据、读写分离场景下用户感知不一致。

228. 读写分离的核心收益是什么?风险点有哪些?

核心收益是把读流量从主库分出去,提高整体读能力和主库写稳定性。MySQL Router 的读写分离文档也明确说明:可以把读流量发往只读实例,把写流量发往读写实例。风险主要是复制延迟、一致性读问题、路由错误、故障切换复杂度和只读副本压力失衡。

229. 分库分表的常见拆分维度有哪些?

常见维度有:按租户、用户 ID、订单号、地域、时间、业务域等。选择维度时看的是数据分布、查询模式、热点集中程度和扩容方式。原则不是"能拆就拆",而是挑一个既能打散流量、又尽量贴合主要查询路径的分片键。这个更多是工程设计经验总结。

230. 水平拆分和垂直拆分分别适合什么场景?
  • 垂直拆分:按业务域或表职责拆,把不同表/模块拆开,适合单库里业务耦合太多、不同模块资源争抢明显的场景。
  • 水平拆分 :把同一张大表的数据按某个分片键切到多个库/表,适合单表数据量、写入量、热点流量都太大。
    一句话:垂直拆"种类",水平拆"规模"。这属于分布式数据库架构常见设计范式。
231. 分库分表后,分页、排序、聚合、唯一约束分别会遇到什么问题?

会遇到典型的"跨分片问题":

  • 分页:需要跨分片合并结果,offset 深时代价高。
  • 排序:各分片先排,再全局归并。
  • 聚合:通常先分片内聚合,再全局聚合。
  • 唯一约束 :单分片能保证,不代表全局能天然保证。
    所以很多看似简单的单库能力,分片后都变成"局部正确 + 全局合并"的问题。
232. 全局主键如何设计?雪花算法有什么优缺点?

全局主键常见方案有数据库号段、Redis/缓存发号器、Snowflake 类时间序列 ID、UUID 等。Snowflake 的经典思路是把时间戳、机器标识和序列号组合进一个整数里,X 当年公开 Snowflake 时就是这么做的。

优点是:分布式可生成、趋势递增、索引友好度通常比 UUID 好。

缺点是:依赖时钟、机器号管理要稳、严格全局连续做不到。

233. UUID 为什么不一定适合做主键?

因为 UUID 往往比较长、无序或近似随机,作为主键会让索引更大、页分裂和缓存局部性更差,对 B+ 树插入模式不够友好。它的好处是生成简单、全局唯一,但如果你非常在意写入局部性和索引性能,UUID 通常不是第一选择。这是数据库索引组织方式直接决定的工程权衡。

234. 分布式事务常见方案有哪些?

常见有:XA/2PC、TCC、Saga、本地消息表/事务消息这几类。MySQL 官方 XA 文档明确对应 2PC;Oracle 的微服务事务文档把 XA、Saga、TCC 都列为主流协议;microservices.io 也把 Saga 定义成一系列本地事务加补偿事务的模式。

235. 2PC、TCC、Saga、本地消息表分别适合什么场景?
  • 2PC/XA:适合参与方都支持 XA、强一致要求高、能接受协调开销的场景。
  • TCC:适合业务能显式拆成 Try/Confirm/Cancel,并且资源预留语义明确的场景。
  • Saga:适合跨服务长链路、可接受补偿和最终一致的场景。
  • 本地消息表 :适合"本地事务 + 异步可靠投递"模型,工程实现相对务实。
    这些结论和 Oracle 对 XA/TCC/Saga 的协议定义,以及 Saga 的"本地事务 + 补偿事务"模式是一致的。
236. 为什么很多业务里会优先选择最终一致性,而不是强一致分布式事务?

因为强一致分布式事务往往带来更高的耦合、协调、阻塞和可用性代价。Saga 模式之所以流行,就是因为它把跨服务事务拆成本地事务,再用消息/事件和补偿实现最终一致,通常更贴合微服务的自治和可用性需求。换句话说,很多业务不是"不想强一致",而是"综合成本后觉得最终一致更划算"。

237. CAP 和 BASE 应该如何落到工程设计上?

CAP 的核心是:在发生网络分区时,系统无法同时完整满足强一致和可用性;这是 Gilbert 和 Lynch 形式化证明过的。落到工程上,就是先问你的业务在分区时更不能接受什么:拒绝服务,还是读到旧数据/不一致数据。

BASE 则更像一种工程取向:在一些场景里接受基本可用、软状态和最终一致,用补偿、幂等、重试、异步化去换更高可用和扩展性。

238. 如果一个核心写链路 TPS 很高,你如何设计数据库层高可用架构?

我会优先考虑:主库专注写、只读副本分担读、写链路尽量短事务、冷热数据分离、必要时按业务维度分库分表、主从/集群做故障切换,并把消息异步化、索引数量和大事务都控制好。因为高 TPS 写链路真正怕的是:锁竞争、刷盘瓶颈、复制延迟、写放大和故障切换抖动。MySQL 官方文档里复制、binlog、读写分离这些能力本身就是这类架构的基础积木。

239. 分库分表后如何做跨分片查询?

常见思路有三类:

  1. 应用层路由 + 聚合:先打到相关分片,再在应用层合并。
  2. 中间件/代理层聚合:让中间件帮你做路由和结果归并。
  3. 搜索/分析侧旁路 :把复杂跨分片查询转移到 ES、OLAP 或离线数仓。
    如果跨分片查询很多,往往说明你的拆分键和主要查询路径不太匹配,需要重新审视分片设计。这个属于分库分表通用工程方法论。
240. 如果业务要求"全局唯一 + 大致递增 + 高可用",你如何设计分布式 ID?

我会优先考虑 Snowflake 类方案,或者"号段 + 多节点容灾"的发号器。Snowflake 的思路天然满足全局唯一和趋势递增,且不依赖单点数据库自增;但要把机器号分配、时钟回拨、节点故障切换处理好。

如果业务还要求极高稳定性,我会把发号服务做成多节点、监控时钟漂移,并预留降级方案。核心目标是:唯一性优先,趋势递增其次,绝对连续通常不强求。

241. 订单创建成功但库存没扣减,你会如何做最终一致性补偿?

我一般会把"下单"和"扣库存"拆成两个本地事务,中间通过可靠消息或本地消息表衔接。订单先成功落库并记录待扣库存事件,再异步驱动库存服务扣减;如果库存扣减失败,就做重试、告警、人工补偿,或者把订单状态置为待处理/失败关闭。核心不是强行上分布式强一致,而是把链路设计成可重试、可幂等、可补偿、可追踪。MySQL 官方对 XA/2PC 的描述说明了强一致事务的协调成本,而工程里很多业务会更倾向基于异步事件的最终一致。

242. 读写分离场景下,如何解决"刚写入就读不到"的问题?

根因是 MySQL 复制默认是异步的,副本存在复制延迟,所以刚写完立刻去读副本,可能读到旧数据。常见做法有:写后短时间内强制读主库、按用户或会话做读主粘滞、按复制位点/延迟判断副本是否可读,或者对强一致读取场景直接只读主库。也就是说,读写分离不是免费午餐,一致性需求高的读不能盲目打到副本


四、Redis 专题

1. Redis 基础

243. Redis 为什么快?

Redis 快,核心原因通常可以概括成:基于内存、数据结构简单高效、单线程事件处理减少了很多锁竞争、网络模型高效、很多操作时间复杂度低。另外,Redis 官方文档也强调了它是一个 data structure server,原生支持多种高效数据类型;而性能优化文档则说明 Redis 通常处理时间极低,延迟问题更多是出在系统条件而不是单条命令本身。

244. Redis 常见数据类型有哪些?分别适合哪些业务场景?

常见有:

  • String:缓存对象、计数器、分布式锁 token
  • Hash:对象属性存储
  • List:消息队列、时间序列尾插
  • Set:去重、共同好友、标签集合
  • ZSet:排行榜、延时任务、按分数排序
  • Bitmap:签到、布尔状态位
  • HyperLogLog:近似去重计数
  • Geo:附近的人/门店检索

Redis 官方数据类型文档就是按这些能力组织的,而且明确把这些类型和缓存、队列、事件处理等场景联系在一起。

245. String、Hash、List、Set、ZSet 的底层编码会变化吗?为什么?

会。Redis 会根据元素数量、元素大小和数据特征,在更紧凑和更通用的内部表示之间切换,目的是在内存占用和操作性能之间做平衡。面试里不一定要死背所有内部编码名字,但要知道:Redis 的外部数据类型是稳定语义,内部编码是为了节省内存和提升效率而动态调整的实现细节。Redis 官方把这些类型定义为原生数据结构,强调的是语义和能力,而不是要求业务直接依赖内部编码。

246. Bitmap、HyperLogLog、Geo 分别适合解决什么问题?
  • Bitmap:适合海量布尔位状态,比如签到、活跃天数、在线标记
  • HyperLogLog:适合大规模去重计数,牺牲精确性换很小内存
  • Geo:适合地理位置附近搜索、门店距离计算

Redis 官方数据类型页和 Bloom/概率结构文档都明确把 HyperLogLog 这类结构定义为近似计数工具,而 Geo 则是专门面向地理空间索引与查询。

247. Redis 持久化 RDB 和 AOF 的区别是什么?
  • RDB:更像某个时刻的数据快照,恢复快,文件通常更紧凑,但可能丢失最后一次快照之后的数据
  • AOF:记录写命令追加日志,数据丢失窗口通常更小,但文件更大、恢复时通常要重放命令

Redis 官方持久化文档明确把 RDB 和 AOF 作为两种主要持久化方式来介绍,并强调它们在恢复速度、数据安全窗口和磁盘行为上的权衡。

248. AOF 重写的目的是什么?

AOF 重写的目的是把旧 AOF 文件里大量重复、冗余的写操作压缩成更少的命令,以减小文件体积、降低恢复重放成本。它不是把语义改了,而是把"达到当前状态所需的最小命令集合"重新整理出来。Redis 官方持久化文档专门说明了 AOF rewrite 的作用和它与 RDB/AOF 后台任务之间的协调。

249. Redis 过期键删除有哪些策略?

Redis 官方说明,过期主要有两种方式:

  • 被动过期:客户端访问 key 时,发现已经过期就删除
  • 主动过期:Redis 周期性随机抽样检查带过期时间的 key,把已经过期的删掉

所以它不是靠单一"定时器扫全表",而是用被动 + 主动结合的方式控制开销。

250. Redis 内存淘汰策略有哪些?如何选择?

Redis 官方说明,达到内存上限后会按配置的 eviction policy 驱逐 key。常见策略包括:

  • noeviction:不淘汰,直接报错
  • allkeys-lru / allkeys-lfu:从所有 key 里按 LRU/LFU 淘汰
  • volatile-lru / volatile-lfu / volatile-ttl / volatile-random:只在设置了 TTL 的 key 里淘汰

选择上一般是:纯缓存更常用 allkeys-lru 或 allkeys-lfu;混合存储时更谨慎,可能更偏 noeviction 或只淘汰带 TTL 的数据。

251. 为什么 Redis 适合做缓存,但不适合盲目替代数据库?

因为 Redis 的定位首先是高性能内存数据结构服务器,它确实支持持久化,但在复杂查询、关系约束、事务语义、海量持久数据治理上,和关系数据库并不是一个赛道。Redis 官方对持久化的描述也很清楚:它提供的是数据落盘能力,不意味着它天然就等价于传统数据库的完整数据管理能力。换句话说,Redis 很擅长"快",但不该被误用成"什么都往里放的唯一真相库"。

252. Redis pipeline 的核心价值是什么?

Redis 官方对 pipelining 的定义很直接:一次发送多条命令,而不是每条都等返回,目的是减少网络 RTT 带来的开销。所以 pipeline 的核心价值不是"把多条命令变原子",而是批量发命令,减少来回往返,提高吞吐。这对网络延迟明显或批量写入场景特别有价值。

2. Redis 高并发问题

253. 缓存穿透、缓存击穿、缓存雪崩分别是什么?
  • 缓存穿透:请求的数据本来就不存在,缓存和数据库都查不到,流量直接打数据库
  • 缓存击穿:某个热点 key 突然失效,大量并发同时回源
  • 缓存雪崩:大量 key 在同一时间段集中失效,或者缓存层整体故障,导致大量请求同时打到下游

这三者的差异重点是:穿透是"根本不存在",击穿是"单个热点失效",雪崩是"大片缓存同时失效"。这是 Redis 作为缓存使用时的典型工程问题。

254. 缓存击穿为什么常发生在热点 Key 上?

因为热点 key 在有效期内会拦住绝大多数请求,一旦它失效,瞬时并发会一起穿透到数据库或下游服务,形成回源洪峰。普通 key 即使失效,影响也小;真正危险的是高并发集中访问的热点 key。这个结论和 Redis 作为高频缓存层的使用方式直接相关。

255. 布隆过滤器适合解决什么问题?它的代价是什么?

Redis 官方 Bloom filter 文档说明,它适合用极小内存判断某元素"可能存在/一定不存在"。所以它很适合挡缓存穿透:如果过滤器判断一定不存在,就不用再查数据库。代价是它是概率型结构,会有假阳性,但不会有假阴性,也就是可能误判"存在",但不会把已存在元素误判成不存在。

256. 热 Key 会带来什么问题?如何发现热 Key?

热 key 会导致单 key 请求集中到单分片/单节点,带来 CPU 抖动、网络热点、延迟上升,严重时会拖垮整个实例的局部性能。发现手段通常包括:业务侧热点统计、代理层监控、Redis INFO 和延迟监控、客户端埋点,以及排查某些命令或 key 的异常访问频率。Redis 官方提供了 INFO 和 latency monitoring 这类诊断能力,可用于观察实例状态和延迟问题。

257. 大 Key 会带来什么问题?如何治理?

大 key 常见问题是:单次读取或删除耗时长、网络传输大、主从复制压力大、持久化和 fork 时内存开销更明显,还可能导致命令阻塞。治理思路通常是:拆 key、改数据模型、分片存储、限制单 key 元素数量、对删除采用异步或分批策略。大 key 的本质问题不是"占内存大"这么简单,而是它会放大很多路径上的单次操作成本。

258. 缓存和数据库一致性为什么天然难?

因为它本质上是两个独立系统:一个快、一个慢;一个可能内存淘汰,一个是持久存储;更新顺序、失败时机、重试、网络抖动都可能让二者短时间不一致。尤其 Redis 还有过期、淘汰、主从复制等机制,所以一致性不是"写完两边就完事",而是必须面对时序、失败和重放问题。

259. 旁路缓存模式是什么?

旁路缓存,也就是 cache-aside,典型流程是:先读缓存,未命中再读数据库并回填缓存;更新时先更新数据库,再删除缓存。它的优点是实现简单、业务可控,是最常见的缓存模式。缺点是天然有短暂不一致窗口,需要通过过期策略、重试和业务约束来兜底。这个模式和 Redis 作为缓存层的典型用法完全匹配。

260. 为什么很多场景选择"先更新数据库,再删除缓存"?

因为如果你"先删缓存,再更新数据库",在数据库更新完成前,其他请求可能又把旧值从数据库读出来并写回缓存,导致脏缓存重新出现。先更新数据库,再删缓存,虽然也不是绝对零风险,但更符合"数据库是真实来源,缓存是派生副本"的思路,通常能把不一致窗口控制得更小。

261. 延迟双删的适用前提是什么?它有哪些不确定性?

延迟双删适用于你明确知道:更新数据库后,可能还有并发旧读把旧值重新写回缓存,于是先删一次,更新后再延迟删一次,试图把脏回填覆盖掉。问题在于它对"延迟多久"高度敏感,且受线程调度、网络抖动、重试时序影响,不能从根上消灭一致性问题。所以它更像经验型补丁,不是银弹。

262. 订阅 binlog / MQ 通知做缓存更新的优缺点是什么?

优点是能把缓存更新从主业务链路解耦,数据库一旦确认变更,就通过 binlog 或消息异步驱动缓存刷新,比较适合多系统共享缓存或复杂更新链路。缺点是链路变长了:消息丢失、消费延迟、重复消费、顺序问题都会把一致性问题转移到消息系统治理上。也就是说,它提升了解耦性,但引入了新的可靠性治理成本。MySQL binlog 本身就是数据库变更事件的官方日志机制。

263. 多级缓存一般怎么设计?

常见是 本地缓存 + Redis + 数据库 三层。热点特别高的数据优先命中本地缓存,Redis 作为共享缓存层,数据库作为最终真相源。设计时要关注容量上限、过期策略、回源保护、降级开关和一致性边界。多级缓存的收益是进一步降低下游压力和 RTT,代价是同步复杂度更高。

264. 本地缓存 + Redis 两级缓存要注意什么一致性问题?

要特别注意:Redis 更新了,不代表每台应用机器的本地缓存也同步更新;本地缓存通常是最容易脏的那一层。所以要有版本号、短 TTL、消息通知、主动失效或刷新机制。否则会出现 Redis 已经是新值,但某台机器本地还在读旧值。多级缓存收益越大,一致性治理就越重要。

265. 过期时间为什么通常要加随机值?

因为如果一批 key 同时用固定 TTL,就容易在同一时刻集中失效,引发缓存雪崩。给 TTL 加随机抖动,可以把失效时间打散,避免回源流量在同一个时间点爆发。这和 Redis 官方的过期/淘汰机制并不冲突,属于缓存系统常见的工程保护手段。

266. 对于热点数据,TTL 应该怎么设计才更稳妥?

热点数据通常不适合简单设一个很短 TTL 然后放任自然过期,否则容易触发击穿。更稳妥的做法是:长 TTL + 主动更新、逻辑过期 + 后台刷新、互斥回源、预热和降级组合使用。也就是说,热点 key 的目标不是"过期后再说",而是尽量避免它在高峰期突然失效。

3. Redis 高可用与分布式应用

267. Redis 主从复制、哨兵、集群分别解决什么问题?
  • 主从复制:解决数据复制和读扩展
  • Sentinel:Redis 官方明确说它为非集群 Redis 提供高可用,还负责监控、通知、自动故障转移和配置发现
  • Cluster:解决分片和横向扩展,同时兼顾一定程度的高可用

一句话:复制解决"副本",Sentinel 解决"故障切换",Cluster 解决"分片扩容 + 集群高可用"。

268. Redis Cluster 的分片机制是什么?

Redis Cluster 官方规范说明,整个集群有 16384 个 hash slots,每个主节点负责其中一部分槽位。key 经过哈希后映射到某个槽位,再由负责该槽位的节点处理。这个设计让扩容和缩容时可以通过迁移槽位来重分布数据。

269. 为什么 Redis Cluster 不适合很多跨槽复杂事务场景?

因为 Cluster 是按 hash slot 分片的,多个 key 如果不在同一槽位,就不在同一主节点上。这样一来,很多需要跨 key 原子操作、复杂事务、Lua 多 key 脚本的场景就会受限,客户端还得处理重定向和路由。Cluster 更擅长水平分片,不擅长大量跨槽强原子业务。

270. 分布式锁为什么常用 Redis 做?

因为 Redis 本身就是高性能内存系统,单 key 原子操作很快,天然支持过期时间,适合做轻量级分布式互斥。Redis 官方分布式锁文档也是基于这一点展开的:先从单实例锁开始,再讨论更安全的 Redlock 算法。换句话说,Redis 做分布式锁的优势在于快、简单、易部署

271. 使用 SET NX EX 实现分布式锁时要注意哪些问题?

要注意至少四点:

  1. 获取锁要原子,通常用 SET key value NX EX seconds
  2. value 要唯一,避免误删别人的锁
  3. 业务执行时间可能超过过期时间,导致锁提前失效
  4. 单实例锁在主从异步复制或故障切换下有风险

Redis 官方分布式锁文档专门说明了单实例加锁和更高可用场景下的差别,这些坑都绕不开。

272. 为什么释放锁要校验 value?

因为锁可能已经过期并被其他客户端重新拿到。如果你不校验 value,直接删 key,就有可能把别人的锁删掉。Redis 官方分布式锁文档明确给出了"先比较 value,再删除"的 Lua 脚本模式,本质就是防止误删非自己持有的锁。

273. Lua 脚本为什么能帮助保证原子性?

因为 Redis 会把 Lua 脚本当成一个整体执行,中间不会插入其他命令,所以像"比较 value 再删除锁"这种多步逻辑就可以在服务端一次完成,避免竞态条件。它不是说"所有业务都该写 Lua",而是说在需要把多步 Redis 操作变成一个原子单元时,Lua 很合适。Redis 官方分布式锁文档正是用 Lua 演示安全释放锁。

274. Redisson 做了哪些增强能力?

Redisson 官方文档里很典型的增强点包括:

  • 更丰富的分布式锁与同步器抽象
  • watchdog 自动续期,避免业务没跑完锁先过期
  • leaseTime 控制锁自动释放
  • MultiLock、读写锁、信号量等更高层封装

所以 Redisson 的价值不是"只是帮你发几条 Redis 命令",而是把很多分布式同步的工程细节封装起来了。

275. Redis 分布式锁为什么不能完全等价于数据库事务锁?

因为数据库事务锁和事务提交/回滚、隔离级别、日志恢复是一体化设计的,而 Redis 锁更多只是应用层分布式互斥手段。Redis 锁没有天然绑定数据库提交语义,也无法直接保证跨系统资源的一致提交。换句话说,Redis 锁能控并发,不等于能替代事务语义

276. Redis 可以如何实现延时队列、限流器、排行榜?
  • 延时队列:常用 ZSet,score 放执行时间,定时扫到期成员
  • 限流器:常用计数器、滑动窗口、令牌桶/漏桶近似实现
  • 排行榜:最典型就是 ZSet,支持按 score 排序、查名次、查区间

这些能力本质都来自 Redis 原生数据类型,尤其是 ZSet 和 String/Hash 的组合能力。

277. 在 Redis 内存淘汰策略里,allkeys-lru 和 volatile-lru 如何取舍?

Redis 官方 eviction 文档里这两类策略的核心区别是:

  • allkeys-lru:所有 key 都参与 LRU 淘汰
  • volatile-lru:只有设置了过期时间的 key 才参与淘汰

取舍上,纯缓存系统更常用 allkeys-lru;如果你实例里还有不能随便淘汰的持久性数据,就更倾向只让带 TTL 的缓存数据参与淘汰。

278. 为什么单实例同时做"持久化存储 + 热缓存"不是总是好主意?

因为 Redis 持久化、AOF 重写、RDB 快照、fork 等操作都会带来额外 CPU、内存和磁盘压力;如果同一个实例还承载核心热缓存流量,这些后台操作的抖动就可能直接传导到在线请求。Redis 官方持久化文档和延迟诊断文档都提到后台持久化与延迟的关联,所以把"强持久化数据"和"极致低延迟热缓存"混在一个实例里,往往不够稳。

279. Redis key eviction 指标升高时,通常说明什么问题?

通常说明实例已接近或超过内存上限,Redis 正在按配置策略驱逐 key 来回到内存限制以内。Redis 官方 eviction 文档明确说,客户端新增数据导致超过 maxmemory 时,Redis 会触发驱逐。工程上这往往意味着:缓存容量不够、数据集增长了、TTL 设计不合理,或者某段时间写入突增。

280. Redis fork 持久化时为什么可能影响延迟?

Redis 官方延迟诊断文档专门把 fork latency 作为一个重要来源来讨论:当 Redis 为 BGSAVE 或 AOF rewrite fork 子进程时,这个 fork 动作本身在主线程上执行,实例越大、内存页越多,这个过程越可能带来可见延迟。也就是说,哪怕真正的持久化工作在后台进程,fork 这一刻 也可能让在线请求抖一下。

281. 某个热点接口 RT 抖动,怀疑 Redis,你会怎么排查?

我会先区分是 Redis 本身慢,还是网络、客户端、系统资源导致的"看起来像 Redis 慢"。优先看 INFO、slowlog、latency monitor、redis-cli --latency、命令分布、是否存在 hotkeys / bigkeys,再结合 CPU、内存淘汰、fork 持久化、网络 RTT 一起看。Redis 官方文档明确建议用 slowlog、latency monitoring 和 --hotkeys/--bigkeys 这类工具来做定位。

282. Redis CPU 很高但 QPS 不高,通常可能是什么原因?

常见原因包括:单条命令很重,大 key 操作、复杂 Lua、频繁扫描类命令、持久化/重写带来的额外开销,或者实例正在做 fork、淘汰、复制等后台工作。也就是说,QPS 不高不代表 Redis 轻松,重命令和后台任务 一样能把 CPU 拉高。Redis 官方延迟诊断文档就明确把 slow commands、fork、系统资源等列为重要原因。

283. Redis 内存够但频繁淘汰 key,说明了什么?

这通常说明"物理机器看起来还有内存",但 Redis 已经触发了自己配置的 maxmemory 限制,所以开始按 eviction policy 驱逐 key。问题本质不是机器总内存,而是 Redis 可用内存上限、数据增长速度、TTL 策略和业务访问模式之间不匹配。

284. 秒杀系统中,你会把哪些校验放到 Redis,哪些放到数据库?

我会把高频、可快速失败、允许短暂最终一致 的校验放到 Redis,比如库存预扣、用户限流、重复请求拦截、活动状态校验;把最终真相和强约束放到数据库,比如最终扣库存落库、订单唯一性、支付状态、不可逆业务状态流转。因为 Redis 适合做前置削峰和快速判断,但不能替代数据库上的最终一致约束。Redis 分布式锁和缓存文档也都隐含了这个边界:它更适合快路径,不等价于事务数据库。

285. 如何设计一个高并发场景下的缓存预热与降级方案?

我的思路一般是:上线前预热核心热点数据;运行时对热点 key 做长 TTL + 主动刷新;在缓存异常时分级降级,比如本地缓存兜底、返回静态结果、限流、熔断、只保核心接口。重点不是"缓存挂了就查库",而是要把回源流量控制在数据库和下游能承受的范围内。Redis 文档对延迟、淘汰、热点键排查都说明:缓存层出问题时,必须把故障当成核心组件故障来治理。


五、消息队列专题

1. Kafka

286. Kafka 的 Broker、Topic、Partition、Replica、Consumer Group 分别是什么?
  • Broker:Kafka 集群中的服务节点
  • Topic:消息主题
  • Partition:主题下的分区,是并发和顺序的基本单位
  • Replica:分区副本,用来做高可用
  • Consumer Group:一组共同消费某个主题的消费者,组内一个分区同一时刻只会分配给一个消费者实例

Kafka 官方文档就是围绕这些基本概念来定义架构的。

287. Kafka 为什么快?

Kafka 快,常见原因是:顺序写磁盘、批量发送、批量拉取、页缓存、高效网络传输,以及把很多吞吐优化建立在日志追加模型上。Kafka 官方介绍和生态文档都长期强调它的核心是高吞吐分布式日志,不是传统逐条消息队列的处理方式。

288. 顺序写、页缓存、批量发送、零拷贝分别带来了什么收益?
  • 顺序写:减少随机 IO 成本
  • 页缓存:让读写尽量命中 OS cache
  • 批量发送/拉取:摊薄网络和协议开销
  • 零拷贝:减少用户态/内核态数据搬运成本

这些优化叠加起来,才能让 Kafka 在日志型场景下获得很高吞吐。

289. Partition 为什么既能提升吞吐,也会带来复杂性?

因为分区能把一个主题拆成多个并行处理单元,生产和消费都能横向扩展,所以吞吐能上去;但与此同时,顺序性、热点分区、分区键设计、扩容、重平衡、消费者并发都变复杂了。也就是说,分区是并发的来源,也是复杂度的来源

290. Consumer Group 的负载均衡原理是什么?

消费者启动后会找到 group coordinator,加入消费者组,然后组协调器触发 rebalance,把分区分配给组内成员。Confluent 官方文档明确说明:成员加入或变化时,会发生 rebalance,并生成新的 group generation。

291. 分区数应该如何规划?

分区数要同时看:目标吞吐、未来扩容、消费者并发上限、单分区热点风险、运维成本和顺序需求。分区太少扩展性不够,分区太多又会增加管理、文件句柄、重平衡和副本同步成本,所以通常要按"未来峰值吞吐 + 可接受复杂度"来预估,而不是拍脑袋。Kafka 官方文档强调分区和消费者并发、主题管理密切相关。

292. 消息有序消费如何实现?为什么"全局有序"代价很大?

Kafka 里常见做法是:同一业务 key 固定落到同一 partition,然后在该 partition 内顺序消费,这样能保证"局部有序"。全局有序意味着整个主题几乎只能退化成单分区或单串行处理,吞吐和扩展性会明显受限,所以代价很大。

293. ISR 是什么?ACK 机制如何影响可靠性和性能?

ISR 是与 leader 保持同步的副本集合。ACK 策略越严格,消息可靠性通常越高,但生产端等待时间也更长,吞吐和延迟可能受影响;反过来,ACK 放宽,性能更好,但丢消息风险更高。Kafka 官方文档把副本和可靠性保证放在核心设计里。

294. Kafka 如何实现高可用?

核心手段是:分区副本、leader/follower 复制、ISR、消费者组重平衡、以及 broker 故障后的分区领导者迁移。也就是说,Kafka 不是靠单机强,而是靠分区副本和集群协调来获得高可用。

295. 消息为什么会丢失?分别可能发生在哪些环节?

可能发生在三个阶段:

  1. 生产端:还没成功写入 broker 就失败
  2. Broker 端:副本不足、故障切换时未同步完成
  3. 消费端:消息处理了但 offset 提交时机不当,或者反过来 offset 提交了但业务没真正落地

所以"Kafka 不丢消息"从来不是默认送的,而是生产、存储、消费三端一起设计出来的。

296. 如何设计生产端消息不丢?

常见做法是:开启合适的 ACK 策略、设置重试、启用幂等生产者、对发送失败做重试和告警,并在关键链路上做好本地事务/消息表兜底。Kafka 官方文档明确把 exactly-once 和幂等处理列为能力之一,但工程上依然要把发送确认和失败重试设计好。

297. 如何设计消费端消息不丢?

核心原则是:先保证业务处理成功,再提交 offset,并且业务处理要具备幂等能力。否则你提前提交 offset,后面业务失败就会真丢;你晚提交 offset,又可能重复消费,所以必须用"幂等 + 正确提交时机"来兜住。

298. 什么情况下会重复消费?

最常见是:消息处理成功了,但 offset 还没来得及提交就发生故障;或者消费端重试、rebalance、网络抖动导致同一消息再次被分配和拉取。RabbitMQ 官方可靠性文档也强调了类似事实:网络故障和确认丢失会导致重复,因此消费端要幂等;Kafka 场景同理。

299. 如何做消费幂等?

常见做法有:业务唯一键去重、幂等表、状态机校验、数据库唯一约束、防重 token、基于消息 ID 的去重缓存。核心思想是:允许消息再来一次,但业务结果不能重复生效。这也是 MQ 系统里最常见、最实用的可靠性设计。

300. 至多一次、至少一次、精确一次分别是什么意思?
  • 至多一次:可能丢,但不重复
  • 至少一次:不轻易丢,但可能重复
  • 精确一次:理想上既不丢也不重,在定义好的边界内只处理一次

Kafka 官方文档明确把 exactly-once 作为能力之一,但要注意它有边界,不是对所有外部系统天然成立。

301. Kafka 的 Exactly Once 适合哪些边界内的问题?哪些问题它解决不了?

它更适合 Kafka 自身生态边界内,比如生产、写入 Kafka、消费并写回 Kafka 这类链路;一旦你把结果写进外部数据库、调用外部 HTTP 服务、发短信邮件,就必须自己补幂等和事务边界。也就是说,Kafka 的 EOS 解决的是受控链路内的处理一致性,不是替你包办整个业务世界。

302. offset 提交时机为什么很关键?

因为 offset 代表"我已经处理到哪了"。你提交太早,业务没真落地就可能丢消息;你提交太晚,故障恢复后又会重复消费。所以 offset 的正确时机本质上是"消费语义"的核心开关。Confluent 文档对 consumer group 和 offset 管理就是围绕这个问题展开的。

303. 为什么会出现消息积压?

本质上就是生产速度持续大于消费速度。原因可能是消费者数量不够、单条处理太慢、下游数据库或 RPC 变慢、某个分区热点、rebalance 抖动、错误重试过多。积压不是 Kafka 独有问题,而是所有队列系统都会出现的供需失衡。

304. 如果一个消费者组明显落后,你怎么排查?

我会先看 lag 分布是全局都高还是少数分区高,再看消费者实例是否存活、rebalance 是否频繁、单条处理耗时、下游依赖是否慢、是否存在热点分区。也就是说,先回答"是组整体慢,还是局部失衡",再决定是扩实例、调分区,还是优化处理逻辑。

305. 增加消费者实例为什么有时并不能提升消费能力?

因为同一消费者组内,一个 partition 同时只能被一个消费者实例消费。如果分区数不够,实例再多也会有空转;另外,如果瓶颈在下游数据库、网络或单条业务处理,单纯加实例也没用。换句话说,消费者并发上限先受分区数限制,再受业务瓶颈限制

306. Rebalance 为什么会影响稳定性?

因为 rebalance 期间,分区分配要重新协商,消费者可能暂停消费、撤销分区、重建状态,短时间内容易带来吞吐下降和延迟抖动。Confluent 文档明确说明了:成员加入或变化会触发 rebalance,这本身就是组级协调事件。

307. 如何减少 Rebalance 带来的抖动?

常见办法有:减少消费者频繁重启、避免心跳超时、合理设置 poll/processing 时间、做静态成员或更平滑的分配策略、控制部署批次。目标不是完全消灭 rebalance,而是让它不要过于频繁,也不要在大流量时把整个消费组抖散。

308. 批量消费和单条消费如何权衡?

批量消费吞吐更高、网络和提交开销更低,但单条失败时处理更复杂,错误定位和重试粒度更粗;单条消费实现简单、失败隔离更清晰,但吞吐通常更低。工程里通常看业务幂等性、失败处理能力和延迟目标来决定。Kafka 的高吞吐模型天然鼓励批量,但不是所有业务都适合把批做很大。

309. Kafka 新版本中的 KRaft 模式解决了什么问题?

KRaft 的核心是让 Kafka 摆脱对 ZooKeeper 的依赖,把元数据管理和控制平面内建到 Kafka 自己的 quorum 机制里。Kafka 官方文档现在已经把 KRaft 和相关协议作为核心组成部分来介绍。

310. KRaft 和 ZooKeeper 时代相比,架构理解上要注意什么变化?

最大的变化就是:以前你要把"Kafka broker + ZooKeeper 协调"分开理解;现在要把 Kafka 本身的控制器 quorum 也视为核心架构组成部分。也就是说,元数据治理不再依赖外部 ZooKeeper,而是 Kafka 自己的一部分

311. 如果你来设计 Kafka Topic,你会如何规划保留时间、分区、副本和压缩策略?

我会按业务目标来定:

  • 保留时间 看回溯需求、补偿需求、存储成本
  • 分区数 看峰值吞吐、并发消费和未来扩容
  • 副本数 看高可用要求
  • 压缩策略 看消息大小、网络带宽和 CPU 成本

Topic 设计不是只看"能不能跑",而是要把吞吐、可靠性、回溯能力和成本一起权衡。

312. 一个订单系统要求"高吞吐 + 不丢消息 + 尽量有序",你会怎么设计 Kafka 方案?

我会按订单号或用户号做分区键,保证同一业务实体局部有序;生产端开启可靠 ACK、重试和幂等;消费端做幂等和正确的 offset 提交;关键事件落消息表或事务消息兜底。这样能做到局部有序 + 高吞吐 + 尽量不丢,而不会为了追求全局有序把系统吞吐打没。

313. 消费者处理很慢,但 broker 很空闲,你会怀疑什么?

我会优先怀疑消费端:下游数据库慢、外部 RPC 慢、业务逻辑重、线程池排队、批量过小、提交策略不合理、单分区热点。broker 很空闲说明问题多半不在 Kafka 存储层,而在消费者自身或消费者下游

314. 某个分区堆积非常严重,其他分区正常,说明了什么?

这通常说明出现了热点分区:可能分区键设计不均、某个 key 特别热、该分区对应的消息处理更慢,或者该分区消费者实例异常。Kafka 的吞吐是分区并行换来的,所以只要分区不均,局部就会先堵。

315. 如何处理"消息处理成功了,但 offset 没提交"这种问题?

本质上要接受"可能重复消费",然后通过幂等设计把重复影响吃掉。因为一旦处理成功但提交失败,恢复后这条消息大概率还会再来一次;这正是至少一次语义的典型表现。正确解法不是幻想永不重复,而是让重复也安全

316. 如何设计 Kafka 消费失败后的重试和死信机制?

常见做法是:区分可重试和不可重试错误;可重试错误进入延迟重试 topic 或定时重试链路;超过阈值后进入死信 topic;所有重试都要配幂等和告警。Kafka 本身不像 RabbitMQ 那样自带经典 DLX 语义,所以通常靠 重试 topic + DLQ topic 在业务层实现。


2. RabbitMQ

317. RabbitMQ 的 Exchange、Queue、Binding、RoutingKey 分别是什么?
  • Exchange:接收生产者消息并决定路由
  • Queue:真正存放待消费消息
  • Binding:把 exchange 和 queue 绑定起来
  • RoutingKey:路由时参与匹配的键

RabbitMQ 的路由模型就是围绕这四个概念构建的。

318. direct、topic、fanout、headers 四种 Exchange 有什么区别?
  • direct:按精确 routing key 匹配
  • topic:按通配模式匹配
  • fanout:广播到所有绑定队列
  • headers:按消息头匹配

选型本质就是在"精确路由、模式路由、广播、头匹配"之间取舍。RabbitMQ 文档长期围绕这些交换机类型来定义路由行为。

319. RabbitMQ 为什么更适合一些低吞吐、高灵活路由场景?

因为 RabbitMQ 在交换机模型、路由规则、ACK、重试、死信、消费控制等方面非常灵活,特别适合业务路由复杂、需要精细投递控制的场景。相对地,Kafka 更强调日志流和高吞吐分区消费。也就是说,RabbitMQ 更像灵活消息路由器,Kafka 更像高吞吐日志平台

320. 死信队列和延迟队列分别适合什么场景?
  • 死信队列:处理被拒绝、过期、超长、不可正常消费的消息
  • 延迟队列:处理"过一段时间再投递"的需求,比如订单超时取消、重试退避

RabbitMQ 官方 DLX 文档明确列出了消息 dead-letter 的几种触发条件,而延迟通常会借助 TTL + DLX,或插件能力来实现。

321. RabbitMQ 的消息确认机制是什么?

RabbitMQ 官方把它分成两块:消费者确认(consumer acknowledgements)生产者确认(publisher confirms)。前者解决"消费者是否成功处理",后者解决"broker 是否已经接收并负责此消息"。这两块一起决定可靠性。

322. 如何保证消息可靠投递?

常见做法是:生产端开启 publisher confirms、必要时消息持久化、队列持久化、交换机和绑定关系正确配置,并对生产端未确认消息做重试和告警。RabbitMQ 官方可靠性文档明确指出:生产者在连接或通道故障恢复后,要重发未收到确认的消息。

323. RabbitMQ 如何处理消费失败重试?

常见做法是:手动 ACK 模式下,失败时选择 nack/reject,再决定是否 requeue;如果要控制次数和退避,通常结合重试队列、TTL、DLX 来实现。RabbitMQ 官方 acknowledges 和 DLX 文档都清楚说明了这些机制。

324. 为什么 RabbitMQ 也需要幂等设计?

因为网络故障、确认丢失、重试和消费者重连,都可能导致消息重复投递。RabbitMQ 官方可靠性文档明确说过:生产恢复时可能重发未确认消息,因此消费者要做去重或幂等处理。

325. RabbitMQ 和 Kafka 的核心差异是什么?如何选型?

RabbitMQ 更强在灵活路由、精细 ACK、死信/重试机制和传统消息队列语义;Kafka 更强在高吞吐、分区并行、日志保留和流式处理。选型时如果更看重路由灵活、消费控制、业务队列语义 ,偏 RabbitMQ;如果更看重吞吐、可回放、流式事件平台,偏 Kafka。

326. 如果你要做订单超时取消,RabbitMQ 延迟队列和 Redis ZSet 你怎么选?

如果业务已经大量依赖 RabbitMQ,且希望和现有消息链路、死信重试统一,我会优先 RabbitMQ;如果需要更灵活的按时间轮询、批量扫描、轻量级延时任务,也可以用 Redis ZSet。关键不在谁"绝对更好",而在你是否已经有可靠的消费、失败补偿、监控体系。RabbitMQ 的 TTL/DLX 机制和 Redis 的 ZSet 都适合做延时触发,但治理方式不一样。

327. 如果消费者总是处理很慢,RabbitMQ 应该从哪些维度优化?

我会看:消费者并发数、prefetch、ACK 时机、单条业务耗时、下游依赖、批处理能力、是否出现大量重试/死信、是否有单队列热点。RabbitMQ 消费慢很多时候不是 broker 问题,而是消费者端处理能力或下游瓶颈问题。RabbitMQ 官方 consumers 文档就把确认模式和流控看作关键因素。

328. RabbitMQ 出现大量未确认消息时,你怎么分析?

先看消费者是不是用了手动 ACK 且处理慢,再看消费者是否卡在下游、是否忘记 ACK、是否 prefetch 太大、是否存在长时间阻塞。大量 unacked 通常意味着消息已经投给消费者,但迟迟没被确认,不一定是 broker 存不下了,而是消费链路推进不动


3. 消息队列通用问题

329. 为什么系统要引入 MQ?

核心原因通常是三类:解耦、异步化、削峰填谷。也就是把同步强耦合调用变成异步事件,把瞬时高峰变成可平滑处理的队列,把上下游节奏解耦开。Kafka 和 RabbitMQ 虽然风格不同,但都在解决这几类问题。

330. 异步削峰填谷的本质是什么?

本质是把"请求到达速率"和"下游处理速率"解耦:高峰时先把任务接住排队,按下游可承受的节奏慢慢处理。这样做的收益是保护核心资源,代价是引入排队等待和最终一致性问题。

331. MQ 如何帮助系统解耦?它又会引入哪些新复杂度?

它能让上游只负责发布事件,不必同步依赖所有下游;下游也能独立扩缩容、独立失败恢复。但它会引入消息可靠性、重复消费、顺序性、积压、回溯、监控、补偿、幂等等一整套新复杂度。也就是说,MQ 不是消灭复杂度,而是重新分配复杂度

332. 如何基于 MQ 做最终一致性?

常见套路是:本地事务先落业务数据,再可靠地产生消息;下游消费消息并做幂等处理,失败则重试或补偿;所有关键步骤要可追踪、可告警。这样就把"跨系统同步成功"改成"异步最终收敛到一致状态"。

333. 为什么消费幂等是 MQ 系统的常备能力?

因为无论 Kafka 还是 RabbitMQ,都可能在故障恢复、确认丢失、重试、重平衡时带来重复投递。官方可靠性文档明确提到:确认在网络故障中可能丢失,因此消费者必须能去重或幂等处理。

334. 什么场景不适合引入 MQ?

如果业务链路很短、同步强一致要求极高、延迟容忍度极低、上下游本来就很简单,或者团队还没有足够的 MQ 运维和治理能力,这时引入 MQ 可能得不偿失。因为你得到了解耦和削峰,也同时引入了消息可靠性和补偿复杂度。

335. 如果链路中已经有 Redis、数据库、MQ,多副本写入一致性该怎么设计?

一般要先明确谁是真实来源。常见做法是以数据库为准,Redis 作为缓存副本,MQ 作为变更传播通道;写入时先保证数据库事务正确,再通过消息或 binlog 驱动缓存失效/更新,下游消费侧全部幂等。不要试图让三者天然同步提交,而是要设计成"一个真相源 + 多个派生副本"的一致性模型。


六、搜索引擎专题

Elasticsearch

336. 倒排索引的核心思想是什么?

倒排索引的核心思想是:不是"从文档找词",而是"从词找到包含它的文档列表"。Elastic 官方对 text 字段和分词搜索的设计本质就建立在这种 inverted index 上,这也是全文检索能高效做关键词匹配的基础。

337. ES 的全文检索流程大致是怎样的?

大致可以理解为:文档写入时先做分析和索引,查询时把用户输入按相同或兼容的分析器处理,再去倒排索引里找匹配文档,最后做相关性评分、排序和返回。也就是说,ES 的快不是来自扫描全文,而是来自写入时提前建立好了可检索结构

338. 分词器为什么会直接影响召回效果?

因为分词器决定了文本会被切成哪些 token,而检索本质上就是在 token 层做匹配。分词切得不合理,写入和查询阶段的 token 对不上,召回自然就差。Elastic 对 text 字段和 analyzer 的设计,本质就是在强调"先分析,再检索"。

339. textkeyword 的区别是什么?

Elastic 官方文档说得很明确:

  • text 适合全文检索,会被分析
  • keyword 适合结构化内容、精确匹配、排序和聚合,不适合全文搜索

所以这两个字段类型不是随便二选一,而是分别服务于"全文语义匹配"和"精确值匹配/聚合排序"。

340. queryfilter 的区别是什么?

在 ES 里,query 更偏"是否匹配 + 如何评分",filter 更偏"是否满足条件,不参与相关性评分"。所以像状态、时间范围、租户 ID 这类纯过滤条件,通常更适合放 filter;而关键词相关性匹配才更适合放 query。这样做既语义更清晰,也更利于性能优化。

341. 聚合查询常见有哪些性能风险?

聚合查询的主要风险通常在于:聚合范围过大、字段基数过高、深层嵌套聚合过多,以及把不该参与聚合的字段拿去做聚合,导致内存和 CPU 成本明显上升。工程里最常见的坑不是"ES 不会聚合",而是把分析型查询直接当 OLAP 用,最后把在线检索集群拖慢。Elastic 官方文档也一直强调查询、过滤和聚合是不同用途的能力,设计时要明确边界。

342. 深分页为什么是经典问题?

因为传统的 from + size 分页在页码很深时,ES 仍然需要跳过前面大量结果,这会让内存和排序开销不断放大,所以深分页性能会越来越差。Elastic 官方推荐在更深的分页场景里优先考虑 search_after,而不是一直堆高 from

343. from + size 为什么不适合深分页?

核心原因是它需要先找到前面的很多结果,再丢掉,只返回后面一小段,因此越往后页,浪费越大。这个模式在结果集很大时会带来明显的排序和内存压力,所以适合浅分页,不适合无限往后翻。Elastic 生态里的官方和社区资料都把它视为深分页的典型反模式。

344. search_after 适合什么场景?

search_after 适合"按稳定排序不断向后翻页"的场景,特别是结果集很大、需要深分页但不要求随意跳页时。它本质更像"基于上一页最后一个排序值继续往后找",而不是传统 offset 分页。Elastic 官方材料和相关说明都把它作为深分页的主流方案。

345. PIT(Point in Time)解决了什么问题?

PIT 的核心作用是给分页过程提供一个相对稳定的视图,避免你在连续翻页时,因为索引数据发生变化而导致结果重复、遗漏或顺序漂移。它常和 search_after 搭配使用,尤其适合深分页场景。换句话说,search_after 解决"怎么往后翻",PIT 解决"翻页期间视图是否稳定"。

346. refresh 机制是什么?为什么写入后不一定马上能查到?

因为 ES 的写入不是每来一条就立刻对搜索可见,而是要等 refresh 之后,新写入的段才会被搜索看到。所以它天然更偏 near real-time search,而不是"写完马上强可见"的数据库语义。工程里这也是很多人第一次用 ES 时容易踩的点:写成功不等于立刻可搜到

347. 分片和副本分别解决什么问题?

分片主要解决的是数据规模和并行处理能力,让一个索引能拆开存、拆开查;副本主要解决高可用和读扩展能力,让主分片故障时还能切换,也能分担读请求。简单说:分片偏扩展,副本偏高可用和读能力。这是 ES 集群设计里的两个基础维度。

348. 为什么分片数不是越多越好?

因为分片本身也有元数据、调度、文件句柄、段管理和查询协调成本。分片过多会让集群更碎,查询广播、合并和运维复杂度都会上升。也就是说,分片不是"越细越先进",而是要和数据量、节点规模、查询模式一起权衡。

349. mapping 设计为什么很关键?

因为 mapping 决定了字段会被怎样索引、是否参与全文检索、是否适合聚合/排序、是否会占用额外存储和内存。一个字段如果类型定义错了,可能不是"查不准"这么简单,还会直接导致索引膨胀、聚合异常慢或排序失效。Elastic 官方对 textkeyword 的区分,本质就是在强调 mapping 设计的关键性。

350. 写入性能和查询性能往往为什么需要权衡?

因为为了查得快,你通常会希望分词更充分、字段更多地可索引、可聚合、可排序;但这些都会增加写入时的分析、倒排构建、存储和 refresh 成本。反过来,为了写得快,你可能会减少索引字段、减少副本、降低 refresh 频率,但查询体验又会受影响。ES 的本质就是一个"写入构建索引、查询消费索引"的系统,所以二者天然存在权衡。

351. 什么场景下你会建议业务不要滥用 ES?

如果业务其实只需要强事务、精确更新、复杂关系约束、低延迟单行读写,而不是真正的全文检索和复杂检索分析,我会建议优先用数据库而不是 ES。因为 ES 擅长搜索,不擅长替代 OLTP 数据库;滥用它常见后果就是一致性治理复杂、写入链路变重、结果还不一定比数据库更稳。Elastic 官方文档对 queryfiltertextkeyword 的定位,本质上也说明了它是围绕检索而设计的。

352. 一个检索系统 QPS 很高,你会如何做 ES 层优化?

我会先从 mapping、查询结构、filter 命中、分片设计、热点索引拆分、缓存命中和深分页治理入手。典型原则是:能 filter 就不要无谓算分,能避免深分页就不要 from + size 硬翻,能做精确字段就不要一律 text,热点查询要避免拖垮所有分片。也就是说,ES 优化不是只盯节点配置,而是要从索引设计 + 查询模式 + 热点治理一起做。

353. 搜索慢是分词问题、聚合问题、深分页问题还是集群问题,你怎么判断?

我会先看慢查询特征:如果召回异常差或关键词表现反常,优先看 analyzer 和字段类型;如果是某些聚合接口慢,先看高基数聚合和嵌套聚合;如果是翻到后面页特别慢,优先怀疑深分页;如果所有请求都慢,再看分片、节点负载和集群整体状态。判断关键是先把"慢"拆成匹配问题、查询结构问题、分页问题、集群资源问题

354. 为什么一个小字段映射错误,也可能导致整体存储和检索成本明显上升?

因为 mapping 是全量数据都会反复经过的规则。一个看似不起眼的字段,如果被错误地做了全文分析、排序、聚合或存了不必要的索引结构,随着文档量增长,它的成本会被成倍放大。所以 mapping 错误不是"某个字段查得不顺手",而是可能变成全局资源浪费。

355. 如何设计"数据库 + ES"双写的一致性方案?

通常我会把数据库作为真相源,ES 作为检索副本,写链路优先保证数据库提交成功,再通过 binlog、消息队列或异步同步任务把变更推到 ES。这样设计的关键不是追求绝对同步提交,而是接受 ES 是最终一致的检索副本,再通过补偿、重试、对账和回放能力把一致性收敛回来。


七、微服务与架构能力

1. 微服务基础

356. 单体、分层、SOA、微服务分别是什么?核心差异在哪里?

可以粗略理解成:

  • 单体:一个部署单元承载大部分业务
  • 分层:在单体内部按表现层、业务层、数据层等做清晰组织
  • SOA:更强调服务化复用和企业级服务集成
  • 微服务:强调小而自治、独立部署、围绕业务能力拆分

核心差异不在"是不是分模块",而在部署边界、团队自治、服务粒度和治理复杂度。Spring Boot 和现代微服务生态的官方文档更多默认你面对的是服务拆分和可观测治理,而不是传统大单体部署。

357. 服务拆分的常见原则有哪些?

我通常会按业务边界、数据边界、团队边界和变更边界来拆。一个好的拆分应该让服务内部高内聚、服务之间低耦合,并尽量让核心数据和核心职责落在同一边界里。也就是说,拆分不是为了"拆而拆",而是为了让独立演进、独立部署、独立治理更自然。

358. 为什么服务拆分过细会带来问题?

因为服务过细之后,调用链会变长,网络开销、故障传播、分布式事务、配置治理、监控排障和团队协作成本都会急剧上升。看上去模块更"小更优雅",但整体系统可能更脆弱。工程里很多问题不是"不够微",而是"微过头了"。

359. 服务注册与发现解决了什么问题?

它解决的是:服务实例会动态增减、地址会变化,但调用方不该手写固定地址。注册中心负责维护"服务名 -> 实例列表"的映射,调用方通过服务发现拿到可用实例,再做负载均衡和故障剔除。微服务一多,没有服务发现,地址管理会非常混乱。

360. 配置中心的核心价值是什么?

配置中心的核心价值是把配置从服务实例里抽出来集中管理,支持动态下发、环境区分、权限控制和统一审计。这样做的重点不是"把 yml 放到别处",而是让配置变更不再和重新发版完全绑定,从而提升运维和治理效率。

361. API 网关为什么几乎是微服务标配?

因为外部流量不应该直接撞到一堆内部服务。网关作为统一入口,可以集中做路由、认证、限流、熔断、日志、灰度和链路追踪入口,既保护内部服务,也让外部接入更稳定。微服务越多,统一入口的价值越高。

362. HTTP 调用和 RPC 调用各自适合什么场景?

HTTP 更适合开放接口、跨语言、跨系统边界和更标准化的接入;RPC 更适合内部服务调用,尤其是你更在意接口约束、序列化效率、治理能力和调用体验时。不是谁一定替代谁,而是对外偏 HTTP,对内常偏 RPC

363. 为什么内部服务很多时候更偏向 RPC,而外部开放更偏向 HTTP?

因为内部服务通常由统一团队控制,能统一协议和治理栈,所以更容易吃到 RPC 在性能和开发体验上的红利;而外部开放面对的是更复杂的客户端环境,HTTP 的通用性、生态和标准化优势更明显。换句话说,内部更追求效率与治理 ,外部更追求兼容与标准

2. 服务治理

364. 负载均衡有哪些常见策略?

常见策略包括:轮询、随机、加权轮询、最少连接、一致性哈希等。选型时不是只问"平均不平均",而是要看实例能力差异、请求是否有粘性需求、是否要按 key 打散热点。不同策略解决的是不同流量分配问题。

365. 限流、熔断、降级、隔离、超时、重试分别解决什么问题?
  • 限流:控制进入系统的流量上限
  • 熔断:下游异常时快速失败,避免雪崩
  • 降级:在资源紧张时保核心功能、舍弃次要能力
  • 隔离:避免某类资源耗尽拖死全局
  • 超时:避免无止境等待
  • 重试:对短暂失败做恢复尝试

这些能力合在一起,目标其实只有一个:让故障局部化,不要放大成全链路事故

366. 为什么重试有时会把系统压垮?

因为重试本质是在失败时再加请求。如果下游已经在边缘状态,盲目重试只会把本来还能缓慢处理的系统直接打穿,形成重试风暴。所以重试必须和超时、熔断、幂等、退避策略一起设计,不能把它当"可靠性万金油"。

367. 服务降级应该优先降什么、不降什么?

通常优先降的是非核心、可延迟、可补偿、可兜底的能力,比如推荐、统计、非关键通知、复杂报表;不轻易降的是核心交易、支付、库存、认证这类主链路。降级的关键不是"功能少一点",而是把资源优先留给最关键的业务目标

368. 舱壁隔离思想在微服务里怎么落地?

最常见的落地方式就是:线程池隔离、连接池隔离、队列隔离、实例隔离,必要时还做租户隔离和优先级隔离。它的核心思想是"一个仓进水,不要整艘船都沉",也就是让某类请求或某个下游出问题时,影响范围被控制在局部。

369. 幂等为什么是重试机制的前提之一?

因为只要发生重试,就等于业务有机会被执行多次。如果业务本身不是幂等的,重试就可能把"临时失败"变成"重复下单、重复扣款"这类更严重事故。所以重试不是先写了再想幂等,而是幂等设计在前,重试策略在后

370. 灰度发布如何设计才能更安全?

核心思路是:按流量、用户、地域、租户或实例逐步放量,配合监控、告警、回滚和对比验证,让风险在小范围暴露,而不是一次性放到全量。灰度真正重要的不是"分批上线"这四个字,而是能否快速发现差异、快速切回

3. 网关能力

371. 网关为什么不仅仅是转发?

因为在微服务体系里,网关除了路由,还承担着统一认证、限流、熔断、日志、追踪、灰度、黑白名单等治理职责。它是流量治理层,不是单纯的四层转发器。系统越复杂,网关越像"策略集中执行点"。

372. 网关做认证鉴权时,和业务服务该如何分工?

通常网关负责"你是谁、你带没带合法凭证、基础权限是否通过",业务服务负责"你在这个具体业务动作里有没有权力"。也就是说,网关更适合做通用认证和粗粒度授权,业务服务仍然要保留细粒度业务权限判断。

373. 黑白名单、限流熔断、日志记录、链路追踪通常为什么放在网关更合适?

因为这些能力高度通用、和具体业务逻辑弱相关,而且放在统一入口更容易做到策略一致、日志统一和链路统一。尤其链路追踪入口,最自然的地方就是最前面的网关,因为 trace 往往从这里开始生成和传播。Spring Boot 的 observability 文档也强调了 tracing 和 metrics 的统一接入价值。

374. 网关做缓存要注意哪些边界?

网关缓存更适合短期、只读、公共、可失效的响应,比如配置、字典、匿名接口静态结果;不适合承载复杂用户态、强一致交易结果或高风险权限数据。原因是网关离用户很近,缓存收益大,但一旦缓存脏了,影响面也大。

375. 网关 RT 很高时,你如何判断是规则问题、下游问题还是线程模型问题?

我会先拆链路:看网关本地处理耗时、下游调用耗时、线程池/事件循环状态、限流熔断规则命中、序列化和日志开销。如果本地规则耗时高,说明策略链可能有问题;如果下游耗时主导,就是依赖慢;如果事件循环或线程池卡住,就是线程模型或阻塞问题。判断关键是把"总 RT"拆成入口处理、治理逻辑、下游调用三段。

4. 认证鉴权

376. Session、Token、JWT 的适用边界分别是什么?

Session 更适合传统服务端会话管理,服务端强控制、可随时失效;Token 更适合前后端分离和多端接入,强调客户端携带凭证;JWT 则是自包含 Token,适合跨服务传播声明,但主动失效和权限实时变更治理更复杂。OAUTH 和 OIDC 官方资料本质上都建立在 token 驱动的授权/身份体系上,但不意味着所有登录态都必须用 JWT。

377. OAuth2.0 核心角色和流程是什么?

RFC 6749 明确定义了 OAuth 2.0 的核心角色:resource owner、client、authorization server、resource server。它的本质是:客户端先去授权服务器拿到访问令牌,再拿令牌访问资源服务器。也就是说,OAuth 2.0 解决的是授权委托,不是先天就等于"身份认证"。

378. OIDC 和 OAuth2.0 的关系是什么?

OpenID Connect Core 官方定义得非常直接:OIDC 是建立在 OAuth 2.0 之上的一个 identity layer。换句话说,OAuth 2.0 主要解决授权,OIDC 在它之上补上了身份认证能力,让客户端能够验证最终用户是谁,并获取基础身份信息。

379. CAS 和 OAuth2.0 / OIDC 的差异是什么?

CAS 的核心定位是企业级单点登录和身份提供方,Apereo 官方首页就把它定义为 SSO solution and identity provider。相比之下,OAuth 2.0 更偏授权框架,OIDC 是其上的身份层。简化理解就是:CAS 更像传统企业 SSO 体系,OAuth/OIDC 更像现代互联网和开放平台常用的授权/认证协议体系

380. 单点登录的核心原理是什么?

核心原理是:用户在统一身份系统里完成一次认证后,多个业务系统通过共享会话、票据或统一令牌机制识别这次登录,从而不需要重复输入密码。Apereo CAS 的官方介绍就明确把它定位为 single sign-on 方案。也就是说,SSO 的关键不是"跳转一下页面",而是多个应用共享同一套身份认证结果

381. RBAC 权限模型如何设计?

NIST 对 RBAC 的定义非常经典:用户、角色、权限之间是多对多关系,权限不直接大规模发给用户,而是先赋给角色,再把角色赋给用户。工程上通常会再叠加角色层级、资源维度和数据权限维度,但基础骨架仍然是用户---角色---权限三层关系。

382. 接口级权限控制常见做法有哪些?

常见做法包括:基于角色的接口访问控制、基于资源和动作的细粒度权限、网关统一鉴权 + 服务内业务校验、按租户/组织/数据范围做二次过滤。RBAC 很适合做第一层粗粒度控制,但真正到接口和数据层,往往还要叠加更细的资源判断。

383. 为什么"登录认证"和"权限授权"不能混为一谈?

因为认证回答的是"你是谁",授权回答的是"你能做什么"。OIDC 官方定义强调身份验证,OAuth 2.0 强调授权委托,这本身就说明二者不是一回事。工程里把它们混在一起,最常见的问题就是:系统只验证了 token 有效,却没有真正校验资源访问权限。

5. 分布式系统设计

384. CAP 和 BASE 在实际系统设计里如何权衡?

CAP 要你面对网络分区时承认取舍存在:不可能同时把一致性和可用性都做满;BASE 则是工程上更务实的路线,允许基本可用、软状态和最终一致。真正落地时,不是先问"我信哪个理论",而是先问"这条业务在分区时更不能接受什么"。

385. 分布式锁一定可靠吗?它的工程边界在哪里?

不一定。Redis 官方关于分布式锁的文档本身就专门讨论了单实例锁和更高可用场景的边界,说明锁的安全性和部署模型密切相关。工程上分布式锁更适合做互斥控制,不适合被误当成"事务一致性总开关";一旦涉及多资源提交、幂等和补偿,光有锁远远不够。

386. 分布式事务为什么很难?

因为它要跨多个服务、多个存储、多个失败点协调一致,而网络超时、节点故障、重复执行、部分成功这些问题都会让"要么全成要么全败"变得非常昂贵。OAuth/OIDC、微服务可观测性这些现代体系之所以这么强调边界和自治,也恰恰因为跨边界强一致是昂贵的。工程里很多系统最后都会退回到最终一致和补偿。

387. 强一致、最终一致、弱一致分别适合什么场景?

强一致更适合支付、账户余额、库存扣减这类核心约束;最终一致适合通知、积分、搜索索引、统计等可延迟收敛场景;弱一致更适合缓存、推荐、会话副本这类允许短时不一致但追求高可用的场景。关键不是背定义,而是按业务损失来选一致性级别

388. 幂等设计有哪些常见做法?

常见做法有:唯一业务号、幂等表、状态机校验、数据库唯一约束、防重 token、消息 ID 去重、请求去重缓存。它们的共同点都是让"相同请求重复来一次"不再导致结果再次生效。幂等在分布式系统里几乎是重试和最终一致的基础设施。

389. 去重表、状态机、唯一索引、防重 token 各自适合什么场景?
  • 去重表:适合消费幂等和异步消息处理
  • 状态机:适合业务动作有明确前后状态关系的场景
  • 唯一索引:适合数据库能直接表达唯一业务约束时
  • 防重 token:适合用户侧短时间重复提交控制

也就是说,它们不是互斥关系,而是分别从存储约束、业务状态、请求入口三个层面解决重复问题。

390. 高可用设计通常从哪些层面展开?

通常会从实例冗余、故障切换、限流熔断、隔离、重试退避、数据副本、监控告警、发布回滚这些层面一起展开。高可用不是单点技术,而是一整套"故障会发生,所以我要让它不扩散、能恢复"的系统设计。Spring Boot observability 文档之所以强调 metrics 和 tracing,也是因为没有可观测性,就谈不上高可用治理。

391. 高并发设计通常从哪些层面展开?

典型会从:无状态扩容、缓存、异步化、限流、连接池/线程池治理、数据库读写分离、热点治理、批处理、分片拆分这些层面展开。高并发本质上不是某一个组件扛得住,而是整条链路没有明显短板

392. 高吞吐设计和低延迟设计为什么经常互相冲突?

因为高吞吐通常鼓励批量、异步、排队、缓冲、顺序写,这些手段会摊薄开销;而低延迟更强调即时处理、少等待、少排队。两者不是绝对对立,但经常不是同一个最优点,所以系统设计必须先明确 KPI 是吞吐优先还是时延优先。

6. 可观测性

393. 可观测性的三大支柱是什么?

最常见的三大支柱是:metrics、logs、traces。Spring Boot 的 observability 文档明确把 metrics 和 traces 作为官方支持的一部分,而业界普遍也把日志视为第三根支柱。三者结合起来,才能同时回答"系统整体怎样、单次请求怎样、具体出错上下文怎样"。

394. 日志、指标、链路追踪分别擅长回答什么问题?
  • 指标:适合看整体健康度、趋势、异常波动
  • 日志:适合看细节上下文、错误原因、业务语义
  • 链路追踪:适合看一次请求穿过多个服务时到底慢在哪、错在哪

所以不是三选一,而是各自回答不同层级的问题。

395. TraceId 和 SpanId 分别是什么?

TraceId 用来标识同一条分布式请求链路,SpanId 用来标识链路中的一个具体片段或操作。也就是说,TraceId 把整条链串起来,SpanId 把链上的每个节点区分开。Spring Boot 的 observability 支持就是围绕这类 tracing 语义来接入 OpenTelemetry 等体系的。

396. 为什么日志里只打 message 远远不够?

因为没有结构化字段,你很难按服务、实例、租户、用户、请求 ID、错误码、耗时去检索和聚合日志。真正能支撑排障和治理的日志,必须带有足够上下文,而不是只输出一句自然语言 message。否则日志只是"留痕",不是"可分析资产"。

397. Spring Boot / Micrometer / Tracing 常见接法你会怎么设计?

Spring Boot 官方 observability 文档已经明确把 OpenTelemetry、Micrometer 和 OTLP exporter 作为官方支持路径。实际设计上,我会让应用统一通过 Micrometer/Observation API 打点和追踪,再把 metrics、traces 发到统一后端系统,这样可以减少厂商锁定,并让日志、指标、追踪的上下文更一致。

398. 告警体系为什么不能只靠 CPU 和内存阈值?

因为很多严重问题根本不会先表现为 CPU/内存爆满,比如线程池排队、连接池耗尽、错误率上升、下游超时、Kafka lag、Redis 热 key、尾延迟暴涨。好的告警体系应该围绕业务指标 + 资源指标 + 中间件指标 + 延迟/错误率一起设计,而不是只盯机器健康。

399. 一次完整的线上问题排查,你如何利用日志、指标、trace 联动定位?

常见顺序是:先看指标发现异常时间窗和异常维度,再用 trace 定位慢在哪一跳、错在哪一段,最后用日志查具体参数、错误堆栈和业务上下文。三者联动的关键不是"都收集了",而是它们之间有统一的请求关联标识,比如 TraceId。Spring Boot observability 的设计目标,本质上就是让这些观测信号更容易串起来。

400. 为什么没有统一链路追踪的系统,排障效率会明显下降?

因为微服务一多,一次请求会穿过网关、服务、缓存、数据库、MQ 等多个组件。没有统一链路追踪时,你只能人工拼日志、猜调用顺序、对时间戳,排障会极其低效;而有统一 tracing 后,你能直接看到请求路径、每一跳耗时和异常点。对分布式系统来说,trace 往往是"把碎片信息拼成一条完整故事线"的关键工具。

401. 你如何设计统一日志字段规范?

我会把日志分成三层字段:

  1. 基础运行字段:时间、级别、服务名、实例、环境、线程名;
  2. 链路字段:TraceId、SpanId、请求 ID、用户 ID、租户 ID;
  3. 业务字段 :业务单号、接口名、错误码、耗时、下游服务名。
    核心目标不是"字段越多越好",而是让日志既能检索、又能和指标、trace 关联。Spring Boot 官方文档把 logging、metrics、traces 作为 observability 的三大支柱,而 tracing 文档明确支持日志关联 ID;OpenTelemetry 规范也明确支持在日志中携带 TraceId 和 SpanId 来做关联。
402. 如何给线程池、数据库连接池、MQ 消费积压建立有效监控?

我会按"资源是否耗尽、请求是否排队、业务是否受影响"三个层面建监控。

  • 线程池:活跃线程数、队列长度、拒绝次数、任务耗时;
  • 数据库连接池:活跃连接、空闲连接、等待时间、获取超时次数;
  • MQ :consumer lag、积压条数、消费 RT、重试/死信数量。
    关键不是把指标都打出来,而是要能直接回答"是不是资源打满了、是不是开始排队了、是不是已经影响业务 RT/错误率"。Spring Boot 官方 observability 文档明确支持统一的 metrics/traces 观测模型;Confluent 消费者文档也把 lag 和 consumer group 状态作为核心观测对象。
403. 为什么"平均 RT 正常"不代表系统健康?

因为平均值会掩盖长尾。比如 99 次请求都是 10ms,1 次请求是 5s,平均值看起来可能还行,但用户已经真实感知到故障了。系统健康更应该看 P95/P99、错误率、超时率、线程池排队、consumer lag、连接池等待这些"尾部和拥塞指标",而不是只看均值。Spring 和 OpenTelemetry 的 observability 体系都强调 traces 和 metrics 是为了还原整体行为与单次请求行为,而不只是看一个均值。


八、按场景整理的重点题

1. 高 QPS 场景

404. 高 QPS 场景下,系统瓶颈通常先出现在网关、应用、缓存、数据库中的哪一层?为什么?

没有固定答案,要看链路形态。但在读多写少、高并发访问场景里,最先出问题的往往是网关限流能力、缓存热点、数据库连接池或热点 SQL。如果缓存命中足够高,数据库未必先炸;如果热点 key 没治理好,Redis 反而会先抖;如果入口没限流,网关和应用线程池也可能先满。正确思路不是猜哪层一定先挂,而是按"入口承压、缓存命中、下游资源池、核心存储"逐层看瓶颈。Redis 官方延迟文档、Spring observability 文档都强调要结合系统级和中间件级指标一起看。

405. 读多写少系统为什么适合多级缓存?

因为读多写少时,数据重复读取非常多,适合用"本地缓存 + Redis + 数据库"这种分层结构,把热点请求尽量挡在更靠前的层。这样能显著降低数据库压力和网络 RTT。代价是缓存一致性变复杂,但在读多写少场景下,这个代价通常是值得付出的。Redis 官方数据类型和缓存相关文档本身就是围绕高频读优化设计的。

406. 热 Key 问题如何定位和治理?

定位上,我会看 Redis 热点统计、客户端埋点、访问分布、实例 CPU/网络流量和 --hotkeys/slowlog/latency monitor。治理上通常有几类办法:本地缓存分担、热点 key 拆分、读写分离、异步预热、限流、热点兜底。Redis 官方 observability 和 latency 文档都给出了 hotkeys、latency monitor、slowlog 这类排查手段。

407. 为什么高 QPS 系统里常常需要限流,而不是一味扩容?

因为流量并不总是可线性扩容,而且很多瓶颈根本不在入口机器数量,比如数据库连接数、Redis 热点、下游第三方接口配额、MQ 消费能力等。限流的价值是给系统留出生存空间,让故障局部化,而不是等所有下游一起被打穿。Spring 观测体系之所以强调指标和 tracing,也是因为限流策略是否有效,要靠 RT、错误率和资源池指标来验证。

408. CDN、静态化、读写分离、ES 检索分流分别适合什么场景?
  • CDN:适合静态资源、公开内容分发;
  • 静态化:适合高频但可预生成的页面或接口结果;
  • 读写分离:适合数据库读压力大、且可接受副本延迟的场景;
  • ES 检索分流 :适合复杂搜索、全文检索、聚合查询,不适合强事务读。
    它们本质上都是在做"把不同类型的读流量分流到最擅长处理它的系统"。MySQL 复制与读写分离、Elastic 的搜索定位,都支持这个设计思路。
409. 如果首页接口 QPS 极高,你会如何做缓存分层与降级?

我会优先做:CDN/边缘缓存、网关短期缓存、本地缓存、Redis 缓存,再加数据库回源保护。首页通常还适合做静态片段化、异步刷新和热点预热。降级策略上,优先保核心模块,非核心模块返回兜底内容或空结果,必要时直接静态页面兜底。核心不是"缓存层数越多越好",而是让回源有闸门、让热点可预热、让故障时能快速降级。Redis 官方延迟和淘汰文档也说明了缓存层本身需要被当成核心组件治理。

2. 高 TPS 场景

410. 高 TPS 写链路最容易遇到哪些问题?

最常见的是:数据库锁竞争、索引维护开销、日志刷盘压力、大事务、消息堆积、分布式事务协调、幂等冲突和下游服务抖动。写链路和读链路不一样,写操作会直接放大一致性和资源竞争问题。MySQL 关于事务隔离、锁和 redo/binlog 的文档都体现了写链路的这些成本。

411. 为什么写多场景比读多场景更容易暴露一致性问题?

因为读多场景很多时候可以靠缓存、副本、近实时结果去兜,而写多场景天然要面对"谁先成功、谁后成功、是否重复、是否回滚、是否补偿"的问题。只要多个系统都要被改,分布式一致性就会立刻浮出来。MySQL XA/2PC 文档、Saga 模式定义都体现了这一点:跨系统写入协调远比单纯读取复杂。

412. MQ 削峰如何保护数据库?

它的本质是把瞬时写压力先缓存在队列里,再按数据库能承受的节奏慢慢消费。这样数据库不用直接面对所有毛刺流量。Kafka 和 RabbitMQ 的官方资料都体现了这一点:消费者速率可以和生产速率解耦。前提是你要监控 lag、消费失败、重试和死信,否则只是把压力从数据库转移成了队列积压。

413. 分布式事务、幂等、重试、补偿如何组合使用?

一个更务实的组合通常是:关键本地事务先保证成功,再通过消息或事件驱动下游;消费侧必须幂等;短暂失败做有限重试;重试仍失败则进入补偿或人工处理。也就是说,很多系统不是强行把所有节点绑成一个大事务,而是用"本地事务 + 幂等 + 重试 + 补偿"组合出最终一致。Oracle 和 MySQL 关于 XA、结构化事务协议与补偿模式的资料都支持这种工程取舍。

414. 如果订单、库存、账户都要写,你怎么设计主链路和异步链路?

我会先明确谁是主链路的"真相动作"。通常订单创建是主链路核心,库存和账户要看业务要求决定是否同步强约束。若必须同步强校验,就把最关键的约束压缩到最小同步区;其他像通知、积分、搜索索引、报表统计放异步。这个题的关键不是背某个模式,而是体现主链路尽量短、异步链路要可补偿

415. 如何设计一个"高 TPS 但不强求实时强一致"的业务系统?

常见思路是:入口快速落本地事务或持久消息,主链路只保最核心约束;其他关联动作走 MQ 异步化;所有消费环节幂等;通过补偿任务、对账和告警把最终状态收敛回来。这个设计本质上是在用最终一致换写吞吐和系统可用性。CAP 和 BASE 的工程落地,本来就是这种取舍。

3. 高吞吐场景

416. 高吞吐系统为什么强调批处理、顺序写、异步化和减少锁竞争?

因为这几种手段都在做同一件事:摊薄固定开销。批处理摊薄协议和网络成本,顺序写减少随机 IO,异步化降低同步等待,减少锁竞争则降低线程阻塞和上下文切换。Kafka 的设计就是这套思路的典型代表。

417. 线程池、批消费、Kafka 分区、批量落库之间如何协同设计?

要让它们围绕同一个目标协同:分区提供并行度,线程池承接消费并发,批消费提升消息处理吞吐,批量落库减少数据库往返次数。但这里有个前提:各层容量要匹配,否则前面批得很猛,后面数据库批量写跟不上,最终还是堵在下游。设计关键是让并发度、批大小和下游承受能力闭环匹配

418. JVM GC 在高吞吐场景里为什么容易成为隐性瓶颈?

因为高吞吐意味着短时间内对象分配非常快,如果批处理、反序列化、临时集合、日志对象都很多,就会把年轻代分配速率和回收频率迅速拉高。哪怕单次 GC 不算特别长,也会形成持续抖动,最终拖慢吞吐和尾延迟。Oracle 的 GC 调优文档一直强调:吞吐和停顿是要一起权衡的。

419. 如果吞吐上不去,你会先看 CPU、锁、GC、磁盘、网络、队列中的哪几个?

我会先问一句:系统是在"算",还是在"等"。如果 CPU 不高、队列很长,就先看锁、下游依赖、磁盘、网络和资源池;如果 CPU 很高,再看算法、序列化、热点方法和 GC。如果是 Kafka / MQ 链路,还要看 lag 和分区热点。也就是说,排查吞吐问题不是固定顺序,而是先判断瓶颈类型

420. 高吞吐系统里为什么"每条都同步刷库"通常不可持续?

因为每条都同步刷库意味着每条都要走一次完整数据库写路径:网络往返、事务、日志、索引维护、锁、可能还要同步调用别的服务。吞吐一高,这个模式很快就会把数据库和下游资源池打到极限。高吞吐系统通常要靠批量、异步、日志化落地来摊薄这些成本。MySQL 的 redo/binlog 和事务机制本身就说明了单条写的固定成本并不低。


九、补充的更进阶专题

1. Java / 并发进阶

421. 虚拟线程和 Reactor 模型分别适合什么场景?能否互相替代?

虚拟线程适合把大量阻塞型并发任务用更接近同步代码的方式写出来,尤其是 IO 密集型服务;Reactor 更适合事件驱动、非阻塞、少线程处理大量连接的模型,常见于网关和网络框架。它们在某些场景能达到相近目标,但思维模型不同,不是简单一键替代。Oracle 官方文档把 structured concurrency 和 virtual threads 作为简化并发编程的重要方向,而 Reactor 本质是另一种事件驱动抽象。

422. 无锁编程真的一定更高效吗?它的代价是什么?

不一定。无锁编程减少了互斥锁阻塞,但会引入 CAS 重试、自旋开销、ABA 问题、可见性语义和实现复杂度。冲突低时它很有优势,冲突高时反而可能大量浪费 CPU。面试里这题更好的回答不是"无锁更快",而是"无锁适合冲突低、临界区小的场景"。这是并发设计的通用规律。

423. 高并发场景下,如何避免共享可变状态?

常见方法是:尽量不可变对象、线程封闭、局部变量化、消息传递、按分区/按 key 串行化,以及把共享状态收敛到少数受控组件里。这个题本质不是某个 API 怎么用,而是并发设计原则:少共享,就少同步;少可变,就少竞态。Java 结构化并发文档也强调把相关任务收拢到一个范围里,提升可控性和可观测性。

424. 你如何设计一个高性能、可观测、可隔离的异步任务执行框架?

我会先做任务模型抽象,再把线程池隔离、队列容量、拒绝策略、超时、重试、幂等、指标、trace 和日志全部内建进去。性能来自资源池设计,隔离来自池和队列边界,可观测来自统一 metrics/logs/traces。Spring Boot observability 和 Java structured concurrency 的资料都支持这种思路:并发执行不只是跑起来,还要能管得住、看得见

2. JVM / 性能进阶

425. JIT 预热对服务启动初期性能有什么影响?

因为热点代码需要运行一段时间后才会被 JIT 优化成更高效的机器码,所以服务刚启动时,很多路径还处在解释执行或较低优化级别,性能可能不如稳定运行后的状态。这个现象会导致"冷启动性能"和"稳定期性能"不一样,所以压测和生产都要考虑预热阶段。Oracle/JVM 文档和 JEP 资料都支持 Java 运行时优化这一事实。

426. 为什么压测结果和生产结果经常不一致?

因为压测环境往往缺了很多真实条件:真实数据分布、缓存命中变化、JIT 预热状态、网络抖动、外部依赖、资源争用、日志量、GC 压力、发布配置差异。尤其 PostgreSQL 文档对 row estimate 的说明也提醒了一个事实:优化器对数据分布非常敏感,数据不真实,计划就可能不真实。也就是说,压测不是没价值,而是越不像生产,结论越容易失真

427. 逃逸分析在什么情况下能带来收益?为什么有时看不出来?

当对象没有逃出方法或线程作用域时,JIT 才有机会做标量替换、栈上分配近似效果或锁消除,所以收益主要来自减少分配和同步成本。之所以有时看不出来,是因为是否触发优化、优化收益是否足够明显,都受代码形态、热点程度和运行时条件影响。换句话说,逃逸分析不是"开了就一定肉眼可见",它是 JIT 在合适条件下的机会优化。

428. 线上 GC 正常,但 RT 仍偶发尖刺,除了 GC 你还会怀疑什么?

我会怀疑 safepoint 停顿、JIT 编译、线程池瞬时排队、锁竞争、下游抖动、网络毛刺、磁盘 flush、DNS/TLS 建连,以及日志或追踪采样带来的局部开销。GC 正常只能排除一部分原因,不代表 JVM 和系统层就没别的瞬时抖动。Spring observability 和 tracing 的价值,正是在这种"平均正常但偶发尖刺"的问题里体现出来。

3. 数据库进阶

429. PostgreSQL 和 MySQL 在你过往项目里你会如何选型?

如果业务更偏互联网典型 OLTP、团队对 MySQL 经验更丰富、生态中间件更成熟,我通常更容易选 MySQL;如果业务对复杂查询、丰富数据类型、扩展能力和 PostgreSQL 生态更有诉求,我会认真考虑 PostgreSQL。PostgreSQL 官方文档对 EXPLAIN、行数估算和丰富 SQL 能力的展示很强;MySQL 官方文档在复制、InnoDB、读写分离等工程实践上也非常成熟。最终选型应该看团队经验 + 业务查询模式 + 运维能力

430. PostgreSQL 更适合哪些复杂查询 / 数据类型场景?

一般会更适合复杂 SQL、较重分析型查询、窗口函数密集、统计估算要求高,以及需要更丰富扩展和数据类型支持的场景。PostgreSQL 官方 EXPLAIN 和 row estimation 文档也体现了它在查询优化和可解释性上的强项。不是说 MySQL 做不了,而是 PostgreSQL 在这类场景下常常更自然。

431. 为什么执行计划"估算行数不准"会导致糟糕的查询计划?

因为优化器是按"估算代价"选计划的,行数估算一旦偏差很大,可能就会错误地选择连接顺序、扫描方式和 join 算法。PostgreSQL 官方明确强调,rows 是估算值,理想情况下应接近实际返回量;一旦这个估算失真,整棵计划树都可能走偏。

432. 如果一张表既有高频 OLTP,又有复杂分析查询,你会如何拆分方案?

我通常不会让同一套在线表结构硬扛两种目标,而是优先考虑读写分离、异步同步到分析库/ES/OLAP、冷热分层,或者把复杂分析转移到专门系统。因为 OLTP 追求的是低延迟和强事务,复杂分析追求的是扫描和聚合能力,二者天然目标冲突。Elastic 和数据库文档都在各自领域强调了这种边界:检索/分析系统和事务系统最好各司其职

4. Redis / 缓存进阶

433. 一致性要求很高时,为什么很多人会减少缓存参与主写链路?

因为缓存一旦参与主写链路,你就同时面对数据库提交、缓存更新、失败重试和时序竞争问题,一致性治理会迅速复杂化。很多团队会选择让数据库做真相源,缓存只做派生副本,避免把缓存写成功也变成主交易成功条件的一部分。Redis 官方的定位也更偏高性能数据结构服务,而不是事务真相系统。

434. 如何设计缓存预热、缓存兜底、缓存降级三层方案?
  • 预热:上线前或低峰时把热点数据提前灌进缓存;
  • 兜底:缓存未命中或异常时,有受控回源、本地缓存或静态默认值;
  • 降级 :缓存层出故障时,不让所有请求直接穿库,而是限流、返回简化结果或关闭非核心模块。
    这三层的目标分别是:少冷启动、少直接穿透、少故障放大。Redis 延迟和 observability 文档都说明,缓存层异常必须被当成一级故障治理。
435. 如果本地缓存和 Redis 数据不一致,怎么控制风险?

常见做法是:本地缓存 TTL 更短、版本号控制、消息通知失效、只缓存少量高热点只读数据,并允许在关键场景下直接绕过本地缓存。要接受一个事实:本地缓存和 Redis 的一致性很难做到强同步,所以最重要的是控制不一致的影响面和持续时间

436. 如何识别"伪热点"与"真热点"?

真热点是长期、高频、持续命中的少数 key 或少数接口;伪热点通常是短时间突发、发布后抖动、缓存预热不充分或某批任务集中访问造成的。识别上要看时间维度和分布稳定性,而不是只看某一分钟流量高。Redis 的 hotkeys、latency monitor 和应用埋点结合起来,才能分清到底是持续热点还是瞬时尖刺。

5. Kafka / MQ 进阶

437. 为什么分区不均会让 Kafka 集群看起来"整体不忙,但局部拥堵"?

因为 Kafka 的并行度建立在 partition 上,如果少数 partition 特别热,而其他 partition 很闲,集群总资源看上去还有余量,但热点分区对应的 broker、磁盘和消费者已经先堵住了。所以 Kafka 性能问题经常不是"整体资源不够",而是"分布不均"。Confluent 的消费者组和 lag 机制本身就很容易暴露这种局部不平衡。

438. 如何设计一个既支持重试、又避免无限重试风暴的消费系统?

我会做分级重试:短暂错误走有限次数即时重试;需要退避的放到延迟重试 topic;超过阈值进入死信;所有重试都配幂等和告警。关键点是重试必须有上限、有分类、有退避,否则它不叫可靠性,叫自我攻击。RabbitMQ 和 Kafka 的官方可靠性资料都支持这种思路。

439. 为什么消息中间件不能替代数据库事务?

因为 MQ 解决的是消息传递和异步协调,不天然等价于资源的一体化提交。你把消息发出去了,不代表数据库、缓存、外部接口都已经在一个原子边界里提交成功。它能帮助最终一致,但不能天然替代数据库事务约束。OAuth/OIDC、分布式系统和 MQ 文档都体现了这种边界:协议和消息机制不能自动代替业务事务语义。

440. 如何做"生产可追踪、消费可追踪、补偿可追踪"的消息治理体系?

要把消息 ID、业务单号、TraceId、生产结果、消费结果、重试次数、死信流向、补偿记录统一打通。生产端要知道消息是否发成功,消费端要知道处理到哪一步,补偿端要能反查原始业务和消息流转。Spring observability、OpenTelemetry 的 trace/log 关联能力非常适合支撑这种治理体系。

6. 架构设计进阶

441. 如何从单体逐步演进到微服务,而不是一开始就全部拆开?

更稳妥的做法通常是:先在单体内部清理边界和模块,再优先把边界清晰、变更独立、流量明显或团队独立的模块拆出去,逐步建立注册发现、网关、配置中心、日志追踪、监控告警这些基础设施。也就是说,先做边界清晰,再做部署拆分,而不是一上来就把一个脏单体切成很多脏服务。Spring Boot 的 observability 文档反过来也说明了一个事实:没有基础治理能力,微服务只会更难维护。

442. 一个系统拆分服务时,你优先按领域边界拆,还是按流量热点拆?为什么?

通常我会先按领域边界拆,因为这决定了职责、数据归属和团队自治;如果热点非常明显、单点瓶颈严重,再在领域边界内针对热点做进一步拆分或旁路扩展。因为只按流量热点拆,很容易把领域模型拆碎,后面一致性和协作成本会很高。更合理的顺序是:先领域边界,再针对热点优化

443. 你如何说服团队不要过度设计?

我通常不会抽象地说"别过度设计",而是让团队回到业务目标、当前规模、真实瓶颈和维护成本上。比如:有没有真实需求支撑这套复杂度?有没有数据证明当前方案已经扛不住?有没有人能长期维护这套系统?工程上最有效的说服方式,是把复杂度和收益摆在一起看。官方文档也往往都是围绕明确场景给能力,不会鼓励无边界堆技术。

444. 一个核心系统的"稳定性红线"你会如何定义?

我会从几个维度定义:核心接口可用率、错误率上限、P99 RT 上限、数据不一致容忍度、消息积压阈值、线程池/连接池安全水位、可接受降级范围,以及发布和回滚标准。红线的价值是把"稳定性"从一句口号变成可以监控、可以告警、可以一票否决的指标。Spring observability 官方支持的 metrics/traces,正是这些红线的基础。

445. 你如何做容量评估与扩容预案?

我会先根据历史峰值、增长趋势、活动预期和关键资源上限,估算入口流量、缓存命中、数据库 QPS、MQ lag、线程池和连接池水位,再做压测和冗余预留。扩容预案不仅要有"多加几台",还要有"哪里可能先挂、回滚怎么做、热点怎么切、降级怎么开"。也就是说,容量评估不是只算机器数,而是对整条链路做容量预算

446. 如果让你负责一个日均亿级请求的平台后端,你会先补哪些基础设施能力?

我会优先补:统一网关、服务注册发现、配置中心、统一日志与 tracing、指标监控与告警、限流熔断隔离、缓存治理、消息治理、发布回滚和容量评估体系。因为到这个量级,业务代码本身往往不是先挂的,先出问题的是基础治理能力不够。Spring Boot 和 OpenTelemetry 官方文档对 observability 的强调,本质上就说明没有观测和治理,规模一大就很难稳。


十、综合场景题(高级)

447. 一个支付下单接口 RT 飙高、错误率升高、数据库连接池接近打满,你的排查顺序是什么?

我会先止血,再定位。

先看入口流量有没有异常、是否需要限流降级;再看数据库连接池为什么快满:是慢 SQL、锁等待、大事务,还是下游事务没及时提交;同时看线程池是否排队、缓存是否失效、MQ 是否重试风暴。最后结合 trace 把一次请求拆开,看慢在网关、服务、数据库还是外部依赖。Spring Boot observability 对 metrics/traces/log correlation 的支持,就是为了这种场景快速串联证据。

448. 某个大促场景下 Redis 热 Key 导致接口抖动,你如何止血和后续治理?

止血上,我会优先做本地缓存兜底、热点 key 拆分、限流、只保核心接口,必要时把部分读请求静态化或降级。后续治理上,要把热点识别、预热、热点分片、访问模式改造、TTL 策略和观测告警全补上。Redis 官方 observability 和 latency 文档都说明,热点和延迟问题不能只在事故时看,要提前纳入治理。

449. Kafka 某个消费组严重积压,导致下游数据库写满,你如何处置?

第一步是控流:必要时限生产、暂停非核心消费者、关闭不必要下游写入。第二步看积压原因是分区热点、消费者抖动、下游数据库慢还是重试风暴。第三步再决定扩消费者、扩分区、批量落库、分级消费,还是把部分消息暂时旁路。重点不是"赶紧加消费者",而是先阻止下游继续被压垮

450. 某服务 Full GC 频繁,但堆不算大、对象也不算多,你会怎么分析?

我会怀疑:对象生命周期不匹配、晋升过快、元空间压力、直接内存、类加载器泄漏、显式 GC、缓存无边界,以及 GC 参数和应用行为不匹配。堆不大、对象数不多,不代表没有内存布局问题。Oracle 的 JVM 诊断和 GC 文档都说明:GC 频繁的根因不能只看"对象多不多",还要看谁活得久、谁回不掉、谁在晋升

451. 一次发布后,只有部分机器 RT 升高,你会如何定位是代码、配置、流量、JVM 还是依赖差异?

我会先对比"有问题实例"和"正常实例"的差异:版本、配置、JVM 参数、实例资源、接收流量分布、下游连接去向。然后看 trace 和指标:是不是某些实例命中了特殊流量、特殊下游,或者 JVM 行为不同。只有部分机器异常,往往意味着问题更偏实例差异、配置差异或流量命中差异,而不是全局代码逻辑本身。

452. 数据库主从延迟突然上升,业务频繁读到旧数据,你如何兼顾止血和后续优化?

止血上,我会把关键读流量切回主库或启用会话级读主策略,避免继续读旧数据;同时查从库回放、网络、IO、大事务和主库写放大情况。后续优化则要从复制延迟根因入手:减少大事务、优化 SQL、提升从库能力、明确哪些读必须强一致。MySQL 复制默认异步,所以"读到旧数据"不是偶发 bug,而是架构必须面对的事实。

453. 一个核心链路出现"偶发重复扣款",你会从幂等、锁、重试、MQ、一致性哪个方向切?

我会优先从幂等和重试时序 切,因为"偶发重复"最常见就是请求重试、消息重复、回调重复,但业务没做好幂等。然后再看锁是否只保护了单实例、MQ 是否重复投递、状态机是否允许非法重复推进。这个问题很少是某一把锁没加好那么简单,通常是幂等边界没设计完整

454. 网关层 CPU 不高,但连接数暴涨、RT 上升,你会怀疑哪些问题?

我会重点怀疑:下游慢导致连接堆积、事件循环被阻塞、keep-alive/连接复用异常、TLS 建连异常、短连接激增、限流规则失效。CPU 不高说明网关不一定在忙算,更可能是在等连接、等下游或被线程模型拖住。这个场景很典型地需要把网络、线程模型和下游调用一起看。

455. 一个高并发读接口要支撑 10 倍流量增长,你会如何做容量设计?

我会先算读路径分层:CDN/本地缓存/Redis/数据库各自要承担多少比例,再看热点分布、缓存命中目标、限流阈值、降级策略和数据库最坏回源压力。10 倍流量不是"10 倍机器"那么简单,关键是要让大部分流量止步在更靠前的层,别让数据库按 10 倍增长。容量设计本质上是按层分摊流量,而不是按实例硬抗

456. 一个高 TPS 写接口要在不大改业务的前提下提升 5 倍吞吐,你会先改哪里?

我会先看最便宜、收益最大的地方:缩短事务、减少索引、批量写、异步化非核心动作、削峰入队、优化连接池与线程池、减少不必要的同步 RPC。如果不大改业务,就优先从写放大最严重的环节下手,而不是先重构整个领域模型。MySQL 的事务、索引和日志机制本来就是高 TPS 写链路的主要成本来源。

457. 订单系统要求"下单成功后,库存、优惠券、积分、通知"全部联动,你如何设计整体架构?

我会把下单主链路收敛到最关键的同步动作,通常是订单落库和必要的库存约束;优惠券返还、积分发放、通知推送、搜索索引等更适合走异步事件。所有异步消费者都必须幂等、可补偿、可追踪。整体上是"核心约束同步,扩展动作异步",这样既保证主链路稳定,又便于后续演进。

458. 如果让你做一次系统稳定性治理专项,你会按什么步骤推进?

我一般会按这几步:先建立基线指标和红线;再梳理核心链路和单点风险;然后补齐观测、限流熔断隔离、资源池治理、缓存治理和发布回滚;最后通过压测、故障演练和复盘机制固化。稳定性治理不是做一堆零散优化,而是把"发现问题、阻断放大、快速恢复、长期治理"变成体系。Spring Boot observability 官方文档恰好就是这个体系里的基础。

459. 如果让你做一次中间件治理专项(Redis / MQ / DB / 线程池),你会优先治理什么?

我会优先治理最容易"放大全链路故障"的地方:线程池和连接池容量边界、Redis 热点/大 key/淘汰、MQ lag/重试/死信、数据库慢 SQL/大事务/主从延迟。优先级不是按技术栈喜好排,而是按"谁最可能先把系统拖死"来排。治理的第一目标是边界清晰、问题可见、故障不扩散

460. 如果让你评审一个新系统架构方案,你会从哪些维度提问?

我会至少从这些维度问:业务目标是什么、流量和数据规模多少、核心链路和真相源在哪里、一致性要求是什么、失败怎么处理、扩容怎么做、观测怎么做、发布回滚怎么做、热点和容量怎么治理、安全认证怎么做。好的架构评审不是看图漂不漂亮,而是看它有没有回答规模、失败、演进、治理这四类核心问题。Spring observability、OAuth/OIDC、数据库执行计划和追踪相关官方文档,本质上都在提醒一件事:系统设计不能只看功能,还要看运行时可治理性。

相关推荐
执笔论英雄2 小时前
【vllm】vllm根据并发学习调度
java·学习·vllm
瑶总迷弟2 小时前
Python入门第6章:字典(键值对数据结构)
java·数据结构·python
斯班奇的好朋友阿法法2 小时前
ollama离线导入大模型
服务器·前端·javascript
dgw26486338092 小时前
深信服数据传输安全-NPN-(2)
网络·安全·vpn
o丁二黄o2 小时前
【MyBatisPlus】MyBatisPlus介绍与使用
java
_MyFavorite_2 小时前
JAVA重点基础、进阶知识及易错点总结(14)字节流 & 字符流
java·开发语言·python
Eric.Lee20212 小时前
python实现pdf转图片png
linux·python·pdf
剑锋所指,所向披靡!3 小时前
linux的目录结构
linux·运维·服务器
zt1985q3 小时前
本地部署 Home Assistant 高级自动化 AppDaemon 并实现外部访问
运维·服务器·网络·网络协议·自动化