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

2026-10-28

实践场景与业务实践 —— 性能/锁/并发/设计/排查/架构

10 题 · 平均 2.9/5 · low 2.67 · medium 3 · high 3 · 第 30 轮

#1 压测600没问题,上线后300就扛不住了,可能是什么原因? low practice

题目

压测600没问题,上线后300就扛不住了,可能是什么原因?

我的回答

压测600没问题上线 300 就不行,首先是环境问题,压测的环境是否是真实环境?数据量级是否一直?服务规格是否一致,部署服务的服务器是否有其他资源在使用?使用的缓存是否存在不同?数据库数据量大的情况下,请求可能会因为慢查询而导致 rt 增加;另外是否缓存没有预热,没有能走缓存,都直接访问到了数据库,由于压测样本问题,使用的数据可能是 demo 数据,导致都能命中缓存,没有压到数据库,导致两个环境上的状态不一;还有是如果服务器上有其他开启的服务,是否抢占了服务器的资源导致 cpu 被占用导致吞吐量降低;还可能是其他接口占用了过多资源导致服务 gc,影响吞吐,具体需要逐一进行排查;

点评

评分:⭐⭐⭐☆☆(3/5)
优点:抓住"压测环境 vs 生产环境差异"核心矛盾、缓存未预热/压测样本命中缓存判断准、有逐一排查意识。
不足:没提连接数/线程池维度(压测 600 挂生产 300 挂常见根因是生产连接池占满、慢 SQL 放大、依赖第三方变慢)、没提依赖链差异、没提数据分布索引失效。
补充:对照 4 类差异——①机器规格/资源占用 ②数据量级与索引分布 ③缓存预热与命中率 ④下游依赖 RT;再查线程池、GC、连接池占用。 > 完整讲解见上方「完整讲解(权威版)」

完整讲解

> 最后更新: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 数据结论当生产结论。

#2 Redisson和Jedis有啥区别?如何选择? low redis

题目

Redisson和Jedis有啥区别?如何选择?

我的回答

二者都是 redis 的客户端,jedis 依赖的内容更少些,也更简单些,整体实现提供的 redis 的基础功能,如果使用功能复杂度没有太多要求只是用作缓存,可以直接使用 jedis;而 Redisson 则是一个比较全面的封装类,如果有分布式锁的需求以及一些特殊需求,比如布隆过滤器等,在 redisson 中都有直接的方法可以使用,而 jedis 中需要自己实现;选择的时候,如果功能相对简单,可以选择 jedis,如果有复杂需求还是推荐使用 redisson

点评

评分:⭐⭐☆☆☆(2/5)
优点:抓对核心(Jedis 轻量基础/Redisson 功能全面锁与布隆)、选型结论正确。
不足:没讲底层差异(Jedis 同步阻塞单连接非线程安全需连接池;Redisson 基于 Netty 异步线程安全自带分布式锁 RedissonLock 看门狗/可重入/公平锁)、没提 Redisson 分布式锁价值(watchdog 自动续期/Lua 原子性/锁重入)、没讲线程安全差异。
补充:三组对比——①底层(同步阻塞 vs Netty 异步)②线程安全(连接池必须 vs 内置安全)③高级能力(无 vs 分布式锁/布隆/限流)。 > 完整讲解见上方「完整讲解(权威版)」

完整讲解

> 最后更新: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 独立接入,需评估双客户端带来的连接与运维成本。

#3 使用分布式锁时,分布式锁加在事务外面还是里面,有什么区别? low practice

题目

使用分布式锁时,分布式锁加在事务外面还是里面,有什么区别?

我的回答

使用分布式锁时,分布式锁要放在事务外部,不能放在事务内部,放在事务内,分布式情况下,每个服务都开启了自己的事务,造成多事务的阻塞,把压力都转到了数据库,可能会造成大量阻塞;放在事务外侧,才能有效阻断分布式环境下多端点之间的竞争,减少事务资源浪费;并且在事务内会让事务过程中额外产生远程调用,将事务的范围扩大影响事务执行,并且在事务内如果没有持有到锁导致阻塞等待,会直接阻塞一个数据库链接,最终可能使得数据库无法使用链接全部被占用,一定不要再事务内做锁;

点评

评分:⭐⭐⭐☆☆(3/5)
优点:结论正确锁放事务外、讲出事务内持锁等待阻塞 DB 连接可能耗尽连接池、讲到事务范围扩大。
不足:没讲锁释放时机 vs 事务提交的竞态(提交发生在解锁之后会出现 B 读旧数据)、没提正确姿势(lock→事务→提交→unlock 且解锁放 finally、TransactionTemplate 手动控制)、没讲锁内做远程调用放大锁粒度与死锁风险。
补充:核心结论不变但要补"解锁≠提交完成"的竞态(推荐 TransactionTemplate 手动控制提交在解锁前)。 > 完整讲解见上方「完整讲解(权威版)」

完整讲解

> 最后更新: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`,且提交先于解锁。

#4 如何保证多线程下i++结果正确? medium java-concurrency

题目

如何保证多线程下i++结果正确?

我的回答

多线程下保证i++正确可以有多种方案,i++命令并不是一个原子命令,如果只是要实现i+1的操作,可以直接用jdk的原子类内部自带加1等的操作,可以保证原子性的加减;另外可以使用CAS,通过比较交换来实现i++操作;最后是加锁,让所有线程串行化执行,来让i++在多线程下正确执行

点评

评分:⭐⭐⭐☆☆(3/5)
优点:三方案覆盖全(原子类/CAS/加锁)、点出 i++ 非原子、有"串行化"意识。
不足:没讲 CAS 的 ABA 问题与自旋开销(高竞争下自旋浪费 CPU,可退化到锁或 `LongAdder` 分段累加);没讲 `LongAdder` 适合高频读多写少的计数场景(分段 `Cell` 数组 + 最后 `sum()`);没讲 `volatile` 单独不能保证 `i++` 原子(只能保证可见性)。
补充:①`volatile` 只保证可见性不保证复合原子性;②高竞争计数用 `LongAdder`(分段累加)比 `AtomicLong` 自旋更优;③CAS 注意 ABA(可用 `AtomicStampedReference`)。 > 完整讲解见上方「完整讲解(权威版)」

完整讲解

> 最后更新: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` 不能替代互斥,只能做可见性标志。

#5 如果让你实现消息队列,会考虑哪些问题? medium practice

题目

如果让你实现消息队列,会考虑哪些问题?

我的回答

消息队列中要考虑的问题其实还是挺多的 我们整理下消息发送大概的流程,来梳理下都需要做的事情,然后对应要考虑的内容就都出现了 首先消息最开始是生产者发送消息到消息队列,然后消息队列可能需要持久化这个消息,再然后需要消费者订阅消息;这是一个标准的流程;生产者发送消息,这里要有发送的能力,要有序列化,要能感知消息队列接受了消息,以及发送消息如果没有结果要不要重试,要做什么样的降级操作么?然后是消息队列收到消息和收到消息的处理,收到消息,如何存储,如何能承担高并发?接受的消息,如何分门别类?如何持久化?最后是消费者,如何和消息队列进行交互,如何给消息队列确认消息,使用什么样的api,如何确保消息消费的幂等性,唯一性等

点评

评分:⭐⭐☆☆☆(2/5)
优点:有链路意识(生产者/队列/消费者)结构清晰、提到持久化/幂等/确认关键点都在。
不足:没讲顺序性(同一 key 需有序)、重复消费(至少一次 vs 恰好一次)、消息堆积(积压怎么扩容);没讲可靠性分级(`acks` 配置/刷盘策略同步异步/副本 leader-follower);没讲消费端负载均衡(分区/消费组)、延迟/死信(重试多少次进死信);停留在列了要考虑什么,没给具体技术方案。
补充:四大必考——①顺序性(分区内有序)②幂等(消费端去重)③不丢(ack+刷盘+副本)④不积压(扩容/消费并行度);再补延迟队列、死信、消费组负载均衡。 > 完整讲解见上方「完整讲解(权威版)」

完整讲解

> 最后更新: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)取舍吞吐。

#6 如何实现一个点赞系统? medium practice

题目

如何实现一个点赞系统?

我的回答

点赞系统先确定边界;都需要哪些功能 首先肯定是点赞,是否能一个重复点赞?重复点赞有上限么?点赞功能要有归属么?点赞能取消么?点赞功能应对什么数据?是单个功能有点赞,还是点赞要复用到多个功能上?比如朋友圈,评论,用户是否都能点赞?是否一个体系?统计上有要求么?有一定时间段内的点赞统计需求么?还是只有量级的统计不考虑周期;圈定访问然后开始做实现;如果只是最基础的单个环境点赞,单个用户同一个内容单次点赞,可以取消,点赞要有归属;点赞功能肯定不能每次点击都进行数据库更新,所以最好的实现是批量进行更新,一段时间才进行一次点赞,前置可以存储在缓存中,另外由于要有归属,并且单个人只能点赞单次可是在redis中存储到zset中,按照时间戳作为分数存储,一个顺序的list,然后定期将list落库;点赞次数定期更新到对应记录关联的点赞数据表的点赞次数栏目;

点评

评分:⭐⭐⭐⭐☆(4/5)
优点:有先定边界再设计的工程思维、用 ZSet 时间戳分数 + 定期落库 + 批量更新点赞数正确且高效、考虑单次点赞/取消/归属需求边界。
不足:没讲防重复点赞的原子性(`ZADD` 返回 0 表示已存在或 `SADD` 判重;高并发下要防并发重复);没讲取消点赞(`ZREM`)与计数一致性(取消要减一防重复取消);没讲热点内容(百万赞)的读优化(缓存 ZSet、读多写少用本地缓存);没讲幂等/防刷(同一用户限频、验证码/风控)。
补充:补①`ZADD NX` 判重;②取消 `ZREM` + 计数减;③热点读(本地缓存 + Redis);④防刷限频。 > 完整讲解见上方「完整讲解(权威版)」

完整讲解

> 最后更新: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 可用,缓存故障要降级;三是只覆盖"单用户单次 + 归属"边界,多用户复合(如关注 + 点赞)要重新定边界。

#7 RT飙高问题排查过程 medium practice

题目

RT飙高问题排查过程

我的回答

RT出现标高,先定范围,是某个接口出现标高,还是大批接口出现标高?还是某些机器的接口标高,其他机器接口不高,对应有不同的排查;单个接口标高,去检查调用链路,是否链路上出现问题,某个sql,或者某个三方应用,这种可以很直观的在skywalking等链路追踪工具中查看到;如果是某个机器上的接口都rt标高,那么检查下监控,看是否出现gc异常,有大量级的gc导致这台机器上的接口都出现超时,无法正常访问;如果是大批量接口出现rt飙高,那么检查下是否某个中间件出现问题,或者机器收到工具,有大量链接阻塞其他访问;查看指标可以看cpu使用率,检查数据库是否有慢SQL,检查redis是否出现无法正常使用,redis上是否有慢查询;

点评

评分:⭐⭐⭐☆☆(3/5)
优点:先定范围再排查的分层思路(单接口/单机/全量)排查方法论正确;区分单接口链路/单机GC/全局中间件三类根因判断准;提到链路追踪工具 SkyWalking。
不足:没提 CPU/GC 的具体判据(Full GC 频率/STW 时长/GC 日志);没提线程 Dump 分析(jstack 看 BLOCKED/死锁/池耗尽);没提 Load 与 RT 的关联(Load 高但 CPU 不高→IO 等待/锁等待而非计算瓶颈);没提连接池耗尽(DB/Redis 连接池、Tomcat 线程池)、依赖超时(下游 RT 放大导致本服务线程被占满)。
补充:补①jstack 看线程状态(RUNNABLE/BLOCKED/WAITING)定位锁与池;②Full GC 判据(频率/GC 日志耗时/基线趋势);③Load 高 CPU 低=IO/锁等待;④线程池/连接池耗尽放大。 > 完整讲解见上方「完整讲解(权威版)」

完整讲解

> 最后更新: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/线程排查。

#8 下单支付过程,点击跳转支付,输入密码,支付完成后跳转到订单页,整个过程可能会有什么问题?架构方面做哪些设计? high practice

题目

下单支付过程,点击跳转支付,输入密码,支付完成后跳转到订单页,整个过程可能会有什么问题?架构方面做哪些设计?

我的回答

整个链路先确认下 下单->生成订单->拉起支付->输入密码确认支付->创建支付订单->支付回调和轮询找支付放要状态->更新订单状态->前端跳转支付详情;从前到后,首先下单阶段可能出现的问题 重复点击,可以前端做控制幂等控制,后台接口依据请求做防重,做幂等通过请求信息和时间窗口控制来控制提交频率;生成订单 超卖,提前进行缓存层级的库存校验,直接就不让进入订单下单页面,另外再生成订单的时候再做一轮校验,然后这个阶段对库存进行冻结;订单创建出来拉起支付失败了,进行重试,或者降级处理;订单和支付订单状态不一致,分布式事务;支付链接被篡改,前端跳转失败,弱网环境导致无法拉起支付,渠道无法使用限流,用户支付成功我方订单失败,订单超时取消,但是支付成功了;支付渠道重复回调,回调幂等;回调信息被篡改,信息除了回调给的支付订单号和状态,其他的不可信,其他信息都要从原始订单中查询;渠道异步消息不可靠,我方没收到回调,主动发起状态查询,确保获取到状态信息;支付后关联操作没有执行,比如增加积分,发送消息等,使用MQ保证消息至少一次的语义即可,在消费端做幂等;

点评

评分:⭐⭐⭐⭐☆(4/5)
优点:链路完整逐段识别问题非常系统;覆盖超卖(库存冻结)/分布式事务/回调幂等/回调信息防篡改(只信支付单号+状态)都是硬核考点抓得准;主动查询兜底渠道异步不可靠是资深做法;MQ 至少一次 + 消费端幂等正确。
不足:库存冻结具体实现(Redis 预扣 + Lua/DB 乐观锁)未展开;对账/退款/超时关单与支付取消的状态机未提(订单状态机流转、定时关单 + 主动查支付);回调验签(RSA 验签 + 平台证书)细节可补充。
补充:①Redis 预扣库存 + Lua 原子;②订单状态机(超时未支付→关单/取消→主动查支付防漏);③回调 RSA 验签;④日终对账。 > 完整讲解见上方「完整讲解(权威版)」

完整讲解

> 最后更新: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 + 审计 + 对账;虚拟商品(无库存)可裁库存段。

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

题目

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

我的回答

房间状态等做独立存储,在单独表中,麦位,在线人数等信息都做缓存记录,从缓存中拉取,这种数据都是实时变动的并不需要落库,需要落库的信息也先更新到缓存中,从缓存中拉取;出现数据更新时,使用长连接,去将更新数据推送到客户端,做数据更新,可以使用websocket也可以使用SSE做信息推送,推送给客户端,进行数据更新来保证房间状态在各个直播间都可以看到一致的数据;不能用前端轮询,万人直播间轮询一下qps的并发可能直接打爆机器;

点评

评分:⭐⭐⭐☆☆(3/5)
优点:- 抓对"状态存 Redis + 长连接推送 + 不用轮询"核心正确 - 明确反对客户端轮询(会打爆机器)正确工程判断 - 提到可用 WebSocket 或 SSE 做服务端推送,方向正确
不足:- 未覆盖广播机制:用户长连接挂不同服务器时,状态变更如何跨服务器广播(Redis Pub/Sub / Kafka) - 未提麦位并发管理:SETNX + Lua 原子占位、排队、自动释放 - 未提在线人数心跳保活:30s 心跳 / 60s 超时清理、EXPIRE 自动过期 - 未提一致性模型:麦位强一致同步广播 vs 聊天消息最终一致 MQ
补充:- **广播机制**:状态变更发生在服务器 B、用户连在网关 A → 用 Redis Pub/Sub 广播到所有 WS 网关,网关收到后推给本地连接 - **麦位并发**:`SETNX mic:{micId} userId` 原子占位 + Lua 脚本保证"检查 + 占位 + 广播"原子性;失败进 `LIST` 排队,空位时 `BLPOP` 唤醒;用户下线/心跳超时自动释放 - **心跳保活**:客户端 30s 心跳、服务器 60s 超时清理(`EXPIRE` 自动过期),避免死连接占麦、在线人数虚高 - **一致性选型**:麦位/在线人数强一致(写 Redis + 同步广播,< 100ms);聊天消息最终一致(MQ 异步,秒级收敛,需幂等 + 对账) > 完整讲解见上方「完整讲解(权威版)」

完整讲解

> 最后更新: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)承担消息,麦位状态仍自建——核心是"麦位状态自建,聊天消息用云"。

#10 如果让你从0搭建这个语音社交平台的后端架构,你会如何选型和拆分服务?为什么? high practice

题目

如果让你从0搭建这个语音社交平台的后端架构,你会如何选型和拆分服务?为什么?

我的回答

选型需要看项目目标,以前如果短时间内快速上的项目,可以先用php,或者python先出一个版本,进行初步使用,初期的用户量不会大,功能实现,有这两个实现先出就可以了;不过现在有了AI去使用java实现也远比之前要效率和快捷,并且可用性和扩展性以及性能都会超过php和python不少;服务拆分看规模大小和团队情况;选型也是如此需要看当前手里团队的能力以及历史情况,不能手现在都是java研发,非要用go体系去写,或者都是php非重新找人;这些都要看当前团队能力和团队划分情况去进行选择和拆分;推荐通用的技术选型,springcloud alibaba,使用的相对较多;然后服务划分,从大块上来 ,先要有网关鉴权,这一层面作为所有服务接口入口,进行统一把控统一鉴权;然后是用户体系需要一个独立服务维护 ,和其他服务在一起不合适;语音社交平台,需要有消息服务,管理各种语音消息,之类的内容,这部分是独立业务单独放出来;另外是金融方面的服务需要单独出来,订单创建,礼物购买,支付,等;还有就是活动,这部分内容和其他内容没什么交集,放在一起也是耦合,单独作为一个服务可以独立开发,选择合适的语言合适的人去做;房间方面可以单独做一个服务,语音房间内的信息可以独立出来和其他服务相互隔离;还有推荐服务,推荐用户信息等,这部分可能会涉及比较多的算法之类事情,是独立的一块;然后就是一些个人信息交互类的,可以独立一个服务或者放到用户服务上先做,后期拆分出来,太细致了也会有问题;计划任务也需要单独一个服务去执行不要再业务服务器上,可以调用业务服务的接口;这是我大致的选型和拆分

点评

评分:⭐⭐☆☆☆(2/5)
优点:- 有"选型看团队能力与现状"的意识,务实避免技术炫技 - 拆分逻辑清晰(网关/用户/消息/金融/活动/房间/推荐/任务),域边界合理 - 提到推荐服务独立(算法密集)、计划任务独立(别挂业务服务器),拆分有分寸
不足:- 拆分偏罗列服务,没讲拆分依据(限界上下文/业务复杂度/团队组织 Conway 定律/扩展性) - 没给具体选型理由(为何 Spring Cloud Alibaba、注册中心/网关/配置中心选什么) - 没讲服务间通信(Feign/gRPC/消息)、数据一致性(分布式事务/最终一致) - 没讲可观测性(链路追踪/日志/监控)、高可用(注册中心/熔断限流) - 没讲演进(先单体重服务化,避免一上来过度微服务)
补充:- **拆分依据**:按业务领域限界上下文 + 团队分工(Conway 定律)+ 独立部署需求,而非罗列服务 - **选型组件**:Nacos(注册/配置二合一)、Gateway(统一网关)、Sentinel(熔断限流)、SkyWalking(链路追踪) - **通信与一致**:同步 gRPC/OpenFeign(必配超时/重试/熔断/幂等)+ 异步 MQ 最终一致;跨服务数据用本地消息表 + 幂等消费 - **演进策略**:先单后微(Monolith First),团队小于 8 人、业务未跑通先做单体,避免一上来过度微服务 > 完整讲解见上方「完整讲解(权威版)」

完整讲解

> 最后更新: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 人、业务还没跑通、迭代频繁未稳定,坚持单体不拆微服务;若必须强一致且无法最终一致,才引入分布式事务框架;任何最终一致方案都要有幂等键、消息表和对账,否则不算一致性方案。

📚 查看全部面试记录