← 每日面试题 学习看板

Java 后端模拟面试题

面试题/高阶题目/2026-09-17-线程池-数据库-系统设计.md

Java 后端模拟面试题

日期:2026-09-17

总题数:3 题

方向覆盖:线程池优化、数据库优化、系统设计


R1 线程池优化 | 难度:进阶

题目

支付服务异步通知模块,共享线程池 notifyExecutor(core=16, max=64, queue=1000, CallerRunsPolicy),任务 RT≈200ms,峰值 QPS≈800/s。P99 RT 从 500ms 飙到 12s,CPU 仅 30%。IM 下游 5% 超时(10s+),风控为强依赖。

问题:

  1. 定位 P99 飙高根因,为什么 CPU 低也会阻塞 Tomcat?CallerRunsPolicy 起什么作用?
  2. 强弱依赖如何重构线程池模型?
  3. 任务超时控制、拒绝策略、监控告警如何设计?

原始回答

Q1:RT 飙升原因是核心线程不足,core 处理量与峰值 QPS 差距大。多余任务堆到消息队列。按 core×5 算约 80 QPS,峰值 800 只能堆队列,导致延迟。因为队列 1000 很长,不会创建新线程,只核心线程跑,所以 CPU 不高。CallerRunsPolicy 会把任务放回请求线程执行,阻塞 Tomcat 线程。 Q2+Q3:IM 下游风控强依赖,线程池内调。其他抛到 Kafka 之类的 MQ 消费,不等返回。线程池打满雪崩是因为没做划分,单独任务用单独线程池。不能把拒绝直接抛回主线程让 Tomcat 等。拒绝策略自定义,被拒的扔到 MQ。任务必须设超时,超时重试,重试不成功抛 MQ,异步处理。监控大盘监控队列长度、活跃线程数、任务数、CPU,异常情况下队列/RT 提升要有告警。

点评

优点:

  1. 主链路清晰:core→queue→CallerRuns→Tomcat 阻塞的完整闭环
  2. 能算出 core×(1000/200)=80 QPS 的吞吐模型
  3. 拒绝策略+监控意识齐全,具备实操经验

不足:

  1. 未说透 core → queue → max 的判据(队列满才扩到 max)
  2. P99 飙到 12s 的关键乘数(5% 慢下游占线程)未提
  3. CPU 30% 的解释偏了:不是"没跑",是"IO 阻塞"(下游 RPC 阻塞)
  4. CallerRunsPolicy 的雪崩链未展开(Tomcat 线程被占用 10s → 上游重试 → 雪崩)
  5. 强弱依赖概念反了(风控才是强依赖,IM 才是慢弱依赖)
  6. 线程池隔离粒度未落地(未按依赖强弱 + RT 分桶)
  7. 超时机制太泛,未提重试策略/幂等/队列长度影响

完整讲解要点

得分:6.5/10(合格偏上)


R2 数据库优化 | 难度:进阶

题目

订单库 3000 万行,InnoDB + REPEATABLE READ。营销后台 UPDATE orders SET status='PROMO' WHERE created_at > '2026-09-16' 卡 30 分钟;查询事务看到过期数据;订单服务 UPDATE ... WHERE user_id=10086 报 Lock wait timeout;SHOW ENGINE INNODB STATUS 出现 gap lock 字样。

问题:

  1. RR 下为什么 SELECT 看到过期数据?讲 MVCC ReadView 生成时机与可见性规则,RC 为何无此问题?
  2. 该 UPDATE 为何持大量锁?Gap Lock 怎么"夹"上去?created_at 无索引时锁范围怎么变?
  3. 如何规避大范围 UPDATE + gap lock 连锁等待?给 SQL 改写 + 索引 + 事务粒度方案。

原始回答

候选人:思路不多,无回答,请求讲解。

点评

优点:暂无

不足:MVCC + 锁机制是数据库优化的核心考点,应能张口就来。

补充:完整讲解见下。

完整讲解要点

1. ReadView 生成时机

隔离级别ReadView 生成结果
RR事务首次 SELECT 时整个事务复用同一快照
RC每次 SELECT 前重新生成读到最新提交数据

ReadView 内容:creatorTrxId + m_ids(活跃事务ID列表) + minTrxId + maxTrxId

可见性判断:

trx_id == creatorTrxId        → 可见
trx_id < minTrxId             → 可见
trx_id >= maxTrxId            → 不可见
minTrxId <= trx_id < maxTrxId → 查 m_ids:在=不可见,不在=可见

2. Gap Lock 机制

锁类型作用
Record Lock锁一条记录
Gap Lock锁两记录之间的区间(开区间),防插入
Next-Key LockRecord + Gap,InnoDB 默认加锁单元

关键判据:created_at 无索引 → 全表扫描 → 每行都加 Next-Key Lock → 锁全表。

user_id = 10086 那条 UPDATE 走 idx_user_id 只应锁一行,但被全表扫描的那个事务扫到这一行时已经加锁,等锁超时。

3. 治理方案

  1. 加索引:ALTER TABLE orders ADD INDEX idx_created_at (created_at)
  2. 收窄范围:AND status IN ('PAID', 'SHIPPED')
  3. 拆小事务:LIMIT 1000 分批 + 循环
  4. 降 RC:ALTER TABLE orders DEFAULT ISOLATION LEVEL READ COMMITTED(RC 下当前读只加 Record,不加 Gap)
  5. 缩短锁等待:innodb_lock_wait_timeout 50s → 5-10s
  6. 运维兜底:pt-archiver / gh-ost 分批 + 限速

排查工具:

SELECT * FROM performance_schema.data_lock_waits;
SELECT * FROM information_schema.innodb_trx;
SHOW ENGINE INNODB STATUS\G

得分:N/A(讲解模式,未作答)


R3 系统设计 | 难度:高阶

题目

开放平台限流系统。峰值 12 万 QPS,500+ 接入方,200+ API。限流维度 app_id × API × IP × 时段。误差 ≤5%,RT 增加 ≤10ms,可用性 99.99%。已有 Redis 集群(10 分片)。

问题:

  1. 分布式限流算法选型?给出核心算法(Lua/Redisson/自研)+ 并发安全 + 误差来源?
  2. 4 维 Key 设计如何避免大 Key 和内存爆炸?分层限流如何设计?画出请求链路上限流决策点?
  3. 降级策略、灰度/全局限流联动、监控指标体系、规则动态下发方案?

原始回答

Q1:推荐令牌桶,Redis Lua 实现。每次 1 秒按 app_id 分配令牌,按时间、用户等级划分。不同 API 有不同 QPS,三方能力决定 API 限流。时段不同 QPS 也不同,每阶段分配令牌桶。并发安全靠 Redis 分片,10 万 QPS × 3-4 层 ≈ 50-60 万 QPS,需要打散到不同分片。误差来源暂无思路。 Q2:4 维组合分开放,不聚合。多层级独立限流,不同层不同规则。Key 聚合会导致量太大、Key 太多。方案无思路,请求讲解。

点评

优点:

  1. 令牌桶 + Lua 选型正确,理由能说出(Redis 友好、允许突发、原子性)
  2. 主动想到 QPS 放大倍数要分片 Redis,工程意识不错

不足:

  1. Key 设计未落地:给了维度但没给具体 Key 结构,无内存估算
  2. 分层限流未画链路:网关/应用/中间件三层的边界和职责未展开
  3. 降级方案完全缺失:fail-open/fail-closed 选择、时钟漂移、动态规则下发都是必考
  4. 误差来源未主动分析(时钟偏差、离散化、网络抖动、本地缓存)
  5. capacity vs rate 的关系未提(允许突发需 capacity > rate)
  6. 未提服务端 TIME 命令避免客户端时钟漂移

完整讲解要点

1. 算法对比

算法平滑允许突发分布式友好
固定窗口否是★★
滑动日志是否★
漏桶是否★★
令牌桶是是★★★★

选令牌桶:允许突发 + Redis Lua 原子 + Redisson RRateLimiter 现成。

2. Lua 核心

local tokens = tokens + elapsed * rate / 1e9
tokens = math.min(capacity, tokens)
if tokens >= requested then tokens = tokens - requested; allowed = 1 end
redis.call('HMSET', key, 'tokens', tokens, 'timestamp', now)
redis.call('EXPIRE', key, 2)

误差来源:时钟偏差(用 Redis TIME)、离散化(纳秒级可忽略)、网络抖动、本地缓存。

3. Key 设计(不聚合,分维度独立)

rl:tenant:{app_id}:{tier}:{time_bucket}     -- 租户级
rl:api:{api_id}:{tier}                      -- API 级
rl:ip:{ip_hash}:{api_id}                    -- IP 级

内存估算:500 app × 24h = 1.2 万 Key;200 API × 3 tier = 600;10 万 IP(快过期)。总 <10 万 Key,Redis 内存 <50MB。

4. 分层限流决策链

[1] CDN/DNS 层(IP 粗粒度防 DDoS)
[2] 网关层限流 ★ 核心(租户+API+IP)
[3] 应用层限流(Sentinel 自适应+熔断)
[4] 中间件层(DB/MQ 连接池兜底)

5. 降级三级

级别现象动作
L1单分片抖动本地 Guava RateLimiter 兜底
L2Redis 挂fail-open(普通)/fail-closed(支付风控)
L3时钟漂移 >1s切服务端 TIME + 告警

6. 动态规则下发

Apollo/Nacos 存规则元数据 → 通知网关 reload Caffeine 本地缓存(5s TTL)→ 不发版改规则。

7. 灰度限流

base_qps + gray_qps + gray_percent + gray_until,新规则 1%→10%→100% 逐步放量。

8. 全局限流联动

错误率 >20% 持续 1 分钟 → 全局降到 50% + 关键 API 熔断 + oncall 告警。

得分:5/10(思路对,落地不足)


面试总结

各轮次表现汇总

轮次方向难度表现得分
R1线程池优化进阶主链路对,关键机制漏说6.5/10
R2数据库优化进阶未作答,进入讲解模式N/A
R3系统设计高阶选型对,Key/分层/降级缺失5/10

强项

  1. 主链路思维清晰:R1 能从配置推到雪崩的完整闭环,工程实操感强
  2. 算法选型直觉准:R3 直接选令牌桶 + Lua,理由站得住
  3. 有实操意识:拒绝策略、监控告警、Redis 分片都能主动想到

待提升点

  1. 底层机制要说透:ThreadPoolExecutor 三段式判据、MVCC ReadView、Redis TIME 等都是"张口就来"的考点
  2. 数字要量化:线程池参数、Key 内存、QPS 容量要有推导过程
  3. 系统设计的三件套:Key 结构 + 分层拓扑 + 降级监控,缺一即掉档
  4. 数据库锁机制薄弱:MVCC + Gap Lock 是数据库优化核心,必须掌握

建议

技术面:

  1. 花 1 天系统过一遍 InnoDB 锁机制(Record/Gap/Next-Key)+ MVCC ReadView,画对比表贴到备忘录
  2. 系统设计题套模板:选型对比 → Key 结构 → 分层拓扑 → 降级/监控/动态下发,四段式必写齐
  3. 线程池题准备"四段答题模板":配置分析 → 队列/线程关系 → 阻塞类型 → 隔离+超时+拒绝+幂等+监控

做题面:

  1. 遇到不会的题不要直接说"没思路",先给结论 + 猜测 + 反问面试官确认范围,比空答好
  2. 每道系统题准备一个"兜底方案",比如"如果只能选 Redis 做,我会..."

会话全文记录

2026-09-17 面试会话

面试官:加载技能,读取历史(9/14、9/15 已覆盖),确定今日方向:线程池优化、数据库优化、系统设计。开始 R1。

R1 Q1 候选人回答:

首先 RTV 飙升的原因是核心线程不足,它单位能处理的量与单位实际到达的 QPS 峰值差距比较大。然后多余的任务就都堆到消息队列里了。通过 QPS 峰值和实际 RTV 请求平均时间算,在峰值的情况下,它必然无法正常处理。当年平均请求时间和 CPU 核数,核心 CPU 核数算出来的 QPS 能达到的结果就是 16×5。也就是 80 QPS,但是峰值到了 800,多余的那些 QPS 是只能堆到任务队列中;这就导致了任务的延迟执行,也就是 RT 飙升的根本原因。但是由于任务又任务队列又特别长,1000 的任务队列,这就导致不会去创建新的线程去处理这些任务,只会核心线程去跑,这就是 CPU 不高。但是又由于配置了拒绝策略,它这个拒绝策略是当无法正常处理的时候,放到请求线程中去处理,不异步处理,这就导致了阻塞 Tomcat 的线程了。

面试官点评:优点是主链路闭环,算出 80 QPS。不足是没说透 core→queue→max 判据、P99 慢下游乘数、CPU 低其实是 IO 阻塞、CallerRunsPolicy 的雪崩链未展开。

R1 Q2+Q3 候选人回答:

第二个方案是 IM 下游风控强依赖,这块可以在线程池内调。然后线程池其他的可以抛到任务队列里边,弄一个 Kafka 之类的 MQ 消息队列,然后去消费,不用等风控的返回,其他的异步去跑,不用在这等。首先线程池打满导致全站雪崩,这个事就是线程池没有做划分。单独任务用单独的独特的线程池。另外不能像这里边直接往主线程抛,让 Tomcat 等着,这个也会导致雪崩。拒绝策略可以自定义,拒绝的扔到 MQ。任务一定要设超时,超时了就进行重试,重试不成的话就抛队列,抛消息队列,最终给一个反馈,比如创建日志记录,做状态更新。监控方面监控各线程池队列多长、活跃线程数、任务数、CPU,异常情况告警。

面试官点评:优点是隔离思路 + 拒绝策略落地 + 监控意识。不足是强弱依赖反了(风控才是强依赖)、隔离粒度未落地、超时机制太泛、未提幂等、未提队列长度影响。

R2 候选人回答:

这题你给我讲一下吧,我思路不多,没有什么思路。

面试官:进入完整讲解模式,讲 MVCC ReadView、Gap Lock 机制、治理方案。

R3 候选人回答:

首先第一个选型,分布式场景我更推荐令牌桶,因为有现成的 Redis,可以通过 Redis 的 Lua 脚本去实现这个分布式限流。这时候我们可以每次在 1 秒内拿出,给每个客户 ID,也就是说 APP ID,去分配足量的、他的符合他标准的令牌。分配令牌的方式,按照时间、用户等级,然后去分别划分。由于限流维度组合是 APP ID、API、IP、时段几个分着来的。这时候你可以去看一下,首先是 APP ID,APP ID 对应的是用户,用户本质上是有自己的一个单独的 QPS,是不同等级来划分的,最高是 1 万。然后 API,其实 API 每个 API 都其实有自己的 QPS 的,因为我们不只是自己的业务,还有下游的业务、三方业务,这时候三方的根据三方的能力,你其实要限定你的 API 的 QPS 的,所以每个 API 实际上都要有自己的令牌。时段,不同的时段请求的令牌,请求的那个 QPS 也不同。并发安全性的话,Redis 本身能支持的大概是在 10 万 QPS,但是当前 QPS 总峰值其实已经 12 万了,而且我们做了多层的令牌,这时候它的 QPS 其实是峰值 12 万,再乘以我们对应的层级的,也就是 3~4,也就是将近 50 万的 QPS,60 万的 QPS。这时候对这些不同的令牌桶,其实要做分片,打到不同的分片上,这样才能保证承担下真的大的并发量。误差方面的话,具体来源我暂时没有思路,到时候可以讲一下。 四维组合我考虑其实是分开的,而不是聚合的,在多层级去限流,因为不同层级有不同的限流。这的设计给出的是把这四个组合在一起做一个限流,就意味着它的量会很大,Key 值会非常多。他的方案你来讲解吧,我暂时没有什么思路,给我做一个整体的讲解。

面试官:给出完整讲解,包括算法对比、Lua 核心、Key 结构、分层限流、降级方案、动态下发。