十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

高并发面试必问10题:缓存、锁、限流与秒杀系统实战解析

高并发面试必问10题:缓存、锁、限流与秒杀系统实战解析 说实话这两年我面试别人和被别人面试问得最多的就是高并发。不是大家故意卷而是高并发这个问题一头连着业务场景另一头连着基础原理从一条问题链能串出缓存、队列、锁、线程池、数据库、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时发现...”也比空谈理论更容易感染对方。高并发基础的知识点网上看几天就能补齐但遇到问题时稳定快速的排查思路只能靠一次次线上故障、一次次夜深人静的处理中积累起来。我自己带团队的时候最看重候选人讲踩坑故事时的细节——比如锁过期时间设了多少秒、为什么选择这个值、出现问题时日志是什么——这些细节骗不了人也是任何面试题都无法伪装的硬实力。希望这篇内容能帮你在下一次高并发面试里少一点紧张多一点从容。
返回列表