实践场景与业务实践 —— 性能/锁/并发/设计/排查/架构
10 题 · 平均 2.9/5 · low 2.67 · medium 3 · high 3 · 第 30 轮
> 最后更新:2026-10-28 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:压测能过而生产扛不住,本质是压测环境与生产环境不同构——机器规格、数据量级、缓存状态、下游依赖至少一项失真,导致压测数字虚高。
> 关键词:环境差异 · 缓存命中率 · 数据量级 · 连接池
> 边界:压测只代表"理想流量模型",容量评估必须用生产同构环境或流量回放,不能拿 demo 数据结论当生产结论。
### L2 · 核心讲解
**核心考点定位**:考察能否把"压测通过但生产失败"从单个现象拆成"压测环境与生产环境的同构性"这一根因,并逐项举证;面试官想排除只归因于机器配置差或只归因于代码 bug 的单一归因。
**机制链(≥3 环)**:
1. 压测用小数据量 + demo 数据 → 查询全走缓存/小索引:压测时命中率高、扫描行数少,单请求 RT 极低,600 QPS 轻松扛住。
2. 生产数据量级大且冷启动 → 缓存未预热、命中率骤降:请求直达数据库,慢查询与回表放大,单请求 RT 上升,可用吞吐被压到 300 QPS 以下。
3. 生产长事务占满连接 → 连接池与线程池排队:数据库 `max_connections` 与连接池 `maxTotal` 成为新瓶颈,请求在 `getConnection()` 处阻塞,300 QPS 即出现堆积与超时。
**证据锚点**(去重 ≥6):
- `max_connections=151`:MySQL 默认连接上限;连接被长事务占满后,应用拿不到连接。
- `maxTotal` / `maxWaitMillis`:HikariCP 等连接池参数;池满且等待超时即抛 `ConnectionTimeoutException`。
- `slow_query_log` + `long_query_time=0.1`:定位慢查询;重点看 `Rows_examined` 与 `Rows_sent` 比值是否扫描放大。
- `innodb_buffer_pool_size`:生产数据量增长后索引页放不进 Buffer Pool,命中率下降、回表变多。
- `ThreadPoolExecutor`:生产线程池被打满时,任务在队列排队,RT 与吞吐同时恶化。
- `Full GC`:生产堆内存压力大时,STW 拖慢全局吞吐,压测环境堆小且冷数据少不易触发。
- `cache hit ratio`:Redis 命中率;未预热时首波流量全部穿透到 DB。
**对比与权衡**:
| 方案 | 正确性 | 吞吐/成本 | 失败模式 | 适用边界 |
|---|---|---|---|---|
| 压测环境与生产同构 | 高 | 高 | 环境搭建成本高、周期长 | 容量评估首选 |
| 生产流量回放 | 高 | 中 | 依赖链路采集完整、数据脱敏 | 已有全链路监控时 |
| 仅 demo 数据压测 | 低 | 低 | 缓存命中率虚高、结论失真 | 只验证功能,不验证容量 |
**常见误区**:
1. 错误:压测能过就是代码没问题。→ 正确:压测只证明"该环境、该数据、该流量模型"下能过。→ 为什么:环境、数据量、缓存状态、下游依赖任一不同,结论即失效。
2. 错误:生产扛不住一定是机器配置差。→ 正确:配置只是一环,数据量级与缓存命中率往往是主因。→ 为什么:同配置下数据量增大,索引页放不进 Buffer Pool,扫描放大推高 RT。
3. 错误:加缓存就一定能扛住。→ 正确:缓存能扛读,但未预热、写热点、穿透仍会打到 DB。→ 为什么:未预热时缓存是空的,首波流量全穿透,反而放大 DB 压力。
**适用边界**:若压测环境与生产完全同构(同规格、同数据量、同流量、同下游 RT)仍扛不住,才怀疑应用代码;否则先解决环境失真。此结论适用于容量评估类问题,不适用于功能正确性类问题。
### 延伸追问(≥3)
1. **为什么压测用 demo 数据会让结论虚高?**
- 要点:demo 数据量小、分布均匀,能全命中缓存、扫描行数少;生产数据分布不均会导致索引失效、回表放大。
- 追问落点:如何构造与生产分布一致的数据集(抽样 vs 全量克隆)?
2. **缓存未预热为什么会放大到数据库?**
- 要点:冷启动命中率为 0,所有请求打到 DB;热点 key 重建又引发击穿与穿透。
- 追问落点:预热策略(定时预热/启动预热)与防击穿(互斥锁/逻辑过期)如何配合?
3. **连接池打满和数据库 max_connections 是什么关系?**
- 要点:连接池耗尽通常先于 DB 上限;池大小、等待超时、DB `max_connections` 三者要联动设计。
- 追问落点:如何定位是连接池瓶颈还是 DB 瓶颈(看 `getConnection()` 超时 vs `SHOW PROCESSLIST`)?
### 面试者参考回答
**【结论】** 压测能过而生产扛不住,本质是压测环境与生产环境不同构:机器规格、数据量级、缓存状态、下游依赖至少一项失真,导致压测数字虚高。
**【三点】** 第一,先查环境差异:机器规格是否一致、是否有其他进程抢占 CPU/内存,用 `top`/`free` 对比;服务器上多个服务共享资源会抢 CPU 与 IO。第二,再查数据与缓存:生产数据量级大且冷启动未预热,缓存命中率低,请求直达数据库触发慢查询与回表放大,用 `slow_query_log`、`Rows_examined`、Redis `hit ratio` 佐证。第三,最后查资源瓶颈:连接池 `maxTotal` 与 `maxWaitMillis`、数据库 `max_connections`、线程池队列是否被打满,长事务占连接导致 300 QPS 就排队超时。处置按根因分类:预热缓存、优化慢 SQL、加索引、调连接池与线程池。
**【边界】** 若压测环境与生产完全同构(同规格、同数据量、同流量、同下游 RT)仍扛不住,才怀疑应用代码;否则先解决环境失真。任何容量结论都要声明适用的数据量与流量模型,不能拿 demo 数据结论当生产结论。
> 最后更新:2026-10-28 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:Jedis 是同步阻塞的轻量客户端,Redisson 是 Netty 异步的功能全栈封装;功能简单用 Jedis,要分布式锁、布隆过滤器等高级能力用 Redisson。
> 关键词:同步阻塞 · Netty 异步 · 分布式锁 · 线程安全
> 边界:两者可共存——Jedis 做基础缓存命令,Redisson 做锁与分布式对象;不是二选一。
### L2 · 核心讲解
**核心考点定位**:考察 Redis 客户端的底层模型差异——同步阻塞 vs 异步非阻塞,以及由此衍生的线程安全与高级能力差异;面试官想排除"两者只是 API 风格不同"的表面认知。
**机制链(≥3 环)**:
1. Jedis 是同步阻塞客户端 → 每次命令一个往返、单连接非线程安全:多线程共用一个 `Jedis` 实例会串数据,必须用连接池。
2. Redisson 基于 Netty 异步 → 命令经 Channel 非阻塞发出、事件循环回调:单连接可复用,天然线程安全,无需手动连接池。
3. Redisson 把 Redis 能力封装成对象 → 分布式锁 `RLock`、布隆 `RBloomFilter`、限流 `RRateLimiter`:内部用 Lua 脚本保证原子性,Jedis 需手写 `SETNX`+`Lua`。
**证据锚点**(去重 ≥6):
- `JedisPool`:Jedis 官方连接池;`maxTotal`/`maxWaitMillis` 控制并发上限,池满线程阻塞。
- `RedissonClient`:Redisson 入口,`getLock()`/`getBloomFilter()` 拿到分布式对象。
- `RLock` + `lockWatchdogTimeout=30000`:Redisson 默认锁租约 30 秒,watchdog 每 TTL/3 续期。
- `RBloomFilter`:布隆过滤器,误判率约 1%,适合判存在性(防穿透)。
- `tryLock(waitTime, leaseTime, TimeUnit)`:Redisson 可重入、公平锁、可指定等待与租约。
- `Netty`:Redisson 底层依赖 Netty 事件循环与 `ByteBuf` 编解码。
- `RedisTemplate`:Spring 默认封装 Jedis/Lettuce,与 Redisson 对象模型不同。
**对比与权衡**:
| 方案 | 底层模型 | 线程安全 | 高级能力 | 适用边界 |
|---|---|---|---|---|
| Jedis | 同步阻塞 | 需连接池 | 无(基础命令) | 简单缓存、依赖最轻 |
| Redisson | Netty 异步 | 内置安全 | 锁/布隆/限流/延迟队列 | 分布式协调、锁 |
| 两者共存 | 混合 | 各自处理 | 全覆盖 | 需要基础命令又要锁 |
**常见误区**:
1. 错误:Jedis 和 Redisson 只是 API 风格不同。→ 正确:底层模型不同——同步阻塞 vs Netty 异步。→ 为什么:阻塞模型在命令耗时期间占住线程,异步模型靠事件循环复用连接。
2. 错误:一个 `Jedis` 实例可以多线程共享。→ 正确:`Jedis` 实例非线程安全,必须用连接池。→ 为什么:共享单连接时命令交错会串数据,读到别人的结果。
3. 错误:Redisson 一定比 Jedis 好。→ 正确:Redisson 功能全但依赖重、启动开销大、抽象层高。→ 为什么:只做 GET/SET 缓存时,Jedis 更轻、依赖更少、可控性更强。
**适用边界**:只做缓存、追求依赖最轻用 Jedis;需要分布式锁、布隆过滤器、限流、延迟队列、分布式集合用 Redisson;两者可共存——Jedis 做基础命令,Redisson 做锁与分布式对象。若项目用 Spring Data Redis,默认客户端常是 Lettuce,Redisson 是独立接入。
### 延伸追问(≥3)
1. **Redisson 的看门狗(watchdog)如何防死锁?**
- 要点:默认锁租约 30 秒,watchdog 每 TTL/3 检查持锁线程,存活则续期、崩溃不续期让锁自然过期。
- 追问落点:业务线程 hang 死时 watchdog 仍续期,业务侧如何用超时兜底?
2. **为什么 Jedis 需要连接池而 Redisson 不需要?**
- 要点:Jedis 同步阻塞、单连接非线程安全;Redisson 基于 Netty 事件循环,单连接多路复用且线程安全。
- 追问落点:连接池参数 `maxTotal`/`maxWaitMillis` 如何设置与调优?
3. **Redisson 的分布式锁为什么用 Lua 脚本?**
- 要点:加解锁是"判断 + 操作"两步,需原子;Lua 在 Redis 单线程内原子执行,防误删他人锁。
- 追问落点:为什么分开 `GET`+`DEL` 会误删别人的锁(锁过期被抢占)?
### 面试者参考回答
**【结论】** 两者都是 Redis 客户端但定位不同:Jedis 是同步阻塞的轻量客户端,Redisson 是基于 Netty 异步、功能全面的分布式协调框架;功能简单用 Jedis,要分布式锁等高级能力用 Redisson。
**【三点】** 第一,底层差异:Jedis 同步阻塞,单连接非线程安全,多线程必须用 `JedisPool`(`maxTotal`/`maxWaitMillis`),命令耗时会占住线程;Redisson 基于 Netty 事件循环,异步非阻塞,单连接可复用且线程安全。第二,能力差异:Redisson 把 Redis 封装成对象,内置分布式锁 `RLock`(watchdog 默认 30 秒每 TTL/3 续期、可重入、公平锁)、布隆过滤器、限流器、延迟队列,用 Lua 脚本保证原子性;Jedis 只提供基础命令,锁需手写 `SETNX`+Lua。第三,选型:只做缓存、要依赖最轻用 Jedis;需要分布式锁、布隆、限流等协调能力选 Redisson,两者也可共存。
**【边界】** 选型不是"谁强用谁",而是按需求:无高级协调需求时 Jedis 足够,强行用 Redisson 徒增依赖与启动开销;但涉及分布式互斥,优先 Redisson,而不是手写 Jedis 锁。若项目已用 Spring Data Redis(常默认 Lettuce),Redisson 独立接入,需评估双客户端带来的连接与运维成本。
> 最后更新:2026-10-28 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:锁必须放在事务外面——事务内加锁会持锁等待占住 DB 连接、放大锁粒度,甚至耗尽连接池;且解锁时机要在事务提交之后。
> 关键词:锁在事务外 · 连接池占用 · 锁粒度 · 提交先于解锁
> 边界:任何"锁内做远程调用/慢查询"都应避免;锁粒度按业务键,不要用全局锁。
### L2 · 核心讲解
**核心考点定位**:考察"锁与事务的边界"——锁要覆盖业务互斥段,但不能覆盖持锁等待与远程调用;面试官想排除"锁放事务内更安全"的错误认知,以及"提交后才解锁"的时序误区。
**机制链(≥3 环)**:
1. 锁放事务内且未持有锁 → 线程阻塞在获取锁:此时事务已开启,持有一个 DB 连接空转等待,锁竞争加剧时连接被大量占用。
2. 事务内持锁等待 → 连接池连接被占住:并发上来后 `maxTotal` 耗尽,其他请求在 `getConnection()` 排队,吞吐骤降甚至雪崩。
3. 锁内做远程调用/慢查询 → 锁粒度被放大:临界区变长,其他线程等待更久,锁超时(watchdog 也难持续续期)。
4. 解锁在事务提交前 → 其他线程读到旧数据:提交未完成锁已释放,B 线程进临界区读到 A 未提交数据,破坏一致性。
**证据锚点**(去重 ≥6):
- `@Transactional`:声明式事务;锁需手动包在事务外,`@Transactional` 不管理锁。
- `TransactionTemplate`:编程式事务;可精确控制"提交 → 解锁"顺序,推荐用于锁内写。
- `maxTotal` / `maxWaitMillis`:连接池参数;持锁等待占连接时池会耗尽,`getConnection()` 超时。
- `lockWatchdogTimeout=30000`:Redisson 默认锁租约 30 秒;锁内慢操作可能触发续期压力。
- `tryLock(2, 30, SECONDS)`:带等待时间与租约的加锁,避免无限阻塞占连接。
- `unlock` 放 `finally`:确保异常也能释放锁,防死锁。
- `SELECT FOR UPDATE`:DB 乐观/悲观锁替代方案;性能低但强一致。
**对比与权衡**:
| 方案 | 一致性 | 吞吐 | 失败模式 | 适用边界 |
|---|---|---|---|---|
| 锁在事务内 | 低 | 低 | 持锁等待占连接、锁粒度放大 | 仅单机无锁竞争 |
| 锁在事务外 + `@Transactional` | 中高 | 中 | 提交在解锁后,B 可能读旧 | 读已提交可接受 |
| `TransactionTemplate` 手动控制 | 高 | 中高 | 需手动提交/回滚 | 需严格"提交先于解锁" |
**常见误区**:
1. 错误:锁放事务内更安全,因为锁和事务一起结束。→ 正确:锁要在事务外,事务内持锁等待会占 DB 连接并放大锁粒度。→ 为什么:锁是互斥资源,事务是数据一致性单元,两者生命周期不同。
2. 错误:事务提交后锁自动释放。→ 正确:锁必须显式在 `finally` 中 `unlock`,且提交要在解锁前。→ 为什么:`@Transactional` 不管理 Redis 锁,锁与事务是两套机制。
3. 错误:锁粒度越大越安全。→ 正确:按业务键 `lock:order:{orderId}` 锁,避免全局锁。→ 为什么:大锁让无关请求互相排队,吞吐骤降,且单个热点 key 成瓶颈。
**适用边界**:若业务无需强互斥(如只读校验),可只用 DB 乐观锁;若锁内必须做远程调用,应把预校验放锁外、锁内只做关键写,或用 MQ 串行化替代;强一致且不能接受读旧,必须用 `TransactionTemplate` 控制"提交 → 解锁"顺序。
### 延伸追问(≥3)
1. **为什么解锁放在事务提交前会导致读到旧数据?**
- 要点:B 拿到锁时 A 事务未提交,B 读不到 A 的写;需"提交先于解锁"。
- 追问落点:如何用 `TransactionTemplate` 保证顺序(提交成功后再 `unlock`)?
2. **事务内持锁等待为什么危险?**
- 要点:线程等待时占着 DB 连接,池满后所有请求排队,出现连接耗尽雪崩。
- 追问落点:如何监控连接池耗尽(`getConnection()` 超时、连接数监控)?
3. **锁内做远程调用有什么问题?**
- 要点:临界区变长,其他线程等待久;远程超时还可能让锁过期,watchdog 续期也有压力。
- 追问落点:远程超时与 watchdog 续期的冲突如何解决(锁外预校验/异步化)?
### 面试者参考回答
**【结论】** 分布式锁必须放在事务外面——事务内加锁会持锁等待占住 DB 连接、放大锁粒度,严重时耗尽连接池;且锁释放与事务提交的时序必须"提交先于解锁"。
**【三点】** 第一,放事务内的问题:未持有锁时线程阻塞在加锁,但事务已开启、占着一个连接空等;并发上来连接池 `maxTotal` 被占满,其他请求 `getConnection()` 超时排队,吞吐骤降甚至雪崩。第二,放事务外且顺序正确:先 `tryLock`(带等待与租约,如 `tryLock(2, 30, SECONDS)`)→ 开启事务 → 业务写 → 提交 → 在 `finally` 中 `unlock`;推荐用 `TransactionTemplate` 手动控制提交,确保提交完成在解锁前,避免其他线程读到未提交的旧数据。第三,锁粒度与锁内操作:按业务键 `lock:order:{orderId}` 锁,锁内只做必要写,不做远程调用/慢查询,否则临界区变长、锁超时(watchdog 续期也扛不住远程阻塞)。
**【边界】** 如果业务可接受短暂读旧或仅做校验,可退化为 DB 乐观锁;若锁内必须远程调用,应预校验在锁外、锁内只写关键路径,或用 MQ 串行化替代。任何方案都要保证解锁在 `finally`,且提交先于解锁。
> 最后更新:2026-10-28 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:`i++` 非原子(读-改-写三步),多线程下保证正确本质是把"读-改-写"互斥或原子化——原子类/CAS、加锁、高并发用 `LongAdder`;`volatile` 只保可见性,单独用不够。
> 关键词:AtomicInteger · CAS 自旋 · LongAdder 分段 · volatile 边界
> 边界:只保单变量;高竞争换 `LongAdder`;ABA 用版本号;复合操作加锁。
### L2 · 核心讲解
**核心考点定位**:考察 `i++` 为何丢更新(读-改-写三条指令非原子),以及原子类/CAS、加锁、LongAdder 三种方案的边界差异;重点排除"volatile 能保证 i++ 原子"的误判,并追问高并发下的选型与 ABA 治理。
**机制链(≥3 环)**:
1. `i++` 编译后非单条指令 → 底层是"读值-加一-写回"三步:两个线程同时读到旧值 0,各自写回 1,i 只增加了一次,发生丢更新。
2. 丢更新 → 需要让"读-改-写"整体不可分割:用 `synchronized`/`ReentrantLock` 串行化临界区,或用 CAS 原子指令保证"比较-交换"一气呵成,每次 i 都精确 +1。
3. CAS 依赖 `cmpxchg` 原子指令 → 高竞争下失败重试(自旋)空转烧 CPU:改用 `LongAdder` 分段 `Cell` 数组分散热点,或退化为锁。
4. `volatile` 只保证可见性与有序性 → 没有互斥语义,修饰的 `i` 并发自增仍会丢更新:必须配合原子类或锁。
**证据锚点**:
- `i++`:非原子,读-改-写三条指令
- `AtomicInteger`:单变量原子自增
- `incrementAndGet()`:原子 +1,返回新值
- `getAndIncrement()`:原子 +1,返回旧值
- `LongAdder`:分段 `Cell` 累加,高并发更优
- `sum()`:`LongAdder` 弱一致汇总
- `AtomicStampedReference`:ABA 版本号解法
- `compareAndSwapInt`:CAS 底层 API
- `cmpxchg`:x86 原子比较-交换指令
- `synchronized`:互斥锁,串行化临界区
- `volatile`:只保证可见性与有序性
- `Cell`:`LongAdder` 内部分段单元
**对比与权衡**:
| 方案 | 正确性 | 吞吐/成本 | 失败模式 | 适用边界 |
|---|---|---|---|---|
| AtomicInteger / CAS | 单变量原子 | 低竞争无锁、快 | 高竞争自旋烧 CPU、ABA | 单变量低竞争 |
| LongAdder | 原子但 sum 弱一致 | 高并发吞吐最高 | sum() 非强一致 | 高频计数、读多写少 |
| synchronized / Lock | 互斥、强一致 | 阻塞、上下文切换成本 | 死锁、停顿 | 复合操作、高竞争 |
| volatile | 不保证原子 | 低、内存屏障 | 并发自增仍丢更新 | 仅可见性标志、发布 |
**常见误区**:
1. 错误:"volatile 能保证 i++ 原子" → 正确:`volatile` 只保证可见性与有序性,`i++` 是复合操作,并发下仍会丢更新 → 为什么:`volatile` 没有互斥语义,"读-改-写"三条指令仍可被打断。
2. 错误:"CAS 一定比锁快" → 正确:低竞争时快,高竞争时自旋空转反而更慢 → 为什么:CAS 是乐观重试,竞争激烈时重试次数暴涨,空转烧 CPU。
3. 错误:"AtomicInteger 在高并发计数下就够用" → 正确:高频计数应换 `LongAdder`,分段 `Cell` 分散热点 → 为什么:全局 CAS 都打在同一个 `value` 上,`LongAdder` 分散到多个 `Cell`,竞争大幅下降。
4. 错误:"ABA 问题业务上很少见" → 正确:链表节点复用、版本号回退等场景会出现,用 `AtomicStampedReference` 加版本号 → 为什么:值 A→B→A 后 CAS 会误判为"未被修改",隐蔽且危险。
**适用边界**:
- **适合**:单变量、低到中竞争的计数器/状态标志(`AtomicInteger`);高频读多写少的计数(`LongAdder`);需要严格一致性的单变量(`AtomicLong`)。
- **不适合**:高竞争下 CAS 自旋空转(换 `LongAdder` 或退锁);复合操作(用 `AtomicReference` 打包或加锁);需要跨变量不变式(必须加锁)。
- **替代方案**:ABA 敏感用 `AtomicStampedReference`;复合操作用 `ReentrantLock`;只做标记用 `volatile`。
### 延伸追问(≥3)
1. **为什么 i++ 在多线程下会丢更新?**
- 要点:`i++` 是"读值-加一-写回"三条指令,非原子;两个线程读到同一旧值各写回,只加了一次
- 追问落点:给出并发反例(两线程各 +1 结果只有 +1),说明丢更新是竞态而非"少了两次"
2. **LongAdder 为什么比 AtomicLong 快?**
- 要点:分段 `Cell` 数组分散热点,线程写不同 `Cell` 减少 CAS 竞争;`sum()` 是弱一致
- 追问落点:读多写少场景选型,及"为什么 sum 弱一致仍可用"
3. **CAS 的 ABA 怎么解决?**
- 要点:`AtomicStampedReference` 加版本号;`AtomicMarkableReference` 加布尔标记
- 追问落点:实际 ABA 场景(链表节点复用),以及为什么裸 CAS 判断不出
4. **volatile 到底能保证什么?**
- 要点:可见性(写后刷新主存、读前失效)与有序性(禁止重排),但不保证原子性
- 追问落点:`volatile` 写屏障与 DCL 单例、状态发布场景
### 面试者参考回答
**【结论】** `i++` 不是原子操作,多线程下保证正确的本质是把"读-改-写"整体互斥或原子化,三条主线:原子类/CAS、加锁、高并发用 `LongAdder`;`volatile` 只保可见性,单独修 `i++` 不够。
**【三点】**
第一,**原子类与 CAS**:`AtomicInteger.incrementAndGet()` 靠 `cmpxchg` 原子完成"比较-交换",单变量低竞争最快;但高竞争时自旋空转烧 CPU,且存在 ABA 问题(用 `AtomicStampedReference` 版本号解决)。
第二,**加锁**:`synchronized`/`ReentrantLock` 串行化临界区,正确性最强,但有阻塞与上下文切换成本,适合复合操作或高竞争场景。
第三,**LongAdder 与 volatile**:高频读多写少的计数用 `LongAdder`(分段 `Cell` 分散热点,`sum()` 弱一致);`volatile` 只保证可见性与有序性,不保证原子,不能单独用来修 `i++`。
**【边界】** 三点提醒:一是只保单变量原子性,复合操作要 `AtomicReference` 打包或加锁;二是高竞争时 CAS 自旋反而更慢,换 `LongAdder` 或退化为锁;三是 `volatile` 不能替代互斥,只能做可见性标志。
> 最后更新:2026-10-28 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:实现 MQ 要按"生产者 → Broker → 消费者"链路设计,四大必考是顺序性、幂等、不丢、不积压;再补可靠性分级、延迟队列与死信兜底。
> 关键词:分区内有序 · 消费幂等 · acks 确认 · 死信队列
> 边界:默认 At-Least-Once(可能重复,需消费幂等);顺序只在分区内;100% 不丢工程上不存在。
### L2 · 核心讲解
**核心考点定位**:考察对消息全链路(生产者/存储/消费者)的理解,以及顺序性、幂等、可靠性(不丢)、堆积治理四大工程问题;面试官要区分"只是列了要考虑什么"与"给出具体技术方案"。
**机制链(≥3 环)**:
1. 生产者发送 → 需要确认机制:`acks=all` + `retries` 重试 + 刷盘策略,决定消息是否真正落盘 → 网络分区或 Leader 挂时不会误报成功。
2. Broker 存储 → 副本复制(leader-follower + ISR)+ 持久化刷盘:决定单点故障是否丢数据 → `HW` 高水位决定消费者可见边界。
3. 消费者拉取 → `offset` 提交时机决定投递语义:自动提交可能在业务完成前记录,导致重复消费 → 需要消费端幂等(唯一索引/去重表)。
4. 堆积与失败 → 分区扩容 + 提高消费并行度应对积压;重试超限进死信队列:避免坏消息无限重试拖垮后续消息。
**证据锚点**:
- `acks=all`:等全部 ISR 副本确认
- `replication.factor=3`:3 副本
- `min.insync.replicas=2`:至少 2 副本确认
- `enable.idempotence=true`:开启幂等,崩溃不重复
- `unclean.leader.election.enable=false`:禁止落后副本当选,防丢数据
- `enable.auto.commit=false`:关闭自动提交
- `commitSync`:业务成功后手动提交
- `replica.lag.time.max.ms=10000`:ISR 踢出阈值(10s)
- `HW`:高水位,消费者可见边界
- `max.poll.records`:单次批量拉取上限
- `retries`:发送失败重试次数
- 死信队列(DLQ):重试超限隔离
**对比与权衡**:
| 关注点 | 关键参数/机制 | 解决什么 | 代价/边界 |
|---|---|---|---|
| 不丢 | acks=all + 副本 3 + 刷盘 | 单点故障不丢 | 吞吐下降、延迟上升 |
| 顺序性 | 分区内有序 + 同 key 同分区 | 同一 key 有序 | 只能分区内有序 |
| 幂等 | enable.idempotence + 消费去重 | 重复消费安全 | 额外存储(去重表) |
| 堆积 | 扩容分区 + 提高消费并行度 | 消费跟不上生产 | 成本上升 |
| 失败 | 重试 + 死信队列 | 坏消息隔离 | 延迟可见、需人工处理 |
**常见误区**:
1. 错误:"MQ 能保证消息不丢" → 正确:默认 At-Least-Once,可能重复,需消费幂等;100% 不丢工程上不存在 → 为什么:网络分区、进程崩溃、副本全挂都无法绝对规避。
2. 错误:"顺序性是全局有序" → 正确:只在分区内有序,跨分区无序 → 为什么:并行消费必然牺牲全局顺序,只能按 key 路由同分区。
3. 错误:"自动提交 offset 就安全" → 正确:自动提交在轮询时提交,业务可能未完成就记录,崩溃导致重复或丢消息 → 为什么:提交与业务处理不是同一事务。
4. 错误:"死信就是丢弃" → 正确:死信是隔离重试失败的消息,供人工/补偿处理 → 为什么:避免坏消息无限重试阻塞后续消息。
**适用边界**:
- **适合**:需要削峰、解耦、异步的场景;可容忍至少一次语义 + 业务幂等;顺序性只承诺分区内。
- **不适合**:要求 Exactly-Once 且无幂等表支撑;要求全局强顺序;单机量小、无并发需求(直接调用更简单)。
- **替代方案**:可靠性等级分级(acks=0/1/all);延迟队列(定时投递);死信兜底(重试超限)。
### 延伸追问(≥3)
1. **消息顺序怎么保证?**
- 要点:同一 key 路由到同一分区,分区内有序;全局有序需单分区牺牲并行
- 追问落点:全局有序与并行的权衡,以及 rebalance 后顺序是否受影响
2. **消息重复消费怎么处理?**
- 要点:消费端幂等(DB 唯一索引/去重表/Redis);默认 At-Least-Once
- 追问落点:At-Least-Once 与 Exactly-Once 的取舍
3. **消息堆积怎么解决?**
- 要点:扩容分区、增加消费者、提高消费并行度、限流保护下游
- 追问落点:消费 lag 监控与告警阈值
4. **什么消息会进死信队列?**
- 要点:重试超限、不可重试异常(反序列化失败、业务校验失败)
- 追问落点:DLQ 消费与人工补偿、延迟队列
### 面试者参考回答
**【结论】** 实现 MQ 核心是按"生产者 → Broker → 消费者"链路设计,并回答四大工程问题:顺序性、幂等、不丢、不积压;再补可靠性分级、延迟与死信兜底。
**【三点】**
第一,**可靠性(不丢)**:生产者 `acks=all` + `retries` 重试;Broker `replication.factor=3` + `min.insync.replicas=2` + 刷盘;消费端 `enable.auto.commit=false`,业务成功后 `commitSync` 手动提交。
第二,**顺序性与幂等**:同一 key 路由同一分区,保证分区内有序;消费端幂等(唯一索引/去重表),因为默认 At-Least-Once 可能重复消费。
第三,**堆积与失败**:分区扩容 + 提高消费并行度应对积压;重试超限进死信队列隔离坏消息,避免无限重试拖垮链路。
**【边界】** 三点提醒:一是顺序只在分区内,全局有序要牺牲并行;二是只能 At-Least-Once,Exactly-Once 需配合事务/幂等表;三是 100% 不丢工程上不存在,需按可靠性等级(acks=0/1/all)取舍吞吐。
> 最后更新:2026-10-28 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:先定边界(单次点赞/取消/归属/统计维度),再用 Redis ZSet(时间戳分数)做判重 + 时间排序,计数异步批量落库,热点内容用本地缓存 + Redis 挡读。
> 关键词:ZADD NX 判重 · ZREM 取消 · 定时落库 · 计数批量更新
> 边界:单用户单内容一次点赞;计数最终一致;需防刷限频;强一致场景不适用。
### L2 · 核心讲解
**核心考点定位**:考察点赞系统的高并发设计——先边界后设计、防重复点赞的原子性、取消与计数一致性、热点内容读优化、防刷幂等;面试官想看到"定边界→给方案"而非一上来就堆 Redis。
**机制链(≥3 环)**:
1. 点赞 → `ZADD key score member`(member=用户,score=时间戳)判重:检查返回值(0 表示已存在)或用 `NX`,保证原子防并发重复 → 同一用户不会重复点赞。
2. 已点赞再点 → 取消 `ZREM` + 计数减一:保证取消幂等,防重复取消把计数减到负数。
3. 计数不能每次点都写库 → 先缓存累加(`INCR`)+ 定时批量落库:高频写合并为低频批量,削峰保护 DB。
4. 读多写少的热点内容 → 计数与 ZSet 进本地缓存 + Redis:百万赞内容读不压垮下游,读放大被缓存挡住。
5. 批量落库失败 → 定时任务重试 + 幂等兜底:最终一致要能对账,防止缓存与 DB 计数漂移。
**证据锚点**:
- `ZADD key score member`:点赞写入
- `ZADD ... NX`:不存在才写,原子判重
- `ZREM`:取消点赞
- `ZSCORE`:查单用户是否已赞
- `ZCARD`:点赞总数
- `ZRANGE`:按分数(时间)排序
- `INCR`:计数累加
- `SETNX`:防刷限频
- 时间戳分数:按时间排序,先赞先排
- 定时落库:批量刷 DB,削峰
- 本地缓存:热点读挡流量
- `Lua` 脚本:原子判重 + 更新
**对比与权衡**:
| 方案 | 防重复 | 计数一致性 | 读性能 | 边界 |
|---|---|---|---|---|
| ZADD NX 判重 | 原子 | 最终一致 | 中 | 依赖 Redis 可用 |
| SADD 判重 + INCR | 原子 | 最终一致 | 中 | 集合较大时内存高 |
| DB 唯一索引 | 强一致 | 强一致 | 低 | 高并发扛不住 |
| 本地缓存 + Redis | 靠 Redis | 最终一致 | 高 | 热点读、需失效兜底 |
**常见误区**:
1. 错误:"每次点赞都写数据库" → 正确:缓存累加 + 定时批量落库 → 为什么:高频写会打爆 DB,必须合并写入削峰。
2. 错误:"ZADD 一定能防重复" → 正确:要 `ZADD NX` 或检查返回值(0 表示已存在),先查后加有竞态 → 为什么:先 `ZSCORE` 再 `ZADD` 非原子,并发下可能重复写入。
3. 错误:"取消点赞直接 ZREM 就行" → 正确:要 `ZREM` + 计数减一,且防重复取消 → 为什么:只删不减会导致计数虚高,重复取消会减到负数。
4. 错误:"热点内容直接查 Redis 就够" → 正确:百万赞热点要本地缓存 + Redis,防单点读放大 → 为什么:超大热点会打爆单个 Redis 实例,需多级缓存。
**适用边界**:
- **适合**:点赞/收藏/投票等"单用户单次 + 归属 + 计数"场景;可容忍计数最终一致;读多写少、有热点内容。
- **不适合**:要求强一致计数(如支付/库存)不适用;需要多用户复合(关注 + 点赞)需重新定边界。
- **替代方案**:防刷限频(`SETNX` + 时间窗);统计周期维度(按天 ZSet);缓存失效兜底(定时重建)。
### 延伸追问(≥3)
1. **怎么防重复点赞?**
- 要点:`ZADD NX`(member=用户)原子判重,返回 0 表示已存在;避免先 `ZSCORE` 再 `ZADD` 的竞态
- 追问落点:高并发下并发重复点击的原子性,`Lua` 脚本原子组合
2. **取消点赞的计数怎么保证?**
- 要点:`ZREM` + 计数减一,且防重复取消(检查返回值)
- 追问落点:取消与点赞并发时的计数一致性
3. **热点内容读怎么优化?**
- 要点:本地缓存 + Redis 多级缓存,读多写少;缓存失效要定时重建
- 追问落点:缓存击穿/穿透与热点 key 治理
4. **点赞数据怎么落库?**
- 要点:定时批量刷 DB(点赞明细 + 计数批量更新),最终一致
- 追问落点:批量落库失败的重试与幂等兜底
### 面试者参考回答
**【结论】** 点赞系统先定边界(单次点赞/取消/归属/统计维度),再用 Redis ZSet 做判重 + 时间排序,计数异步批量落库,热点读用本地缓存 + Redis。
**【三点】**
第一,**防重复**:`ZADD NX`(member=用户)原子判重,返回 0 表示已存在;不能先 `ZSCORE` 再 `ZADD`,并发下会竞态重复。
第二,**取消与计数**:`ZREM` 取消 + 计数减一,防重复取消;计数先缓存累加(`INCR`),定时批量刷库,避免每次点赞都写 DB。
第三,**热点与防刷**:百万赞内容用本地缓存 + Redis 挡读,防单点读放大;同一用户限频(`SETNX` 时间窗)防刷。
**【边界】** 三点提醒:一是计数是最终一致(批量落库),需强一致场景不适用;二是依赖 Redis 可用,缓存故障要降级;三是只覆盖"单用户单次 + 归属"边界,多用户复合(如关注 + 点赞)要重新定边界。
> 最后更新:2026-10-28 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:RT 飙高先切维再归因——按单接口/单机/全量三个维度切分故障范围,再用链路追踪(单接口)、GC+线程池(单机)、中间件+依赖(全量)三套独立证据链归因;关键判据是「Load 高 CPU 低 = IO/锁等待,不是计算瓶颈」。
> 关键词:先切维 · 单接口/单机/全量 · 链路追踪 · 线程 Dump · CPU 分解 · FullGC 基线
> 边界:适用于线上接口 RT 整体或部分飙高的被动排障;发布后首批高且自愈属冷启动,不在此排查链;伴随错误率升高需先看依赖熔断与降级状态。
### L2 · 核心讲解
**核心考点定位**:RT 飙高排查的核心方法论是「先切维再归因」。用实例、接口、时间三个维度把故障范围切成两半,差异维度直接指向根因;再对每个维度用独立证据链归因。难点在于区分「CPU 计算瓶颈」与「IO/锁等待」——二者对策完全相反,是排障题里最常见的方向性错误。
**机制链(≥3 环)**:
1. 某接口 RT 飙高 → 链路追踪定位 span:慢 SQL 或三方调用,因为单接口异常只影响自身调用链,`SkyWalking` 可直观看到耗时分布
2. 单机所有接口 RT 飙高 → GC 异常或线程池/连接池耗尽:因为资源被单机内部争抢,Load 高 CPU 高看 GC 日志,CPU 低看锁等待
3. 全量接口 RT 飙高 → 中间件或下游依赖故障:因为全局共享 Redis/DB/MQ 被拖垮,叠加超时重试放大流量翻倍
4. 现象反复但指标正常 → 按时间维度对比基线与 P99:因为平均值被快请求稀释,只有长尾才反映真实恶化
**证据锚点**:
- `SkyWalking`:链路追踪定位慢 span,区分自身/Redis/MySQL/RPC 调用
- `jstack`:看线程状态分布——`RUNNABLE`/`BLOCKED`/`WAITING` 指向计算/锁/等 IO
- `jstat -gcutil`:看 `FGC` 次数与 `FGCT` 累计耗时,FullGC 基线趋势判泄漏
- `pidstat -w`:看 `cswch/s`(自愿切换=锁/IO)与 `nvcswch/s`(非自愿=时间片耗尽)
- `%usr`:用户态计算热点(死循环、序列化、正则回溯、CAS 自旋)
- `%sys`:内核态调用(频繁 syscall、锁竞争、上下文切换)
- `%wa`:等 IO(磁盘慢、网络等待)
- `top -Hp`:找高 CPU 线程 TID,转十六进制后 jstack 匹配栈帧
- `async-profiler`:采样式定位 CPU 热点,替代 jstack 快照
- `HikariCP`:`connectionTimeout` 反映连接池排队耗尽
- `ThreadPoolExecutor`:`CallerRunsPolicy` 背压,队列满时拖回调用方
- 12.5%:8 核机器上单线程忙等 `top` 仅显示约 12.5%(整机口径)
- 100ms:P99/P999 长尾判据阈值,与基线对比防「流量掉了所以看着好了」
**对比与权衡**:
| 方案 | 正确性 | 吞吐/成本 | 失败模式 | 适用边界 |
|---|---|---|---|---|
| 先切维再归因 | 高(维度差异直接指根因) | 低(先看几个指标即可) | 切错维度会漏根因 | 所有 RT 异常通用 |
| 直接猜答案(先查慢 SQL) | 低(方向性错误常见) | 中(可能白查一轮) | CPU 高≠SQL 慢,锁等待被误判 | 仅现象单一明确时 |
| 一把抓线程 Dump | 中(能看阻塞与死锁) | 高(多实例采样成本大) | 快照无法证明热点 | 必须配合 CPU 分解 |
| 只盯平均值 | 低(掩盖长尾) | 低 | 恶化被快请求稀释 | 不作为判据 |
**常见误区**:
1. 错误:RT 高就查慢 SQL → 正确:先切维(单接口/单机/全量)→ 为什么:全量 RT 高可能是 Redis 故障或依赖超时,单机 RT 高可能是 GC/线程池
2. 错误:Load 高就是 CPU 计算瓶颈 → 正确:Load 高 CPU 低是 IO/锁等待 → 为什么:等待线程休眠不消耗 CPU
3. 错误:jstack 单次快照就能定位 CPU 热点 → 正确:快照适合看阻塞与死锁,热点用 async-profiler/JFR 采样 → 为什么:热点方法平均只占几个采样点
4. 错误:CPU 高就是死循环 → 正确:还要看 GC 线程、JIT 编译、CAS 自旋、正则回溯 → 为什么:`%usr` 高有多种成因,需先分解再定位
5. 错误:平均 RT 正常就没事 → 正确:要看 P99/P999 → 为什么:平均值被大量快请求稀释,长尾才是雪崩风险来源
**适用边界**:本讲解适用于「线上接口 RT 整体或部分飙高」的被动排障;若为发布后首批高且 10-30 分钟自然回落,属冷启动(JIT/缓存/连接池/流量分配),不在此排查链;若伴随错误率升高或丢单,需先看依赖可用性、熔断降级与超时重试配置,而非直接进 GC/线程排查。
### 延伸追问(≥3)
1. **单机所有接口 RT 高,jstack 看到大量 BLOCKED,下一步怎么定位?**
- 要点:抓 BLOCKED 集中在哪个 monitor(`grep "waiting to lock" | sort | uniq -c`),配 `pidstat -w` 看 `cswch/s`;JFR `jdk.JavaMonitorBlocked` 定位具体锁与代码行。
- 追问落点:锁竞争分层优化——缩小临界区→锁分段→读写锁/StampedLock→LongAdder/CAS 无锁。
2. **Load 高但 CPU 只有 30%,是什么原因?怎么区分?**
- 要点:Load 高 CPU 低 = 线程在等 IO 或等锁(`%wa` 高=磁盘/网络等待,`cswch/s` 高=锁/IO 自愿切换),不是计算瓶颈;对策是查磁盘/网络/锁,而不是加机器。
- 追问落点:`%usr`/`%sys`/`%wa` 分解,`pidstat -w` 区分自愿/非自愿切换。
3. **Full GC 频繁,怎么判断是泄漏还是分配太快?**
- 要点:`jstat -gcutil` 多次采样,看每次 FGC 后老年代基线是否逐次抬高;抬高=内存泄漏(找 GC Roots 持有者),回到同基线=分配速率过高/堆太小。
- 追问落点:GC cause 分类——Allocation Failure / System.gc / Metadata GC Threshold / Humongous。
4. **排查中发现某实例依赖超时设 3s 且重试 3 次,有什么风险?**
- 要点:超时重试放大——下游抖动→上游重试→流量翻倍→更抖,线程池被占满后 RT 雪崩;应设退避、熔断(Sentinel)与线程池 `CallerRunsPolicy` 背压。
- 追问落点:超时/重试/熔断三件套的配合,依赖超时与线程池容量的关系。
### 面试者参考回答
**【结论】** RT 飙高排查的关键是先切维再归因:按单接口/单机/全量三个维度切分,再对每个维度用独立证据链归因;关键判据是 Load 高 CPU 低 = IO/锁等待而非计算瓶颈。
**【三点】** 第一,单接口 RT 高,用链路追踪(`SkyWalking`)看 span,定位慢 SQL 或三方调用;第二,单机所有接口 RT 高,先看监控是否 GC 异常(`jstat -gcutil` 看 `FGC` 次数与 `FGCT`),再抓 `jstack` 看线程状态(`BLOCKED`=锁竞争、`RUNNABLE`=计算、`WAITING`=等 IO),辅以 `pidstat -w` 看上下文切换;第三,全量接口 RT 高,查中间件(Redis 慢查询、DB 慢 SQL、MQ 堆积)与依赖超时,警惕超时重试放大。补充判据:Load 高 CPU 低=IO/锁等待;CPU 高先分 `%usr`/`%sys`/`%wa`。
**【边界】** 适用于被动排障;发布首批高且自愈属冷启动;伴随错误率升高需先看依赖熔断与降级,而非直接进 GC/线程排查。
> 最后更新:2026-10-28 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:下单支付链路的设计核心是「分段建模 + 状态机 + 幂等 + 三重兜底」——把链路切成下单/支付/回调/后置四段,每段定义状态机与幂等键,用分布式事务保订单与支付单一致,用验签、主动查询、对账三重兜底对抗渠道不可靠。
> 关键词:分段建模 · 状态机 · 幂等 · 库存冻结 · 验签 · 主动查询 · 对账
> 边界:适用于「同步下单 + 异步支付回调」的电商/在线支付场景;预授权/跨境/分账等资金场景需升级 TCC 并加审计与对账;虚拟商品(无库存)可裁剪超卖与冻结段。
### L2 · 核心讲解
**核心考点定位**:本题考的是资金链路的系统性设计。核心是「分段建模 + 状态机 + 幂等 + 三重兜底」:把链路切成下单(防重复/防超卖)、支付(拉起/状态一致)、回调(验签/幂等/状态机)、后置(积分/消息)四段;用状态机约束订单流转;用幂等键对抗重复点击与重复回调;用验签、主动查询、对账三重兜底对抗渠道异步不可靠。难点是识别「用户已支付但我方订单失败」这类跨系统不一致。
**机制链(≥3 环)**:
1. 用户重复点击下单 → 前端防重 + 后端幂等:同一请求被放大 N 倍,靠请求指纹 + 时间窗口收敛为一次提交
2. 并发下单超卖 → 缓存层预扣 + DB 二次校验 + 库存冻结:先在入口 `Redis` 挡流量,再在生成订单时用 `Lua` 原子扣减兜底
3. 支付成功但回调丢失 → 主动状态查询:渠道异步消息不可靠,靠轮询/主动查补上支付状态
4. 支付渠道重复回调 → 回调幂等:以 `支付单号` 为唯一键,重复回调直接返回成功
5. 回调信息被篡改 → 验签:`RSA` 验签 + 平台证书,只信支付单号与状态,其余信息从原始订单查询
**证据锚点**:
- `Redis` 预扣库存:入口层先挡流量,`Lua` 脚本原子扣减防超卖
- `Lua`:单次编译执行,期间不接受其他命令,替代 MULTI/EXEC 无回滚事务
- `乐观锁`:`update stock = stock - 1 where stock >= 1`,DB 侧兜底防超卖
- `TCC`:Try 冻结 / Confirm 提交 / Cancel 补偿,主链路强一致
- `本地消息表`:本地事务写业务 + 消息表,定时投递,保证后置操作最终一致
- `MQ` 至少一次:后置关联操作(积分/消息)语义,消费端必须幂等
- `RSA` 验签:回调数据防篡改,配平台证书校验
- `支付单号`:回调幂等唯一键,重复回调直接返回成功
- `状态机`:待支付→支付中→已支付→已发货,约束流转,拦截乱序/重复回调
- 30min:订单超时未支付定时关单阈值,关单前先主动查支付状态防漏
- `@Idempotent`:下单防重(请求指纹 + 时间窗口)
- `分布式事务`:订单与支付单一致性方案,Seata AT / TCC / 事务消息
- `Seata AT`:undo_log 自动回滚,对业务透明但需全局锁,适合侵入小的场景
- `请求指纹`:下单幂等键(用户 + 商品 + 时间窗口),重复点击收敛为一次
- `差错账`:对账发现不一致自动生成,人工退款/补单处理
**对比与权衡**:
| 方案 | 正确性 | 吞吐/成本 | 失败模式 | 适用边界 |
|---|---|---|---|---|
| 缓存层预扣(Redis+Lua) | 中(最终需 DB 落库) | 高(扛住峰值) | 缓存与 DB 不一致需对账 | 高并发/热销/秒杀 |
| DB 乐观锁(版本号/CAS) | 高(强一致) | 低(冲突重试) | 热点行争抢退化 | 低并发/兜底校验 |
| 只靠前端防重 | 低 | 高 | 后端仍可重复下单 | 不作为主方案 |
| 本地消息表 | 高(最终一致) | 中(表+定时扫) | 消息重复需消费幂等 | 后置操作(积分/通知) |
**常见误区**:
1. 错误:回调信息全部可信 → 正确:只信支付单号与状态,金额/商品等从原始订单查询 → 为什么:回调内容可能被篡改,验签只保证来源
2. 错误:分布式事务能 100% 一致 → 正确:TCC 也非 100% 可靠,需对账兜底 → 为什么:协调者故障/网络分区仍可能不一致
3. 错误:库存只做一次校验 → 正确:入口缓存校验 + 生成订单 DB 二次校验 + 冻结 → 为什么:缓存可能过期或不一致,需 DB 兜底
4. 错误:回调到达就更新订单状态 → 正确:按状态机流转,乱序/重复回调过滤 → 为什么:直接覆盖会错误回退已支付状态
5. 错误:订单超时直接取消 → 正确:超时关单前先主动查支付状态 → 为什么:可能出现「用户已支付但订单被取消」的资损
**适用边界**:适用于「同步下单 + 异步支付回调」的电商/在线支付场景。若为预授权、跨境、分账等资金场景,主链路需升级为 TCC 并加审计(trace_id/流水表)与日终对账;若为虚拟商品(无库存),超卖与库存冻结段可裁剪;若支付渠道支持同步返回,主动查询兜底可弱化为重试;若为纯内部门票/无资损场景,验签与对账强度可下调,但幂等与状态机不可省。
### 延伸追问(≥3)
1. **库存超卖的具体落库实现怎么做?**
- 要点:入口 `Redis` 预扣(`DECR` + `Lua` 原子,防负数,超时释放)+ 生成订单时 DB 二次校验(乐观锁 `where stock >= 1` 或悲观锁)+ 冻结;最终一致靠对账/延迟双删。
- 追问落点:缓存与 DB 最终一致性方案(Canal+MQ/延迟双删/TTL 兜底),热 key 打散。
2. **回调 RSA 验签的具体流程?**
- 要点:用平台证书验签(验签 + 验证书链)防中间人/篡改;验签通过后只取支付单号与状态,金额/商品回查原始订单;验签失败记录并告警。
- 追问落点:证书轮换与多证书兼容,验签失败的人工处置。
3. **订单状态机怎么设计?**
- 要点:定义状态与合法流转(待支付→已支付→已发货→已完成,超时→已关闭);超时未支付定时关单,关单前先主动查支付状态防「已支付但被取消」;并发更新用乐观锁/CAS。
- 追问落点:状态流转表、重复/乱序回调按状态机过滤、关单与支付回调查询的竞态。
4. **对账怎么做?**
- 要点:日终拉取渠道账单与本地支付单对比,不一致生成差错账;大额交易实时对账;差错账人工处理(退款/补单)。
- 追问落点:对账系统架构(独立服务+定时+告警),差错账的处理流程。
### 面试者参考回答
**【结论】** 下单支付链路设计的关键是分段建模 + 状态机 + 幂等 + 三重兜底(验签/主动查询/对账)。
**【三点】** 第一,下单段:前端防重 + 后端幂等(请求指纹 + 时间窗口),库存用 `Redis` 预扣(`Lua` 原子)+ 生成订单时 DB 二次校验 + 冻结防超卖;第二,支付段:拉起支付失败重试/降级,订单与支付单一致性用分布式事务(TCC/本地消息表),回调必须验签(`RSA` + 平台证书,只信支付单号与状态)、按状态机流转、以支付单号为幂等键;第三,兜底:渠道异步不可靠靠主动状态查询 + 定时关单前先查支付,后置关联操作(积分/消息)走 `MQ` 至少一次 + 消费端幂等,日终对账发现差错账。
**【边界】** 适用于同步下单 + 异步回调的电商支付;预授权/跨境/分账等资金场景升级 TCC + 审计 + 对账;虚拟商品(无库存)可裁库存段。
> 最后更新:2026-10-28 · 基于第 2 次作答
### L1 · 30 秒速记
> 结论:万人在线房间的一致性 = 状态集中存 Redis + 长连接推送(WebSocket/SSE)+ 跨服务器广播(Redis Pub/Sub)+ 并发原语防冲突(SETNX/Lua)+ 心跳保活。
> 关键词:状态集中 · 跨服务器广播 · SETNX+Lua 原子占位 · 心跳保活 · 强/最终一致选型
> 边界:麦位状态要强一致(同步广播),聊天消息才允许最终一致(MQ 异步);禁止客户端轮询。
### L2 · 核心讲解
**核心考点定位**:考察"分布式实时系统"中状态一致性的完整设计——状态存哪、怎么广播、并发怎么防、一致性怎么选;面试官想看到"长连接 + 状态集中 + 跨服务器广播 + 并发原语"的完整链路,而不是只背 Redis 或 WebSocket 单个组件。本题高难度,重点是跨服务器的广播机制与麦位并发,这两处是大多数考生缺失的。
**机制链(≥3 环)**:
1. 用户长连接挂在不同 WS 网关 → 状态变更发生在服务器 B,网关 A 上的用户看不到:本地内存或单机推送无法跨服务器同步,必须引入广播总线(Redis Pub/Sub)把变更推给所有网关。
2. 状态集中到 Redis → 网关做成无状态:在线人数用 `INCR/DECR` 原子计数、麦位用 `SET` 集合、排队用 `LIST`,任意网关都能直接读,横向扩容不再受会话内存限制。
3. 多用户同时申请同一麦位 → 必须原子占位:`SETNX`/Lua 脚本保证"检查空位 + 占位"一步完成,否则"先查后写"两步之间会产生竞态,出现两人抢到同一麦位。
4. 用户掉线或断网 → 需要心跳保活:客户端 30s 心跳、服务器 60s 超时清理,配合 `EXPIRE` 自动释放麦位并回退在线计数,避免死连接长期占用麦位。
5. 广播路径选型 → 麦位走同步广播(强一致),聊天消息走 MQ(最终一致):麦位被用户实时感知,要用 `Redis Pub/Sub`(延迟 < 10ms);聊天消息允许秒级收敛,用 `Kafka` 持久化更可靠。
**证据锚点**:
- `room:{roomId}:online`:在线人数计数器 key,用 `INCR/DECR` 原子增减,避免并发错乱。
- `room:{roomId}:mics`:麦位集合 key,用 `SET`/`SADD` 存当前上麦用户,麦位数量有限(4–10 个)。
- `room:{roomId}:mics:queue`:麦位排队队列 key,用 `LIST` 存申请失败的用户,空位时按序分配。
- `SETNX`:原子占位,key 不存在才写入;麦位申请用它保证"一个麦位一个用户"。
- Lua 脚本:把"检查麦位空 + 占位 + 通知广播"合并为一步原子执行,避免竞态。
- `EXPIRE`:给在线会话设过期时间,服务器 60s 超时清理死连接,自动释放麦位。
- `BLPOP`:阻塞等待空位,适合麦位排队,空位出现时唤醒队首用户。
- `Netty`:WebSocket 网关的常用实现,负责维护海量长连接与心跳。
- `Redis Pub/Sub`:跨服务器广播机制,延迟 < 10ms;无持久化,适合状态类广播。
- `Kafka`:可靠、有持久化,适合聊天消息,延迟 < 100ms,可回放。
- `WebSocket`:全双工长连接,生产环境首选;`SSE`(Server-Sent Events)单向推送、HTTP 兼容。
- 客户端 30s 心跳:探测连接存活;服务器 60s 超时:判定死连接并清理。
- 万 QPS:万人客户端若用轮询,QPS ≈ 在线人数 × 轮询频率,直接打爆业务服务器。
- 强一致延迟 < 100ms:写 Redis + 同步广播,麦位用户可即时感知;最终一致秒级收敛:MQ 异步。
**对比与权衡**:
| 方案 | 正确性 | 吞吐/成本 | 失败模式 | 适用边界 |
|---|---|---|---|---|
| 状态存各网关本地内存 | 跨网关不一致 | 高但无法横向扩展 | 网关重启丢状态、无法路由 | 单机小房间,不适用万人 |
| 状态存 Redis + Pub/Sub 广播 | 强一致 | 高,Redis 扛万级更新 | Pub/Sub 丢消息、网关断连 | 麦位/在线人数等状态,本题主方案 |
| 状态存 Redis + MQ 异步广播 | 最终一致 | 高,削峰 | 秒级延迟、消息重复需幂等 | 聊天消息/弹幕,可接受短暂不一致 |
| 客户端轮询拉状态 | 取决于拉取频率 | 极低,QPS 打爆服务器 | 连接风暴、服务器崩溃 | 禁止使用,万人场景不可行 |
**常见误区**:
1. 错误:用客户端轮询同步房间状态。→ 正确:用长连接 + 服务端推送(WebSocket/SSE)。→ 为什么:万人轮询 QPS = 在线人数 × 频率,直接打爆业务服务器,这是本题最关键的工程判断。
2. 错误:房间状态放各网关本地内存。→ 正确:状态集中存 Redis,网关无状态化。→ 为什么:用户连接分散在多个网关,本地内存无法跨网关一致,且网关无法横向扩展。
3. 错误:麦位申请用"先查后写"两步。→ 正确:`SETNX` 或 Lua 脚本原子占位。→ 为什么:两步之间有多用户竞态,会两人同抢一麦位;Lua 保证"检查 + 占位 + 广播"原子性。
4. 错误:所有状态一律走 MQ 异步最终一致。→ 正确:麦位走同步广播强一致,聊天消息才走 MQ。→ 为什么:MQ 有秒级延迟,麦位用户能感知到不一致;按"用户可感知的实时"分层选型。
**适用边界**:麦位、在线人数这类用户能直接感知的状态要强一致——写 Redis 后同步广播(延迟 < 100ms);聊天消息、弹幕这类允许秒级收敛的走 MQ 最终一致(需幂等 + 去重)。如果房间状态要求绝对不丢且需审计,给 Redis 加 AOF 持久化,或改存 MySQL 并配合本地消息表兜底。若做到百万级并发(YY 语音、KOOK),自建成本过高,应直接引入 IM 云(融云/环信/腾讯云 IM)承担消息,麦位状态仍自建。
### 延伸追问(≥3)
1. **万人在线的 WebSocket 连接怎么横向扩展?**
- 要点:WS 网关无状态化(会话状态放 Redis),多副本部署;用 Redis Pub/Sub 广播状态变更;客户端经 LB 做 WebSocket 路由,网关崩溃时连接迁移靠重连 + 状态重建。
- 追问落点:网关重启后用户连接如何恢复?如何做优雅停机与连接迁移?
2. **麦位申请高并发怎么保证原子性?**
- 要点:`SETNX mic:{micId} userId` 原子占位,失败进 `LIST` 排队;Lua 脚本合并"检查 + 占位 + 广播";空位出现时 `BLPOP` 唤醒队首。占位要设 TTL 防持麦者异常。
- 追问落点:持麦者进程崩溃但麦位未释放怎么办?TTL 设多少合适?
3. **强一致和最终一致怎么选型?**
- 要点:麦位/在线人数用强一致(写 Redis + 同步广播,< 100ms);聊天消息用最终一致(MQ 异步,秒级收敛);核心是"用户能感知的实时"其实是准实时,不必追求毫秒级强一致。
- 追问落点:Pub/Sub 丢消息怎么兜底?最终一致需要幂等键 + 对账吗?
4. **心跳保活如何避免死连接占麦?**
- 要点:客户端 30s 心跳、服务器 60s 超时清理,配合 `EXPIRE` 自动过期释放麦位并回退在线计数;超时判定要容忍网络抖动。
- 追问落点:心跳超时误判怎么处理?如何区分断网与静默用户?
### 面试者参考回答
**【结论】** 万人在线的核心是"状态集中存 Redis + 长连接推送 + 跨服务器广播 + 并发原语 + 心跳保活":麦位管理用 `SETNX + Lua` 保证原子性,在线人数用心跳保活 + 超时清理;跨网关状态变更用 Redis Pub/Sub 广播;禁止客户端轮询。
**【三点】** 第一,架构分层:WebSocket 网关(Netty)负责用户长连接,无状态化(状态放 Redis)便于横向扩展;房间状态服务管业务逻辑,状态全部存 Redis(在线人数 `INCR/DECR`、麦位 `SET`、排队 `LIST`);Redis Pub/Sub 负责跨服务器广播——用户连在网关 A、状态变更发生在服务器 B,B 发消息到 Pub/Sub,A 订阅后推送给本地连接。第二,麦位管理有并发陷阱:多用户同时申请同一麦位,用 `SETNX mic:{micId} userId` 原子占位,失败进排队队列(`LIST`),空位时 `BLPOP` 阻塞等待;Lua 脚本保证"检查空位 + 占位 + 广播"的原子性;用户下线/心跳超时触发释放,释放后通知排队用户。第三,在线人数用心跳保活:客户端 30s 心跳、服务器 60s 超时清理(`EXPIRE` 自动过期,避免死连接);新用户进房直接读 Redis 拿当前状态,之后通过 WebSocket 订阅状态变更秒级感知。不能用客户端轮询——万人 QPS 会打爆服务器。选型上麦位强一致(同步广播,< 100ms)、聊天消息最终一致(MQ,秒级收敛)。
**【边界】** 如果房间状态要求绝对不丢且需审计,给 Redis 加 AOF 持久化或改存 MySQL + 本地消息表兜底;如果做到百万级并发(YY 语音、KOOK),自建成本过高,应直接用 IM 云(融云/环信/腾讯云 IM)承担消息,麦位状态仍自建——核心是"麦位状态自建,聊天消息用云"。
> 最后更新:2026-10-28 · 基于第 1 次作答
### L1 · 30 秒速记
> 结论:从 0 搭建先做单体(Monolith First),业务跑通、出现独立扩缩容与团队并行需求后再按限界上下文拆微服务;选型用 Spring Cloud Alibaba(Nacos + Gateway + Sentinel + SkyWalking),通信同步 gRPC/OpenFeign、异步 MQ,跨服务数据走本地消息表最终一致。
> 关键词:先单后微 · 限界上下文 · Conway 定律 · Nacos/Gateway/Sentinel/SkyWalking · 最终一致
> 边界:团队小于 8 人或业务未跑通时不要一上来拆微服务;拆分必须匹配团队边界,避免过度服务化。
### L2 · 核心讲解
**核心考点定位**:考察"从 0 搭建"的系统设计能力——拆分依据(限界上下文 / Conway 定律 / 独立部署)、技术选型理由、服务间通信与数据一致性、可观测性、演进策略。面试官想看到"先单后微 + 有依据的拆分 + 组件选型给理由",而不是罗列一串服务名。本题高难度,多数考生只讲"拆哪几个",不讲"为什么拆、为什么选这个组件"。
**机制链(≥3 环)**:
1. 先明确业务域边界 → 按限界上下文拆分:用户/房间/消息/支付/活动/推荐各成一个领域、一份数据,避免共享库,实现高内聚低耦合。
2. 团队与架构相互塑造 → 康威定律(Conway's Law):系统架构最终反映组织沟通结构,拆分必须匹配团队边界,一个团队负责一个服务,避免跨团队频繁调用。
3. 服务拆分后要通信 → 同步 vs 异步选型:强一致 + 实时返回用 gRPC/OpenFeign,解耦 + 削峰 + 最终一致用 MQ(RocketMQ/Kafka)。
4. 跨服务数据一致性 → 本地消息表 + MQ:业务表和消息表同库事务,消息可靠投递,下游幂等消费,避免分布式事务过度复杂。
5. 拆分时机与粒度 → 先单后微:团队小或业务未跑通先做单体,出现扩展瓶颈再按领域拆,避免一上来过度微服务。
**证据锚点**:
- `Spring Cloud Alibaba`:国产微服务技术栈全家桶,注册/配置/熔断/网关一站集成,符合国内工程实践。
- `Nacos`:注册中心 + 配置中心二合一;客户端 5s 心跳、15s 判不健康、30s 自动剔除;至少 3 节点集群,主端口 `8848`。
- `Gateway`:统一入口网关,做路由、鉴权、限流、灰度,避免各服务重复做鉴权。
- `Sentinel`:熔断/限流/降级组件,保护下游,防止服务雪崩。
- `SkyWalking`:链路追踪,`traceId` 透传跨服务定位问题。
- `OpenFeign`:声明式 HTTP 客户端 + 负载均衡,同步调用。
- `gRPC`:HTTP/2 + Protobuf,多路复用、强类型,内部高性能通信。
- `Dubbo`:Netty + 自定义协议,Java 生态 RPC,性能优于 HTTP/JSON。
- `RocketMQ`:事务消息 + 可靠投递,适合最终一致;`Kafka`:大吞吐日志流。
- 本地消息表:业务表和消息表同库事务,异步投递保证不丢,配合下游幂等消费。
- `Conway` 定律:系统架构最终反映组织沟通结构,拆分匹配团队边界。
- DDD 限界上下文:按业务领域拆分,而非按 web/service/dao 技术层。
- `Monolith First, Microservices Second`:团队小于 8 人、业务未跑通先做单体。
- 治理四件套:超时 + 重试 + 熔断 + 幂等,同步调用必备,否则下游慢拖垮上游。
**对比与权衡**:
| 方案 | 正确性 | 吞吐/成本 | 失败模式 | 适用边界 |
|---|---|---|---|---|
| 单体重服务化(先单后微) | 强一致易保证 | 初期成本最低 | 后期扩展受限,需再拆 | 团队小、业务未跑通 |
| 按限界上下文拆微服务 | 跨服务需一致性方案 | 高,独立扩缩容 | 分布式事务、排障难 | 业务跑通、团队并行 |
| 同步调用(gRPC/OpenFeign) | 强一致实时 | 中,占用线程 | 下游慢拖垮上游 | 查询/支付确认等实时 |
| 异步 MQ(最终一致) | 最终一致 | 高,削峰 | 消息重复需幂等 | 解耦、削峰、通知 |
| 全量拆微服务 | 一致性复杂 | 运维成本极高 | 过度设计、爆炸半径大 | 不推荐,团队小禁用 |
**常见误区**:
1. 错误:从 0 搭建就直接拆十几个微服务。→ 正确:先单后微(Monolith First)。→ 为什么:团队小、业务未跑通时,分布式事务、排障、部署成本远大于收益,应先跑通再拆。
2. 错误:按技术层(web/service/dao)拆服务。→ 正确:按业务限界上下文(DDD)拆。→ 为什么:技术层拆分会让一个业务变更横跨多个服务,违反高内聚;领域拆分才对应独立部署与团队并行。
3. 错误:只讲拆哪几个服务,不讲选型理由。→ 正确:选型看团队能力、业务场景、扩展性,并给出理由。→ 为什么:服务拆分必须匹配团队能力与组织结构(Conway 定律),避免技术炫技。
4. 错误:跨服务强一致一律用分布式事务。→ 正确:优先最终一致(本地消息表 + MQ)+ 幂等。→ 为什么:强一致分布式事务性能差、复杂度高,多数业务场景可接受秒级最终一致。
**适用边界**:如果团队小于 8 人、业务还没跑通、迭代频繁未稳定,坚持先做单体,不要一上来拆微服务;当出现"某个领域需要独立扩缩容、多个团队并行开发、故障隔离"的真实需求时,才按限界上下文拆。选型要匹配团队能力(全是 Java 团队就别强行上 Go),强一致实时用 gRPC/Feign、解耦最终一致用 MQ,跨服务数据用本地消息表 + 幂等消费,可观测性用 SkyWalking + 统一日志 + Prometheus/Grafana。
### 延伸追问(≥3)
1. **服务拆分依据是什么?怎么判断该不该拆?**
- 要点:按 DDD 限界上下文 + 独立部署需求 + 团队边界(Conway 定律);判断标准是"能否独立扩缩容、独立发布、故障隔离",而不是服务越多越好。
- 追问落点:什么时候不该拆?拆错了怎么合回来?
2. **服务间通信同步 vs 异步怎么选?**
- 要点:强一致 + 实时返回用 gRPC/OpenFeign(必配超时/重试/熔断/幂等);解耦 + 削峰 + 最终一致用 MQ(RocketMQ/Kafka)。
- 追问落点:同步调用如何防雪崩?熔断阈值怎么设?
3. **跨服务数据一致性怎么保证?**
- 要点:优先最终一致——本地消息表(业务表 + 消息表同库事务)+ MQ 可靠投递 + 下游幂等消费;必须强一致的场景才引入分布式事务。
- 追问落点:消息表投递失败怎么补偿?消费重复怎么去重?
4. **微服务可观测性怎么搭?**
- 要点:SkyWalking/OpenTelemetry 做链路追踪(traceId 透传),统一日志采集,Prometheus + Grafana 监控指标;一个请求跨多服务时能快速定位。
- 追问落点:traceId 怎么跨服务透传?慢链路怎么分析?
### 面试者参考回答
**【结论】** 从 0 搭建不直接上微服务,而是先单后微:业务跑通、出现独立扩缩容与团队并行需求后,再按 DDD 限界上下文拆分;选型用 Spring Cloud Alibaba(Nacos + Gateway + Sentinel + SkyWalking),通信同步 gRPC/Feign、异步 MQ,跨服务数据走本地消息表最终一致。
**【三点】** 第一,拆分依据:按业务限界上下文(用户/房间/消息/支付/活动/推荐)各成一域一份数据,不按技术层拆;同时符合康威定律——拆分必须匹配团队边界,一个团队一个服务,避免跨团队频繁调用;判断标准是独立扩缩容、独立发布、故障隔离,不是越多越好。第二,技术选型:注册/配置用 Nacos(二合一,客户端 5s 心跳、15s 不健康、30s 剔除,至少 3 节点、主端口 8848),入口网关用 Gateway 统一鉴权路由,熔断限流用 Sentinel,链路追踪用 SkyWalking;选型要匹配团队能力(全是 Java 就别强行上 Go)。第三,通信与数据:强一致实时返回用 gRPC/OpenFeign,必配超时/重试/熔断/幂等四件套;解耦削峰最终一致用 RocketMQ/Kafka;跨服务数据优先本地消息表 + 幂等消费,避免分布式事务过度复杂。先单后微——团队小于 8 人、业务未跑通先做单体,扩展瓶颈出现再拆。
**【边界】** 如果团队小于 8 人、业务还没跑通、迭代频繁未稳定,坚持单体不拆微服务;若必须强一致且无法最终一致,才引入分布式事务框架;任何最终一致方案都要有幂等键、消息表和对账,否则不算一致性方案。