说实话,这两年我面试别人和被别人面试,问得最多的就是高并发。不是大家故意卷,而是高并发这个问题一头连着业务场景,另一头连着基础原理,从一条问题链能串出缓存、队列、锁、线程池、数据库、JVM一堆东西,特别适合用来判断一个人是背过八股文还是真在线上扛过流量。这篇文章我整理了10个我在面试中必问、或者被问得最多的高并发场景问题,每个都附带我自己的答题思路、背后的技术原理,以及一些真实场景里踩过的坑。不管你是在准备面试,还是在设计自己的系统,这篇都值得收藏下来慢慢看。
先说一个核心观点:高并发面试题里没有标准答案,面试官真正想看的,是你遇到问题时的思考路径——你在哪些环节做了取舍、你的方案有没有兜底、你有没有考虑过极端情况。下面这10个问题,每一个我都会按“面试官在考察什么 → 核心原理拆解 → 推荐答题框架 → 实操经验/踩坑记录”这个顺序来讲,方便你直接照着准备。
1. 一个请求从浏览器到服务器,到底慢在哪里?——高并发系统的全链路延迟拆解
这道题通常是高并发面试的开场题,看起来简单,其实能考察你对整个请求链路的理解深度。面试官会问:“假设你的系统平时响应50ms,双11流量上来之后变成500ms,你怎么排查?”
你要是上来就说“加缓存、加机器”,基本就凉了。这道题的正确打开方式是建立全链路延迟模型:客户端网络 → DNS解析 → CDN/负载均衡 → 网关 → 应用服务器 → 线程池排队 → 内存计算 → 本地缓存 → RPC调用 → 数据库SQL执行 → 响应序列化 → 网络回传。
我自己的排查习惯是“从外到内、分层打点”。先用链路追踪工具(比如SkyWalking或Zipkin)看整个调用链的耗时分布,确定瓶颈是在入口网关、应用服务还是数据库。很多时候问题不在代码,而在于:线程池被IO阻塞占满、数据库连接池排空、Redis慢查询、或者Full GC频繁导致应用停顿。举一个我在电商项目里遇到的案例:某个商品详情接口平时8ms,大促时涨到300ms,链路追踪一看,发现Redis响应正常,问题出在应用层连续三次查询了同一个缓存Key,每次缓存Miss都打到了数据库,数据库CPU直接飙高,整个接口被DB慢查询拖死。所以这道题的回答要体现你“会分层排查”而非“只会瞎猜”。
答题的时候,建议按“现象描述 → 建立假设 → 链路追踪定位 → 资源监控验证 → 针对性优化”的逻辑来讲,最后落到“系统压测数据”上。你能说出来“我们这个接口在压测时,P999延迟从120ms降到了45ms”,面试官就会觉得你是真的做过,而不是背过。
2. 缓存穿透、缓存击穿、缓存雪崩:三个名字相近、应对方式完全不同的经典问题
这算是高并发面试的必考题了,三个概念相似又容易混淆。考的是你有没有在真实流量下处理过缓存异常,而不是只会背书。
- 缓存穿透:查询一个根本不存在的数据,缓存和数据库都没有,导致每次请求都穿透到数据库。
- 缓存击穿:某个热点Key在缓存过期的瞬间,大量请求同时访问该Key,全部打到数据库。
- 缓存雪崩:大量Key在同一时间过期,或者Redis直接宕机,导致请求全部落到数据库。
我在不同公司问过不下50个人,很多人能把概念背得滚瓜烂熟,但问到“你项目里怎么防”就卡住了。
针对缓存穿透,我的标准做法是双层过滤:第一层用布隆过滤器拦截不存在的Key,第二层对查询结果为null的数据也做短暂缓存(比如60秒),防止恶意流量穿透。需要注意,布隆过滤器存在误判率,所以只能挡掉“一定不存在”的请求,不能完全替代DB层的兜底。
缓存击穿的解法核心是热点Key的互斥重建。可以用分布式锁保证只有一个线程去数据库重建缓存,其他线程在缓存没有回填之前先等或者降级。JVM级别的锁只对单机有效,在分布式场景下一定要用Redis分布式锁或者Redisson的可重入锁。另外,更推荐的做法是逻辑过期而不是物理过期——存一个逻辑过期时间字段,请求过来发现逻辑过期,立刻返回旧值,同时异步去刷新缓存。这种方式对高并发场景更友好,不会造成瞬时击穿。
缓存雪崩要分两层应对。第一层是Key层面:设置过期时间时加上一个随机偏移量(比如3~5分钟内的随机数),避免大量Key在同一秒过期。第二层是Redis高可用层面:Redis主从架构配合Sentinel或Cluster模式,确保单点故障时能自动切换,同时开启本地缓存兜底,即使Redis挂了,应用还能在短时间内靠本地缓存撑住。
这里再说一个我踩过的坑:布隆过滤器用Google的Guava实现,单机用没问题,但在多实例部署时每台机器的布隆过滤器是独立的,新增数据时无法广播,导致某些实例判断结果不一致。后来我们换成了Redis的BitMap实现布隆过滤器,把位数组放到Redis里共享,才解决了这个问题。
3. 热点Key问题:一个商品的流量占了全站50%,你的缓存架构撑得住吗?
热点Key是高并发场景非常现实的杀手。2012年淘宝大促时“万笔交易同时抢一个商品”就是典型的热点场景,平时压抑的热度会在某个瞬间全部爆发。面试官问这道题,通常想看:你有没有真正遇到过“某几个Key高热度”的流量模型,以及你有多少种手段来缓解局部压力。
核心思路永远是分而治之 + 多级缓存。热点Key不能只存在Redis一个节点上,否则这个节点的网卡带宽、CPU、单线程命令处理能力都会被打满。我常用的方案是:本地缓存(Caffeine/Guava) + Redis主集群 + Redis热点副本集群。在JVM层面先挡掉一部分流量,如果本地缓存没命中,再加一层Key哈希访问Redis。当系统监测到某个Key的QPS超过阈值时,自动把这个Key复制出多份(比如在Key后面加后缀_hash1、_hash2…),分散到Redis的不同分片上。
热点Key的监测也不能靠人工。“每次大促前手动整理热点清单”这种方案效率太低还容易漏。现在主流做法是接入实时监控,比如Sentinel的热点参数限流,或者自研采样统计:在网关层采集每个Key的访问频次,用滑动窗口判断是否存在突刺流量,一旦触发阈值就自动走热点Key治理策略。
答这道题时,面试官还会追问“热点Key和普通Key在过期策略上有区别吗”。你如果能说出“热点Key不应该设置物理过期时间,而要用逻辑过期、定时主动刷新,避免在过期瞬间引发击穿风险”,就会让面试官连连点头。这属于只有在线上一线踩过坑才能攒下来的经验,比背诵“缓存三大问题”有分量得多。
4. 接口幂等性怎么设计?你会怎么处理重复提交、重复支付?
高并发场景下,幂等性问题不是“可能遇到”,而是“一定会遇到”。最典型的场景是支付回调:用户在下单页面点了一下支付,网络抖动超时,用户又点了一次;或者支付平台回调由于网络重发,同样的通知来了好几遍。你的后端如果没做幂等保护,就会导致一个订单被创建多次、一个库存被扣减多次,这是严重的资金和库存事故。
幂等设计的核心,是让同一个请求不论执行一次还是多次,效果完全相同。我的设计方法是“唯一业务键 + 状态机 + 分布式锁”三件套。
第一步,客户端生成全局唯一请求号(RequestId/UUID),服务端接收到请求后,先去Redis里用SETNX检查该RequestId是否已经处理过。如果SETNX返回成功,说明是第一次请求,继续正常处理;如果返回失败,说明请求重复,直接返回上次的结果。这里有个关键点:SETNX命令要跟业务处理放在同一个事务边界内,或者用Redis事务/Multi-Lua脚本保证原子性。
第二步,数据库层面也要有兜底。在订单表、流水表上建立唯一索引(如request_id),即使并发下多个请求同时到达,数据库唯一索引也能保证只有一条成功,其他直接抛异常捕获处理。数据库的唯一索引是幂等保护的最后一道防线,无论应用层网络怎么重试,都不会丢数据。
第三步,处理状态机。订单状态从“待支付 → 已支付 → 已发货 → 已签收”,每一步都要有前置状态校验。比如支付回调来了,发现订单状态已经是“已支付”,就直接返回成功,不去重复扣库存。这个操作很多新人会忽略,结果并发重复回调时,一个订单被减了两次库存。
我还踩过这样一个坑:一开始只用分布式锁(基于Redisson)来保护幂等,结果锁只有30秒有效期,某个下游接口处理时间超过了30秒,锁自动过期,第二个请求进来了,重复执行了业务逻辑。后来改成“数据库唯一索引 + 状态机校验”,才彻底根治。所以在面试时我会强调:锁是手段,唯一键和状态机才是幂等的定海神针。
5. 数据库连接池到底要配多大?为什么你的“很大”配置反而把数据库打死了?
这个问题的出题角度很有意思,面试官可能直接问你:“你们服务数据库连接池Size设的多少?为什么是这个值?”很多人答不上来,因为从来没想过这个问题。实际上,连接池参数的背后是对数据库并发模型的理解。
先给结论:连接池大小不是越大越好,在IO密集型的数据库场景下,过大的连接池反而会严重降低吞吐量。PostgreSQL官方文档里给的一个经验公式是:连接数 = ((核心数 × 2) + 有效磁盘数)。比如一台4核的数据库机器,磁盘是SSD,连接数可以设为10~12个左右;如果是HHD机械磁盘,连接数建议更小。
为什么这么小?因为每个数据库连接背后都是一个线程,线程多了,CPU大量时间花在上下文切换上,而不是真正执行SQL;连接池过大还可能导致数据库端的锁竞争加剧以及内存占用飙升。像Oracle数据库在单实例上默认连接数的上限是几百个,但绝大多数业务根本用不到。
更合理的方案是应用层连接池大小按接口的RT(响应时间)和QPS来反推。假设你单个SQL平均耗时20ms,单连接每秒能处理约50个请求(1000ms/20ms),如果接口QPS是2000,那么理论最小连接数是40。在这个基础上再加一些缓冲(比如1.5倍),就能得出一个比较合理的配置值。这比盲目配置200个连接靠谱得多。
在实际项目里,我会把连接池的监控指标(活跃连接数、空闲连接数、等待获取连接的线程数)接入Prometheus,大促前观察一下等待线程数是否经常飙高,如果经常飙高就说明连接池偏小,反之则说明池子太大。另外,一定要设置合理的连接超时时间(比如30秒)和空闲回收时间(比如60秒),防止数据库端主动断开连接后,应用还拿着废弃连接,抛出“Connection is closed”的间歇性报错。
6. 削峰填谷:怎么用消息队列扛住瞬时流量?MQ堆积了怎么办?
高并发场景真正考验系统的不是平均流量,而是瞬间峰值。而消息队列的核心价值就是削峰填谷:把突发的请求先存起来,让后端的消费者按照自己的处理能力稳定消费,避免流量尖峰直接打到数据库或下游接口上。
面试问题通常是:“大促时下单QPS冲到20000,但数据库只能承受2000,你怎么设计?”你需要清楚地说明,引入MQ之后整个流程变成了“前端请求 → 写入消息 → 返回‘已受理’ → 消费者异步拉取 → 处理订单 → 反馈结果”。用户感知不到异步带来的延迟,但系统承受的压力完全变了。
关于MQ选型,Kafka、RocketMQ、RabbitMQ各有适用场景:吞吐量要求极高且允许小概率消息丢失时选Kafka;金融级事务消息、需要严格顺序和事务保证时选RocketMQ;轻量级、生态成熟、中小规模场景选RabbitMQ。面试时不妨顺带聊聊你所在业务场景下为什么选这个MQ,比空谈“用Kafka高吞吐”要有说服力。
消息堆积是另一个高频追问点。消费者消费速度跟不上生产者发送速度时,堆积就会发生。我的排查路径是:先看是消费逻辑本身太慢,还是下游依赖接口变慢导致消费线程阻塞,抑或是消费者的并发度设置太低。常用的解决手段包括:增加topic的分区数和消费者实例数、批量消费、调整消费线程池大小、以及在下游依赖不稳定时启用“空转降级”策略。
关于堆积还有一个大家容易忽略的“扩展陷阱”:Kafka的分区数是Topic并发度的上限,只有分区数够了,增加消费者实例才有意义。你要是加了20个消费者但Topic只有5个分区,那15个消费者实例是闲着的。这个点我在面试中提问过,一半以上的人没意识到。
7. 分布式锁:基于Redis和ZooKeeper的实现差异,以及它们的致命坑
分布式锁这道题,考察的是你对分布式协调、一致性模型的理解。但凡做过分布式系统,就一定会在某个节点碰到“对同一资源做互斥操作”的问题。
先说基于Redis的实现。最简单的方案是SET key value NX EX 30,同一时间只有一个客户端能SET成功,拿到锁的客户端处理完业务后调用DEL释放锁。但这个方案有几个经典坑:第一,锁的过期时间设短了,业务没执行完锁就过期了,其他线程趁虚而入;设长了,持有锁的节点宕机后锁要等很久才释放。解决方案是锁续期——Redisson的watchDog会默认每10秒为未释放的锁自动续期到30秒,避免业务长任务导致锁失效。第二,直接DEL有可能释放掉别人的锁,所以释放前要校验锁的value是否对应当前请求的唯一标识,再用Lua脚本实现先对比再删除,保证原子性。
Redis锁还有个更深层的风险:主从Redis架构下,如果Master节点加锁成功后还没来得及同步到Slave节点就宕机了,哨兵切换之后新Master没有这把锁的信息,其他线程就能成功加锁,造成并发安全。要解决这个问题,要么用Redisson的RedLock算法(在多个Redis节点上加锁,超过半数成功才算加锁成功),要么直接改用ZooKeeper或etcd实现。
ZooKeeper锁的原理是基于ZNode的临时顺序节点:每个客户端创建一个临时顺序节点,最小的节点获得锁;其他节点监听前一个节点,当前一个节点被删除(锁释放)时,后一个节点被唤醒尝试加锁。相比Redis锁,ZK锁没有过期时间问题,因为临时节点在会话断开时会自动删除,不会死锁。但ZK锁也有短板:性能不高,频繁创建删除ZNode对ZK集群压力很大,不适合超高QPS的场景。
如果你的面试回答里能把这个“Redis锁 vs ZooKeeper锁”的对比讲清楚,再结合自己项目里踩过的“误删锁”“锁过期业务没跑完”的案例,面试官基本就会认定你在分布式锁上有实战经验。
8. 限流算法这么多,你觉得哪个最好?Sentinel是怎么做的?
限流是保护系统不被流量冲垮的最后一道防线。面试题往往会从算法展开:计数器、滑动窗口、漏桶、令牌桶,你熟悉吗?它们的区别是什么?你项目里用的哪个?
计数器最简单,单位时间窗口内计数,超过阈值就拒绝,但临界问题严重:假设1分钟内限流100次,用户在第59秒发100个请求,第60秒再发100个请求,两秒内就放过去了200个。滑动窗口把时间窗口划分为很多小格子,滚动统计,能缓解计数器的临界问题,但实现复杂度上升。漏桶算法将请求放入桶中,底部以固定速率漏出,相当于恒定速率处理请求,能很好控制突发流量,但无法应对短时间突发的高流量——哪怕桶是空的,也只能以固定速率处理。令牌桶算法则以固定速率生成令牌放入桶中,请求拿到令牌才能通过,当桶内积攒了一定数量的令牌时,就能临时允许一段突发流量通过。
从实际使用来看,令牌桶是最均衡的方案,既能平滑流量,又能容忍一定程度的突发,这也是Guava RateLimiter和Sentinel默认的流控模式的基础。但注意,Guava的RateLimiter是单机限流,分布式场景要结合Redis做统一限流,否则每台机器都放行同一配额,总量会被成倍放大。
你还可以主动提到Sentinel——这是阿里的开源流量治理组件,目前微服务高并发治理的实战主流方案。Sentinel不只是限流,还集成了熔断降级、系统自适应保护、热点参数限流、授权规则等功能,能直接在控制台上动态配置。Sentinel的流量控制有几种模式:快速失败(默认,超过阈值直接拒绝)、Warm Up(预热模式,让流量从低到高慢慢增加,避免冷启动时大量请求直接把系统打崩)、排队等待(匀速排队模式,让请求以匀速通过,类似漏桶)。你用Sentinel的时候,如果接口是冷启动的(比如某些中间件初始化耗时大),一定要开启Warm Up,不然预热期流量上来能把服务压挂。这个坑是我在实际上线时踩过的:新服务一部署完,流量直接推进来,JIT还没热起来,数据库连接池还在初始化,结果接口大量超时,扩容机器反而越来越慢。
9. 秒杀系统设计:从商品详情到扣库存的全链路如何扛住十万级QPS?
秒杀是面试中号称“高并发系统设计”的终极考题,因为它把前面提到的缓存、MQ、限流、分布式锁全部串起来了。面试官会给你一个场景:“假设你们平台要做一款爆品的秒杀活动,预估峰值QPS 10万,怎么设计?”你要展示的是完整的分层设计思路,而不是抓住某一层的技术细节猛讲。
我从设计秒杀系统时总结的四个核心分层:
第一层是前端动静分离。秒杀商品详情页、倒计时页等静态资源全部放到CDN,用户在浏览器端点击“立即抢购”后,请求只走后端动态接口,而不会加载大量的静态资源。同时可以做答题验证码或滑块拼图,过滤掉一部分自动化脚本流量。
第二层是网关限流 + 染色分流。在网关层对秒杀接口单独配置限流阈值,一旦超过预定QPS就快速失败返回“已售罄”或“排队中”,避免流量直接冲击后端核心服务。另外,秒杀流量可以染色标记,网关识别后转发到独立的秒杀服务集群,与主业务服务隔离,防止对普通商品浏览造成影响。
第三层是Redis预先扣减库存 + MQ异步落库。秒杀的核心逻辑不能直接把数据库库存读出来判断,否则数据库瞬间被打爆。正确做法是:系统启动时把库存预热到Redis,用Redis的原子操作(Lua脚本判断库存大于0就减一)实现瞬时扣减;扣减成功的请求再发送一条消息到MQ,由消费者异步更新数据库库存和生成订单。用户面对的是“Redis的快速反馈”,而真正重的数据库操作被异步化,削峰效果立竿见影。
第四层是兜底降级 + 限流 + 监控。如果流量继续超出预期,则直接丢弃部分流量(快速失败),同时监控Redis的QPS、连接数、内存,以及下游MQ的堆积量、数据库的慢查询数,一旦逼近阈值就动态调整限流阈值或进行服务降级。
关于秒杀系统面试,还有一个坑要提醒你:很多人在讲方案时说“扣减库存用Redis的DECR就行”——太天真了。DECR无法做到“库存不足时不扣减”,并发下会出现超卖。你必须用Lua脚本把“判断库存 > 0”和“执行扣减”放在同一个原子操作里。我当时接手一个退款超卖项目时,发现代码是先把库存查出来,再在应用层判断if (stock > 0) 然后执行扣减,两个操作之间有窗口期,并发场景下必然超卖。换成Redis Lua脚本后,问题瞬间消失。
10. 全链路压测怎么做?怎么让压测数据可信且不影响真实用户?
最后一个问题,我觉得也是最考验“真实项目经历”的题目。面试官问“你们系统做过压测吗?怎么做的?”,你如果只回答“用了JMeter,压了2000个线程”是远远不够的,因为那只是接口测试,不是全链路压测。
全链路压测的核心目标,是在接近真实业务流量、真实数据量、真实环境的情况下,评估当前系统的容量上限并找出瓶颈。压测数据要可信,至少要解决三件事:
一是压测环境隔离。你总不能拿测试流量打到生产库吧?主流的做法是:压测流量通过网关识别之后,路由到影子库/影子表(Shadow Table),压测产生的数据只写影子结点,不会污染真实业务数据。这要求在代码里对压测请求做一个标记传输(比如Header里带压测标识),从入口到HTTP Client、RPC框架、数据库访问层,所有组件都要识别这个标记并自动路由到影子资源,改造工作量很大。
二是压测数据构造。模拟数据不能是简单的全零字段,而是要符合真实分布。比如用户ID的分布、SKU的热度分布、订单的状态分布,很多线上系统里90%的流量都打在10%的商品上,如果你压测数据是全平均的,压出来的结果对容量评估参考价值也不大。想让数据接近真实,可以从生产环境脱敏抽取一小部分数据,再放大克隆。
三是施压模型和观测。施压工具现在业界用得比较多的是阿里开源的PTS或是自研的压测平台,能动态产生千万级QPS。施压过程中要同时观测:各个微服务的CPU、内存、GC、线程池活跃度、数据库连接池活跃数、慢SQL、Redis命中率、MQ堆积量等等,压力从低到高递增,找到每个节点的瓶颈拐点。压完之后还需输出容量报告,例如“网关QPS上限2万,超了之后延迟陡增;应用层线程池是瓶颈;数据库连接池不是”,每个瓶颈都要有后续的优化建议。
关于这道题,建议大家结合自己做过的案例来讲,比如“我们针对双11大促做了三轮压测,第一轮发现Redis热点Key导致CPU飙高,第二轮发现数据库连接池偏小,第三轮压测数据才稳定在全链路支持每秒3万单”。这种实际的迭代过程,比背任何概念都有说服力。
最后分享一个面试答题的小技巧:不管面试官问的是限流、缓存、还是消息队列,你一定要把话题往“线上具体场景”上靠,哪怕只是“我当时遇到QPS冲到5000时发现...”,也比空谈理论更容易感染对方。高并发基础的知识点,网上看几天就能补齐,但遇到问题时稳定快速的排查思路,只能靠一次次线上故障、一次次夜深人静的处理中积累起来。我自己带团队的时候,最看重候选人讲踩坑故事时的细节——比如锁过期时间设了多少秒、为什么选择这个值、出现问题时日志是什么——这些细节骗不了人,也是任何面试题都无法伪装的硬实力。希望这篇内容能帮你在下一次高并发面试里少一点紧张,多一点从容。