日期:2026-09-17
总题数:3 题
方向覆盖:线程池优化、数据库优化、系统设计
支付服务异步通知模块,共享线程池 notifyExecutor(core=16, max=64, queue=1000, CallerRunsPolicy),任务 RT≈200ms,峰值 QPS≈800/s。P99 RT 从 500ms 飙到 12s,CPU 仅 30%。IM 下游 5% 超时(10s+),风控为强依赖。
问题:
Q1:RT 飙升原因是核心线程不足,core 处理量与峰值 QPS 差距大。多余任务堆到消息队列。按 core×5 算约 80 QPS,峰值 800 只能堆队列,导致延迟。因为队列 1000 很长,不会创建新线程,只核心线程跑,所以 CPU 不高。CallerRunsPolicy 会把任务放回请求线程执行,阻塞 Tomcat 线程。 Q2+Q3:IM 下游风控强依赖,线程池内调。其他抛到 Kafka 之类的 MQ 消费,不等返回。线程池打满雪崩是因为没做划分,单独任务用单独线程池。不能把拒绝直接抛回主线程让 Tomcat 等。拒绝策略自定义,被拒的扔到 MQ。任务必须设超时,超时重试,重试不成功抛 MQ,异步处理。监控大盘监控队列长度、活跃线程数、任务数、CPU,异常情况下队列/RT 提升要有告警。
优点:
core×(1000/200)=80 QPS 的吞吐模型不足:
core → queue → max 的判据(队列满才扩到 max)core → queue → max,只有队列满才创建 max 线程订单库 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 字样。
问题:
created_at 无索引时锁范围怎么变?候选人:思路不多,无回答,请求讲解。
优点:暂无
不足:MVCC + 锁机制是数据库优化的核心考点,应能张口就来。
补充:完整讲解见下。
| 隔离级别 | 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:在=不可见,不在=可见
| 锁类型 | 作用 |
|---|---|
| Record Lock | 锁一条记录 |
| Gap Lock | 锁两记录之间的区间(开区间),防插入 |
| Next-Key Lock | Record + Gap,InnoDB 默认加锁单元 |
关键判据:created_at 无索引 → 全表扫描 → 每行都加 Next-Key Lock → 锁全表。
user_id = 10086 那条 UPDATE 走 idx_user_id 只应锁一行,但被全表扫描的那个事务扫到这一行时已经加锁,等锁超时。
ALTER TABLE orders ADD INDEX idx_created_at (created_at)AND status IN ('PAID', 'SHIPPED')ALTER TABLE orders DEFAULT ISOLATION LEVEL READ COMMITTED(RC 下当前读只加 Record,不加 Gap)innodb_lock_wait_timeout 50s → 5-10s排查工具:
SELECT * FROM performance_schema.data_lock_waits;
SELECT * FROM information_schema.innodb_trx;
SHOW ENGINE INNODB STATUS\G
开放平台限流系统。峰值 12 万 QPS,500+ 接入方,200+ API。限流维度 app_id × API × IP × 时段。误差 ≤5%,RT 增加 ≤10ms,可用性 99.99%。已有 Redis 集群(10 分片)。
问题:
Q1:推荐令牌桶,Redis Lua 实现。每次 1 秒按 app_id 分配令牌,按时间、用户等级划分。不同 API 有不同 QPS,三方能力决定 API 限流。时段不同 QPS 也不同,每阶段分配令牌桶。并发安全靠 Redis 分片,10 万 QPS × 3-4 层 ≈ 50-60 万 QPS,需要打散到不同分片。误差来源暂无思路。 Q2:4 维组合分开放,不聚合。多层级独立限流,不同层不同规则。Key 聚合会导致量太大、Key 太多。方案无思路,请求讲解。
优点:
不足:
| 算法 | 平滑 | 允许突发 | 分布式友好 |
|---|---|---|---|
| 固定窗口 | 否 | 是 | ★★ |
| 滑动日志 | 是 | 否 | ★ |
| 漏桶 | 是 | 否 | ★★ |
| 令牌桶 | 是 | 是 | ★★★★ |
选令牌桶:允许突发 + Redis Lua 原子 + Redisson RRateLimiter 现成。
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)、离散化(纳秒级可忽略)、网络抖动、本地缓存。
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。
[1] CDN/DNS 层(IP 粗粒度防 DDoS)
[2] 网关层限流 ★ 核心(租户+API+IP)
[3] 应用层限流(Sentinel 自适应+熔断)
[4] 中间件层(DB/MQ 连接池兜底)
| 级别 | 现象 | 动作 |
|---|---|---|
| L1 | 单分片抖动 | 本地 Guava RateLimiter 兜底 |
| L2 | Redis 挂 | fail-open(普通)/fail-closed(支付风控) |
| L3 | 时钟漂移 >1s | 切服务端 TIME + 告警 |
Apollo/Nacos 存规则元数据 → 通知网关 reload Caffeine 本地缓存(5s TTL)→ 不发版改规则。
base_qps + gray_qps + gray_percent + gray_until,新规则 1%→10%→100% 逐步放量。
错误率 >20% 持续 1 分钟 → 全局降到 50% + 关键 API 熔断 + oncall 告警。
| 轮次 | 方向 | 难度 | 表现 | 得分 |
|---|---|---|---|---|
| R1 | 线程池优化 | 进阶 | 主链路对,关键机制漏说 | 6.5/10 |
| R2 | 数据库优化 | 进阶 | 未作答,进入讲解模式 | N/A |
| R3 | 系统设计 | 高阶 | 选型对,Key/分层/降级缺失 | 5/10 |
技术面:
做题面:
面试官:加载技能,读取历史(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 结构、分层限流、降级方案、动态下发。